🐛 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,15 +179,15 @@ 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();
} }
final dbLocalMessages = filteredMessages; final dbLocalMessages = filteredMessages;
@@ -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,14 +298,13 @@ 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, MessageStatus.sent,
MessageStatus.sent, );
); }).toList();
}).toList();
for (final message in messages) { for (final message in messages) {
await _database.saveMessageWithSender(message); await _database.saveMessageWithSender(message);
@@ -319,20 +332,21 @@ class MessagesNotifier extends _$MessagesNotifier {
_allRemoteMessagesFetched = false; _allRemoteMessagesFetched = false;
talker.log('Starting message sync'); talker.log('Starting message sync');
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true)); if (!_disposed) {
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, fetchAccount: _fetchAccount,
fetchAccount: _fetchAccount, );
);
if (lastMessage == null) { if (lastMessage == null) {
talker.log('No local messages, fetching from network'); talker.log('No local messages, fetching from network');
@@ -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');
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true)); if (!_disposed) {
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,18 +585,17 @@ 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, _) { _fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0; onProgress?.call(
onProgress?.call( localMessage.id,
localMessage.id, _fileUploadProgress[localMessage.id] ?? {},
_fileUploadProgress[localMessage.id] ?? {}, );
); },
}, ).future;
).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');
} }
@@ -606,22 +631,20 @@ 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, ) // update original message
) // update original message .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; }
} return m;
return m; }).toList();
}).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
} }
talker.log('Message with nonce $nonce sent successfully'); talker.log('Message with nonce $nonce sent successfully');
@@ -638,13 +661,12 @@ 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; }
} return m;
return m; }).toList();
}).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
showErrorAlert(e); showErrorAlert(e);
} }
@@ -686,13 +708,12 @@ 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; }
} return m;
return m; }).toList();
}).toList();
state = AsyncValue.data(newMessages); state = AsyncValue.data(newMessages);
} catch (e, stackTrace) { } catch (e, stackTrace) {
talker.log( talker.log(
@@ -707,13 +728,12 @@ 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; }
} return m;
return m; }).toList();
}).toList();
state = AsyncValue.data(_sortMessages(newMessages)); state = AsyncValue.data(_sortMessages(newMessages));
showErrorAlert(e); showErrorAlert(e);
} }
@@ -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
ref.read(flashingMessagesProvider.notifier).state = {}; if (!_disposed) {
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,13 +26,12 @@ 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) .map((e) => e.key)
.map((e) => e.key) .last;
.last;
final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;
@@ -51,27 +50,27 @@ 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( kReactionTemplates[mostReaction]?.icon ?? '',
kReactionTemplates[mostReaction]?.icon ?? '', style: const TextStyle(fontSize: 20),
style: const TextStyle(fontSize: 20), ),
), const Gap(4),
const Gap(4), Text(
Text( 'x${item.reactionsCount[mostReaction]}',
'x${item.reactionsCount[mostReaction]}', style: const TextStyle(fontSize: 11),
style: const TextStyle(fontSize: 11), ),
), ],
], )
) : null,
: null,
), ),
PostBody( PostBody(
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(), }),
], ],
); );
}, },