🐛 Fixes the lifecycle issue of chat #211

This commit is contained in:
2025-12-27 22:48:55 +08:00
parent f541580281
commit 6b0343d3dc
4 changed files with 138 additions and 124 deletions

View File

@@ -48,6 +48,9 @@ class MessagesNotifier extends _$MessagesNotifier {
late Future<SnAccount?> Function(String) _fetchAccount; late Future<SnAccount?> Function(String) _fetchAccount;
// Disposal handling
bool _disposed = false;
@override @override
FutureOr<List<LocalChatMessage>> build(String roomId) async { FutureOr<List<LocalChatMessage>> build(String roomId) async {
_apiClient = ref.watch(apiClientProvider); _apiClient = ref.watch(apiClientProvider);
@@ -76,10 +79,17 @@ class MessagesNotifier extends _$MessagesNotifier {
talker.log('MessagesNotifier built for room $roomId'); talker.log('MessagesNotifier built for room $roomId');
// Set up disposal handling
ref.onDispose(() {
_disposed = true;
talker.log('MessagesNotifier disposed for room $roomId');
});
// Only setup sync and lifecycle listeners if user is a member // Only setup sync and lifecycle listeners if user is a member
if (identity != null) { if (identity != null) {
ref.listen(appLifecycleStateProvider, (_, next) { ref.listen(appLifecycleStateProvider, (_, next) {
next.whenData((state) { next.whenData((state) {
if (_disposed) return; // Check disposal before accessing ref
if (state == AppLifecycleState.paused) { if (state == AppLifecycleState.paused) {
_lastPauseTime = DateTime.now(); _lastPauseTime = DateTime.now();
talker.log('App paused, recording time'); talker.log('App paused, recording time');
@@ -88,7 +98,9 @@ class MessagesNotifier extends _$MessagesNotifier {
final diff = DateTime.now().difference(_lastPauseTime!); final diff = DateTime.now().difference(_lastPauseTime!);
if (diff > const Duration(minutes: 1)) { if (diff > const Duration(minutes: 1)) {
talker.log('App resumed after >1 min, syncing messages'); talker.log('App resumed after >1 min, syncing messages');
syncMessages(); if (!_disposed) {
syncMessages(); // Check disposal before calling syncMessages
}
} else { } else {
talker.log('App resumed within 1 min, skipping sync'); talker.log('App resumed within 1 min, skipping sync');
} }
@@ -167,13 +179,13 @@ class MessagesNotifier extends _$MessagesNotifier {
List<LocalChatMessage> filteredMessages = dbMessages; List<LocalChatMessage> filteredMessages = dbMessages;
if (withLinks == true) { if (withLinks == true) {
filteredMessages = filteredMessages = filteredMessages
filteredMessages.where((msg) => _hasLink(msg)).toList(); .where((msg) => _hasLink(msg))
.toList();
} }
if (withAttachments == true) { if (withAttachments == true) {
filteredMessages = filteredMessages = filteredMessages
filteredMessages
.where((msg) => msg.toRemoteMessage().attachments.isNotEmpty) .where((msg) => msg.toRemoteMessage().attachments.isNotEmpty)
.toList(); .toList();
} }
@@ -190,8 +202,9 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
if (offset == 0) { if (offset == 0) {
final pendingForRoom = final pendingForRoom = _pendingMessages.values
_pendingMessages.values.where((msg) => msg.roomId == roomId).toList(); .where((msg) => msg.roomId == roomId)
.toList();
final allMessages = [...pendingForRoom, ...uniqueMessages]; final allMessages = [...pendingForRoom, ...uniqueMessages];
_sortMessages(allMessages); // Use the helper function _sortMessages(allMessages); // Use the helper function
@@ -239,8 +252,9 @@ class MessagesNotifier extends _$MessagesNotifier {
} }
if (offset == 0) { if (offset == 0) {
final pendingForRoom = final pendingForRoom = _pendingMessages.values
_pendingMessages.values.where((msg) => msg.roomId == roomId).toList(); .where((msg) => msg.roomId == roomId)
.toList();
final allMessages = [...pendingForRoom, ...uniqueMessages]; final allMessages = [...pendingForRoom, ...uniqueMessages];
_sortMessages(allMessages); _sortMessages(allMessages);
@@ -284,8 +298,7 @@ class MessagesNotifier extends _$MessagesNotifier {
final List<dynamic> data = response.data; final List<dynamic> data = response.data;
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0'); _totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
final messages = final messages = data.map((json) {
data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json); final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage( return LocalChatMessage.fromRemoteMessage(
remoteMessage, remoteMessage,
@@ -319,15 +332,16 @@ class MessagesNotifier extends _$MessagesNotifier {
_allRemoteMessagesFetched = false; _allRemoteMessagesFetched = false;
talker.log('Starting message sync'); talker.log('Starting message sync');
if (!_disposed) {
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true)); Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
}
try { try {
final dbMessages = await _database.getMessagesForRoom( final dbMessages = await _database.getMessagesForRoom(
_room.id, _room.id,
offset: 0, offset: 0,
limit: 1, limit: 1,
); );
final lastMessage = final lastMessage = dbMessages.isEmpty
dbMessages.isEmpty
? null ? null
: await _database.companionToMessage( : await _database.companionToMessage(
dbMessages.first, dbMessages.first,
@@ -347,8 +361,10 @@ class MessagesNotifier extends _$MessagesNotifier {
// Sync with pagination support using timestamp-based cursor // Sync with pagination support using timestamp-based cursor
int? totalMessages; int? totalMessages;
int syncedCount = 0; int syncedCount = 0;
int lastSyncTimestamp = int lastSyncTimestamp = lastMessage
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch; .toRemoteMessage()
.updatedAt
.millisecondsSinceEpoch;
do { do {
final resp = await _apiClient.post( final resp = await _apiClient.post(
@@ -395,7 +411,11 @@ class MessagesNotifier extends _$MessagesNotifier {
showErrorAlert(err); showErrorAlert(err);
} finally { } finally {
talker.log('Finished message sync'); talker.log('Finished message sync');
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(false)); if (!_disposed) {
Future.microtask(
() => ref.read(chatSyncingProvider.notifier).set(false),
);
}
_isSyncing = false; _isSyncing = false;
} }
} }
@@ -492,7 +512,9 @@ class MessagesNotifier extends _$MessagesNotifier {
if (!_hasMore || state is AsyncLoading) return; if (!_hasMore || state is AsyncLoading) return;
talker.log('Loading more messages'); talker.log('Loading more messages');
if (!_disposed) {
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true)); Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
}
try { try {
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
final offset = currentMessages.length; final offset = currentMessages.length;
@@ -515,7 +537,11 @@ class MessagesNotifier extends _$MessagesNotifier {
); );
showErrorAlert(err); showErrorAlert(err);
} finally { } finally {
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(false)); if (!_disposed) {
Future.microtask(
() => ref.read(chatSyncingProvider.notifier).set(false),
);
}
} }
} }
@@ -559,8 +585,7 @@ class MessagesNotifier extends _$MessagesNotifier {
try { try {
var cloudAttachments = List.empty(growable: true); var cloudAttachments = List.empty(growable: true);
for (var idx = 0; idx < attachments.length; idx++) { for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile = final cloudFile = await FileUploader.createCloudFile(
await FileUploader.createCloudFile(
ref: ref, ref: ref,
fileData: attachments[idx], fileData: attachments[idx],
onProgress: (progress, _) { onProgress: (progress, _) {
@@ -606,8 +631,7 @@ class MessagesNotifier extends _$MessagesNotifier {
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
if (editingTo != null) { if (editingTo != null) {
final newMessages = final newMessages = currentMessages
currentMessages
.where((m) => m.id != localMessage.id) // remove pending message .where((m) => m.id != localMessage.id) // remove pending message
.map( .map(
(m) => m.id == editingTo.id ? updatedMessage : m, (m) => m.id == editingTo.id ? updatedMessage : m,
@@ -615,8 +639,7 @@ class MessagesNotifier extends _$MessagesNotifier {
.toList(); .toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
} else { } else {
final newMessages = final newMessages = currentMessages.map((m) {
currentMessages.map((m) {
if (m.id == localMessage.id) { if (m.id == localMessage.id) {
return updatedMessage; return updatedMessage;
} }
@@ -638,8 +661,7 @@ class MessagesNotifier extends _$MessagesNotifier {
localMessage.id, localMessage.id,
MessageStatus.failed, MessageStatus.failed,
); );
final newMessages = final newMessages = (state.value ?? []).map((m) {
(state.value ?? []).map((m) {
if (m.id == localMessage.id) { if (m.id == localMessage.id) {
return m..status = MessageStatus.failed; return m..status = MessageStatus.failed;
} }
@@ -686,8 +708,7 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(pendingMessageId); await _database.deleteMessage(pendingMessageId);
await _database.saveMessageWithSender(updatedMessage); await _database.saveMessageWithSender(updatedMessage);
final newMessages = final newMessages = (state.value ?? []).map((m) {
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) { if (m.id == pendingMessageId) {
return updatedMessage; return updatedMessage;
} }
@@ -707,8 +728,7 @@ class MessagesNotifier extends _$MessagesNotifier {
pendingMessageId, pendingMessageId,
MessageStatus.failed, MessageStatus.failed,
); );
final newMessages = final newMessages = (state.value ?? []).map((m) {
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) { if (m.id == pendingMessageId) {
return m..status = MessageStatus.failed; return m..status = MessageStatus.failed;
} }
@@ -865,8 +885,9 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(messageId); await _database.deleteMessage(messageId);
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
final newMessages = final newMessages = currentMessages
currentMessages.where((m) => m.id != messageId).toList(); .where((m) => m.id != messageId)
.toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
return; return;
} }
@@ -969,9 +990,9 @@ class MessagesNotifier extends _$MessagesNotifier {
Future<LocalChatMessage?> fetchMessageById(String messageId) async { Future<LocalChatMessage?> fetchMessageById(String messageId) async {
talker.log('Fetching message by id $messageId'); talker.log('Fetching message by id $messageId');
try { try {
final localMessage = final localMessage = await (_database.select(
await (_database.select(_database.chatMessages) _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( return _database.companionToMessage(
localMessage, localMessage,
@@ -1005,7 +1026,9 @@ class MessagesNotifier extends _$MessagesNotifier {
_isJumping = true; _isJumping = true;
// Clear flashing messages when starting a new jump // Clear flashing messages when starting a new jump
if (!_disposed) {
ref.read(flashingMessagesProvider.notifier).state = {}; ref.read(flashingMessagesProvider.notifier).state = {};
}
try { try {
talker.log('Fetching message $messageId'); talker.log('Fetching message $messageId');
@@ -1047,8 +1070,9 @@ class MessagesNotifier extends _$MessagesNotifier {
// Calculate offset to position target message in the middle of the loaded chunk // Calculate offset to position target message in the middle of the loaded chunk
const chunkSize = 100; // Load 100 messages around the target const chunkSize = 100; // Load 100 messages around the target
final offset = final offset = (newerCount - chunkSize ~/ 2)
(newerCount - chunkSize ~/ 2).clamp(0, double.infinity).toInt(); .clamp(0, double.infinity)
.toInt();
talker.log( talker.log(
'Calculated offset $offset for target message (newer: $newerCount, chunk: $chunkSize)', 'Calculated offset $offset for target message (newer: $newerCount, chunk: $chunkSize)',
); );
@@ -1060,8 +1084,9 @@ class MessagesNotifier extends _$MessagesNotifier {
// Check if loaded messages are already in current state // Check if loaded messages are already in current state
final currentIds = currentMessages.map((m) => m.id).toSet(); final currentIds = currentMessages.map((m) => m.id).toSet();
final newMessages = final newMessages = loadedMessages
loadedMessages.where((m) => !currentIds.contains(m.id)).toList(); .where((m) => !currentIds.contains(m.id))
.toList();
talker.log( talker.log(
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new', 'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
); );

View File

@@ -26,8 +26,7 @@ class PostItemScreenshot extends ConsumerWidget {
final renderingPadding = final renderingPadding =
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8); padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
final mostReaction = final mostReaction = item.reactionsCount.isEmpty
item.reactionsCount.isEmpty
? null ? null
: item.reactionsCount.entries : item.reactionsCount.entries
.sortedBy((e) => e.value) .sortedBy((e) => e.value)
@@ -51,8 +50,7 @@ class PostItemScreenshot extends ConsumerWidget {
isInteractive: false, isInteractive: false,
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
isRelativeTime: false, isRelativeTime: false,
trailing: trailing: mostReaction != null
mostReaction != null
? Row( ? Row(
children: [ children: [
Text( Text(
@@ -72,6 +70,7 @@ class PostItemScreenshot extends ConsumerWidget {
item: item, item: item,
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
isFullPost: isFullPost, isFullPost: isFullPost,
isRelativeTime: false,
isTextSelectable: false, isTextSelectable: false,
isInteractive: false, isInteractive: false,
hideOverlay: true, hideOverlay: true,

View File

@@ -816,17 +816,7 @@ class PostBody extends ConsumerWidget {
Row( Row(
spacing: 8, spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [const Icon(Symbols.edit, size: 16), text],
const Icon(Symbols.edit, size: 16),
hideOverlay
? text
: Tooltip(
message: !isFullPost && isRelativeTime
? item.editedAt!.formatSystem()
: item.editedAt!.formatRelative(context),
child: text,
),
],
), ),
); );
} }

View File

@@ -240,7 +240,7 @@ class PostSubscriptionFilterWidget extends HookConsumerWidget {
horizontal: 16, horizontal: 16,
), ),
); );
}).toList(), }),
], ],
); );
}, },