🐛 Fix bugs in message db

This commit is contained in:
2025-11-20 00:01:36 +08:00
parent 18e890d63c
commit 18d16fdd57
5 changed files with 283 additions and 450 deletions

View File

@@ -160,6 +160,7 @@ class AppDatabase extends _$AppDatabase {
String roomId, String roomId,
String query, { String query, {
bool? withAttachments, bool? withAttachments,
Future<SnAccount?> Function(String accountId)? fetchAccount,
}) async { }) async {
var selectStatement = select(chatMessages) var selectStatement = select(chatMessages)
..where((m) => m.roomId.equals(roomId)); ..where((m) => m.roomId.equals(roomId));
@@ -186,7 +187,9 @@ class AppDatabase extends _$AppDatabase {
..orderBy([(m) => OrderingTerm.desc(m.createdAt)])) ..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
.get(); .get();
final messageFutures = final messageFutures =
messages.map((msg) => companionToMessage(msg)).toList(); messages
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
.toList();
return await Future.wait(messageFutures); return await Future.wait(messageFutures);
} }
@@ -215,18 +218,19 @@ class AppDatabase extends _$AppDatabase {
); );
} }
Future<LocalChatMessage> companionToMessage(ChatMessage dbMessage) async { Future<LocalChatMessage> companionToMessage(
ChatMessage dbMessage, {
Future<SnAccount?> Function(String accountId)? fetchAccount,
}) async {
final data = jsonDecode(dbMessage.data); final data = jsonDecode(dbMessage.data);
SnChatMember? sender; SnChatMember? sender;
try { try {
final senderRow = final senderRow =
await (select(chatMembers) await (select(chatMembers)
..where((m) => m.id.equals(dbMessage.senderId))).getSingle(); ..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
final senderAccount = SnAccount.fromJson(senderRow.account); SnAccount senderAccount;
SnAccountStatus? senderStatus; senderAccount = SnAccount.fromJson(senderRow.account);
if (senderRow.status != null) {
senderStatus = SnAccountStatus.fromJson(jsonDecode(senderRow.status!));
}
sender = SnChatMember( sender = SnChatMember(
id: senderRow.id, id: senderRow.id,
chatRoomId: senderRow.chatRoomId, chatRoomId: senderRow.chatRoomId,
@@ -239,15 +243,57 @@ class AppDatabase extends _$AppDatabase {
breakUntil: senderRow.breakUntil, breakUntil: senderRow.breakUntil,
timeoutUntil: senderRow.timeoutUntil, timeoutUntil: senderRow.timeoutUntil,
isBot: senderRow.isBot, isBot: senderRow.isBot,
status: senderStatus, status: null,
lastTyped: senderRow.lastTyped, lastTyped: senderRow.lastTyped,
createdAt: senderRow.createdAt, createdAt: senderRow.createdAt,
updatedAt: senderRow.updatedAt, updatedAt: senderRow.updatedAt,
deletedAt: senderRow.deletedAt, deletedAt: senderRow.deletedAt,
chatRoom: null, chatRoom: null,
); );
} catch (_) { } catch (err) {
sender = null; // 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( return LocalChatMessage(
id: dbMessage.id, id: dbMessage.id,
@@ -377,4 +423,10 @@ class AppDatabase extends _$AppDatabase {
return await (select(postDrafts) return await (select(postDrafts)
..where((tbl) => tbl.id.equals(id))).getSingleOrNull(); ..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
} }
Future<void> saveMember(SnChatMember member) async {
await into(
chatMembers,
).insert(companionFromMember(member), mode: InsertMode.insertOrReplace);
}
} }

View File

@@ -158,10 +158,11 @@ class LocalChatMessage {
}); });
SnChatMessage toRemoteMessage() { SnChatMessage toRemoteMessage() {
final msgData = Map<String, dynamic>.from(data); if (sender == null) {
if (sender != null) { throw Exception('Cannot create remote message without sender');
msgData['sender'] = sender!.toJson();
} }
final msgData = Map<String, dynamic>.from(data);
msgData['sender'] = sender!.toJson();
return SnChatMessage.fromJson(msgData); return SnChatMessage.fromJson(msgData);
} }
@@ -170,8 +171,20 @@ class LocalChatMessage {
MessageStatus status, { MessageStatus status, {
String? nonce, String? nonce,
}) { }) {
final msgData = Map<String, dynamic>.from(message.toJson()) final jsonData = message.toJson();
..remove('sender'); jsonData.remove('sender');
// Ensure proper defaults for collections to prevent type cast errors
if (jsonData['meta'] == null) jsonData['meta'] = <String, dynamic>{};
if (jsonData['members_mentioned'] == null) {
jsonData['members_mentioned'] = <String>[];
}
if (jsonData['attachments'] == null) {
jsonData['attachments'] = <Map<String, dynamic>>[];
}
if (jsonData['reactions'] == null) {
jsonData['reactions'] = <Map<String, dynamic>>[];
}
final msgData = Map<String, dynamic>.from(jsonData);
return LocalChatMessage( return LocalChatMessage(
id: message.id, id: message.id,
roomId: message.chatRoomId, roomId: message.chatRoomId,

View File

@@ -6,6 +6,7 @@ import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:island/database/drift_db.dart"; import "package:island/database/drift_db.dart";
import "package:island/database/message.dart"; import "package:island/database/message.dart";
import "package:island/models/account.dart";
import "package:island/models/chat.dart"; import "package:island/models/chat.dart";
import "package:island/models/file.dart"; import "package:island/models/file.dart";
import "package:island/models/poll.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:uuid/uuid.dart";
import "package:island/screens/chat/chat.dart"; import "package:island/screens/chat/chat.dart";
import "package:island/pods/chat/chat_rooms.dart"; import "package:island/pods/chat/chat_rooms.dart";
import "package:island/screens/account/profile.dart";
part 'messages_notifier.g.dart'; part 'messages_notifier.g.dart';
@@ -45,6 +47,8 @@ class MessagesNotifier extends _$MessagesNotifier {
bool _isUpdatingState = false; bool _isUpdatingState = false;
DateTime? _lastPauseTime; DateTime? _lastPauseTime;
late final Future<SnAccount?> Function(String) _fetchAccount;
@override @override
FutureOr<List<LocalChatMessage>> build(String roomId) async { FutureOr<List<LocalChatMessage>> build(String roomId) async {
_roomId = roomId; _roomId = roomId;
@@ -53,6 +57,15 @@ class MessagesNotifier extends _$MessagesNotifier {
final room = await ref.watch(chatroomProvider(roomId).future); final room = await ref.watch(chatroomProvider(roomId).future);
final identity = await ref.watch(chatroomIdentityProvider(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) { if (room == null) {
throw Exception('Room not found'); throw Exception('Room not found');
} }
@@ -133,6 +146,7 @@ class MessagesNotifier extends _$MessagesNotifier {
_roomId, _roomId,
searchQuery, searchQuery,
withAttachments: withAttachments, withAttachments: withAttachments,
fetchAccount: _fetchAccount,
); );
} else { } else {
final chatMessagesFromDb = await _database.getMessagesForRoom( final chatMessagesFromDb = await _database.getMessagesForRoom(
@@ -142,7 +156,12 @@ class MessagesNotifier extends _$MessagesNotifier {
); );
dbMessages = await Future.wait( dbMessages = await Future.wait(
chatMessagesFromDb chatMessagesFromDb
.map((msg) => _database.companionToMessage(msg)) .map(
(msg) => _database.companionToMessage(
msg,
fetchAccount: _fetchAccount,
),
)
.toList(), .toList(),
); );
} }
@@ -207,7 +226,10 @@ class MessagesNotifier extends _$MessagesNotifier {
); );
final dbMessages = await Future.wait( final dbMessages = await Future.wait(
chatMessagesFromDb chatMessagesFromDb
.map((msg) => _database.companionToMessage(msg)) .map(
(msg) =>
_database.companionToMessage(msg, fetchAccount: _fetchAccount),
)
.toList(), .toList(),
); );
@@ -278,6 +300,9 @@ class MessagesNotifier extends _$MessagesNotifier {
for (final message in messages) { for (final message in messages) {
await _database.saveMessage(_database.messageToCompanion(message)); await _database.saveMessage(_database.messageToCompanion(message));
if (message.sender != null) {
await _database.saveMember(message.sender!); // Save/update member data
}
if (message.nonce != null) { if (message.nonce != null) {
_pendingMessages.removeWhere( _pendingMessages.removeWhere(
(_, pendingMsg) => pendingMsg.nonce == message.nonce, (_, pendingMsg) => pendingMsg.nonce == message.nonce,
@@ -306,7 +331,10 @@ class MessagesNotifier extends _$MessagesNotifier {
final lastMessage = final lastMessage =
dbMessages.isEmpty dbMessages.isEmpty
? null ? null
: await _database.companionToMessage(dbMessages.first); : await _database.companionToMessage(
dbMessages.first,
fetchAccount: _fetchAccount,
);
if (lastMessage == null) { if (lastMessage == null) {
talker.log('No local messages, fetching from network'); talker.log('No local messages, fetching from network');
@@ -474,6 +502,7 @@ class MessagesNotifier extends _$MessagesNotifier {
_pendingMessages[localMessage.id] = localMessage; _pendingMessages[localMessage.id] = localMessage;
_fileUploadProgress[localMessage.id] = {}; _fileUploadProgress[localMessage.id] = {};
await _database.saveMessage(_database.messageToCompanion(localMessage)); await _database.saveMessage(_database.messageToCompanion(localMessage));
await _database.saveMember(mockMessage.sender);
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
state = AsyncValue.data([localMessage, ...currentMessages]); state = AsyncValue.data([localMessage, ...currentMessages]);
@@ -894,7 +923,10 @@ class MessagesNotifier extends _$MessagesNotifier {
await (_database.select(_database.chatMessages) await (_database.select(_database.chatMessages)
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull(); ..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
if (localMessage != null) { if (localMessage != null) {
return _database.companionToMessage(localMessage); return _database.companionToMessage(
localMessage,
fetchAccount: _fetchAccount,
);
} }
final response = await _apiClient.get( final response = await _apiClient.get(

View File

@@ -148,9 +148,6 @@ class ChatRoomScreen extends HookConsumerWidget {
final inputKey = useMemoized(() => GlobalKey()); final inputKey = useMemoized(() => GlobalKey());
final inputHeight = useState<double>(80.0); final inputHeight = useState<double>(80.0);
// Track previous height for smooth animations
final previousInputHeight = usePrevious<double>(inputHeight.value);
// Periodic height measurement for dynamic sizing // Periodic height measurement for dynamic sizing
useEffect(() { useEffect(() {
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) { final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
@@ -624,428 +621,179 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
} }
Widget chatMessageListWidget(List<LocalChatMessage> messageList) => Widget chatMessageListWidget(
previousInputHeight != null && previousInputHeight != inputHeight.value List<LocalChatMessage> messageList,
? TweenAnimationBuilder<double>( ) => AnimatedPadding(
tween: Tween<double>( duration: const Duration(milliseconds: 200),
begin: previousInputHeight, curve: Curves.easeOut,
end: inputHeight.value, padding: EdgeInsets.only(
), top: 16,
duration: const Duration(milliseconds: 200), bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
curve: Curves.easeOut, ),
builder: child: SuperListView.builder(
(context, height, child) => SuperListView.builder( listController: listController,
listController: listController, controller: scrollController,
padding: EdgeInsets.only( reverse: true, // Show newest messages at the bottom
top: 16, itemCount: messageList.length,
bottom: findChildIndexCallback: (key) {
MediaQuery.of(context).padding.bottom + 8 + height, if (key is! ValueKey<String>) return null;
), final messageId = key.value.substring(messageKeyPrefix.length);
controller: scrollController, final index = messageList.indexWhere(
reverse: true, // Show newest messages at the bottom (m) => (m.nonce ?? m.id) == messageId,
itemCount: messageList.length, );
findChildIndexCallback: (key) { return index >= 0 ? index : null;
if (key is! ValueKey<String>) return null; },
final messageId = key.value.substring( extentEstimation: (_, _) => 40,
messageKeyPrefix.length, itemBuilder: (context, index) {
); final message = messageList[index];
final index = messageList.indexWhere( final nextMessage =
(m) => (m.nonce ?? m.id) == messageId, index < messageList.length - 1 ? messageList[index + 1] : null;
); final isLastInGroup =
// Return null for invalid indices to let SuperListView handle it properly nextMessage == null ||
return index >= 0 ? index : null; nextMessage.senderId != message.senderId ||
}, nextMessage.createdAt
extentEstimation: (_, _) => 40, .difference(message.createdAt)
itemBuilder: (context, index) { .inMinutes
final message = messageList[index]; .abs() >
final nextMessage = 3;
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( final messageWidget = chatIdentity.when(
skipError: true, skipError: true,
data: data:
(identity) => GestureDetector( (identity) => GestureDetector(
onLongPress: () { onLongPress: () {
if (!isSelectionMode.value) { if (!isSelectionMode.value) {
toggleSelectionMode(); toggleSelectionMode();
toggleMessageSelection(message.id); toggleMessageSelection(message.id);
} }
}, },
onTap: () { onTap: () {
if (isSelectionMode.value) { if (isSelectionMode.value) {
toggleMessageSelection(message.id); toggleMessageSelection(message.id);
} }
}, },
child: Container( child: Container(
color: color:
selectedMessages.value.contains(message.id) selectedMessages.value.contains(message.id)
? Theme.of(context) ? Theme.of(
.colorScheme context,
.primaryContainer ).colorScheme.primaryContainer.withOpacity(0.3)
.withOpacity(0.3) : null,
: null, child: Stack(
child: Stack( children: [
children: [ MessageItem(
MessageItem( key: settings.disableAnimation ? key : null,
key: message: message,
settings.disableAnimation isCurrentUser: identity?.id == message.senderId,
? key onAction:
: null, isSelectionMode.value
message: message, ? null
isCurrentUser: : (action) {
identity?.id == message.senderId, switch (action) {
onAction: case MessageItemAction.delete:
isSelectionMode.value messagesNotifier.deleteMessage(
? null message.id,
: (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,
); );
}, case MessageItemAction.edit:
progress: messageEditingTo.value =
attachmentProgress.value[message.id], message.toRemoteMessage();
showAvatar: isLastInGroup, messageController.text =
isSelectionMode: isSelectionMode.value, messageEditingTo.value?.content ??
isSelected: selectedMessages.value '';
.contains(message.id), attachments.value =
onToggleSelection: toggleMessageSelection, messageEditingTo.value!.attachments
onEnterSelectionMode: () { .map(
if (!isSelectionMode.value) { (e) =>
toggleSelectionMode(); UniversalFile.fromAttachment(
} e,
}, ),
), )
if (selectedMessages.value.contains( .toList();
message.id, case MessageItemAction.forward:
)) messageForwardingTo.value =
...([ message.toRemoteMessage();
Positioned( case MessageItemAction.reply:
top: 8, messageReplyingTo.value =
right: 8, message.toRemoteMessage();
child: Container( case MessageItemAction.resend:
width: 16, messagesNotifier.retryMessage(
height: 16, message.id,
decoration: BoxDecoration( );
color: }
Theme.of( },
context, onJump:
).colorScheme.primary, (messageId) => scrollToMessage(
shape: BoxShape.circle, messageId: messageId,
), messageList: messageList,
child: Icon( messagesNotifier: messagesNotifier,
Icons.check, listController: listController,
size: 12, scrollController: scrollController,
color: ref: ref,
Theme.of(
context,
).colorScheme.onPrimary,
),
),
),
]),
],
),
), ),
), progress: attachmentProgress.value[message.id],
loading: showAvatar: isLastInGroup,
() => MessageItem( isSelectionMode: isSelectionMode.value,
message: message, isSelected: selectedMessages.value.contains(
isCurrentUser: false, message.id,
onAction: null,
progress: null,
showAvatar: false,
onJump: (_) {},
),
error: (_, _) => const SizedBox.shrink(),
);
return settings.disableAnimation
? messageWidget
: TweenAnimationBuilder<double>(
key: key,
tween: Tween<double>(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<String>) 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,
),
),
),
]),
],
), ),
onToggleSelection: toggleMessageSelection,
onEnterSelectionMode: () {
if (!isSelectionMode.value) toggleSelectionMode();
},
), ),
), if (selectedMessages.value.contains(message.id))
loading: Positioned(
() => MessageItem( top: 8,
message: message, right: 8,
isCurrentUser: false, child: Container(
onAction: null, width: 16,
progress: null, height: 16,
showAvatar: false, decoration: BoxDecoration(
onJump: (_) {}, color: Theme.of(context).colorScheme.primary,
), shape: BoxShape.circle,
error: (_, _) => const SizedBox.shrink(), ),
); 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 return settings.disableAnimation
? messageWidget ? messageWidget
: TweenAnimationBuilder<double>( : TweenAnimationBuilder<double>(
key: key, key: key,
tween: Tween<double>(begin: 0.0, end: 1.0), tween: Tween<double>(begin: 0.0, end: 1.0),
duration: Duration( duration: Duration(milliseconds: 400 + (index % 5) * 50),
milliseconds: 400 + (index % 5) * 50, curve: Curves.easeOutCubic,
), // Staggered delay builder:
curve: Curves.easeOutCubic, (context, animationValue, child) => Transform.translate(
builder: (context, animationValue, child) { offset: Offset(0, 20 * (1 - animationValue)),
return Transform.translate( child: Opacity(opacity: animationValue, child: child),
offset: Offset( ),
0, child: messageWidget,
20 * (1 - animationValue), );
), // Slide up from bottom },
child: Opacity(opacity: animationValue, child: child), ),
); );
},
child: messageWidget,
);
},
);
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(

View File

@@ -314,9 +314,6 @@ class AppScaffold extends HookConsumerWidget {
return null; return null;
}, []); }, []);
final appBarHeight = appBar?.preferredSize.height ?? 0;
final safeTop = MediaQuery.of(context).padding.top;
final noBackground = isNoBackground ?? isWideScreen(context); final noBackground = isNoBackground ?? isWideScreen(context);
final builtWidget = Focus( final builtWidget = Focus(
@@ -325,16 +322,7 @@ class AppScaffold extends HookConsumerWidget {
extendBody: extendBody ?? true, extendBody: extendBody ?? true,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: Column( body: body,
children: [
IgnorePointer(
child: SizedBox(
height: appBar != null ? appBarHeight + safeTop : 0,
),
),
if (body != null) Expanded(child: body!),
],
),
appBar: appBar, appBar: appBar,
bottomNavigationBar: bottomNavigationBar, bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet, bottomSheet: bottomSheet,