🐛 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;
// Disposal handling
bool _disposed = false;
@override
FutureOr<List<LocalChatMessage>> build(String roomId) async {
_apiClient = ref.watch(apiClientProvider);
@@ -76,10 +79,17 @@ class MessagesNotifier extends _$MessagesNotifier {
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
if (identity != null) {
ref.listen(appLifecycleStateProvider, (_, next) {
next.whenData((state) {
if (_disposed) return; // Check disposal before accessing ref
if (state == AppLifecycleState.paused) {
_lastPauseTime = DateTime.now();
talker.log('App paused, recording time');
@@ -88,7 +98,9 @@ class MessagesNotifier extends _$MessagesNotifier {
final diff = DateTime.now().difference(_lastPauseTime!);
if (diff > const Duration(minutes: 1)) {
talker.log('App resumed after >1 min, syncing messages');
syncMessages();
if (!_disposed) {
syncMessages(); // Check disposal before calling syncMessages
}
} else {
talker.log('App resumed within 1 min, skipping sync');
}
@@ -167,15 +179,15 @@ class MessagesNotifier extends _$MessagesNotifier {
List<LocalChatMessage> filteredMessages = dbMessages;
if (withLinks == true) {
filteredMessages =
filteredMessages.where((msg) => _hasLink(msg)).toList();
filteredMessages = filteredMessages
.where((msg) => _hasLink(msg))
.toList();
}
if (withAttachments == true) {
filteredMessages =
filteredMessages
.where((msg) => msg.toRemoteMessage().attachments.isNotEmpty)
.toList();
filteredMessages = filteredMessages
.where((msg) => msg.toRemoteMessage().attachments.isNotEmpty)
.toList();
}
final dbLocalMessages = filteredMessages;
@@ -190,8 +202,9 @@ class MessagesNotifier extends _$MessagesNotifier {
}
if (offset == 0) {
final pendingForRoom =
_pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
final pendingForRoom = _pendingMessages.values
.where((msg) => msg.roomId == roomId)
.toList();
final allMessages = [...pendingForRoom, ...uniqueMessages];
_sortMessages(allMessages); // Use the helper function
@@ -239,8 +252,9 @@ class MessagesNotifier extends _$MessagesNotifier {
}
if (offset == 0) {
final pendingForRoom =
_pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
final pendingForRoom = _pendingMessages.values
.where((msg) => msg.roomId == roomId)
.toList();
final allMessages = [...pendingForRoom, ...uniqueMessages];
_sortMessages(allMessages);
@@ -284,14 +298,13 @@ class MessagesNotifier extends _$MessagesNotifier {
final List<dynamic> data = response.data;
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
final messages =
data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
}).toList();
final messages = data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
}).toList();
for (final message in messages) {
await _database.saveMessageWithSender(message);
@@ -319,20 +332,21 @@ class MessagesNotifier extends _$MessagesNotifier {
_allRemoteMessagesFetched = false;
talker.log('Starting message sync');
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
if (!_disposed) {
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
}
try {
final dbMessages = await _database.getMessagesForRoom(
_room.id,
offset: 0,
limit: 1,
);
final lastMessage =
dbMessages.isEmpty
? null
: await _database.companionToMessage(
dbMessages.first,
fetchAccount: _fetchAccount,
);
final lastMessage = dbMessages.isEmpty
? null
: await _database.companionToMessage(
dbMessages.first,
fetchAccount: _fetchAccount,
);
if (lastMessage == null) {
talker.log('No local messages, fetching from network');
@@ -347,8 +361,10 @@ class MessagesNotifier extends _$MessagesNotifier {
// Sync with pagination support using timestamp-based cursor
int? totalMessages;
int syncedCount = 0;
int lastSyncTimestamp =
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch;
int lastSyncTimestamp = lastMessage
.toRemoteMessage()
.updatedAt
.millisecondsSinceEpoch;
do {
final resp = await _apiClient.post(
@@ -395,7 +411,11 @@ class MessagesNotifier extends _$MessagesNotifier {
showErrorAlert(err);
} finally {
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;
}
}
@@ -492,7 +512,9 @@ class MessagesNotifier extends _$MessagesNotifier {
if (!_hasMore || state is AsyncLoading) return;
talker.log('Loading more messages');
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
if (!_disposed) {
Future.microtask(() => ref.read(chatSyncingProvider.notifier).set(true));
}
try {
final currentMessages = state.value ?? [];
final offset = currentMessages.length;
@@ -515,7 +537,11 @@ class MessagesNotifier extends _$MessagesNotifier {
);
showErrorAlert(err);
} 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 {
var cloudAttachments = List.empty(growable: true);
for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: attachments[idx],
onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
onProgress?.call(
localMessage.id,
_fileUploadProgress[localMessage.id] ?? {},
);
},
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: attachments[idx],
onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress ?? 0.0;
onProgress?.call(
localMessage.id,
_fileUploadProgress[localMessage.id] ?? {},
);
},
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
@@ -606,22 +631,20 @@ class MessagesNotifier extends _$MessagesNotifier {
final currentMessages = state.value ?? [];
if (editingTo != null) {
final newMessages =
currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map(
(m) => m.id == editingTo.id ? updatedMessage : m,
) // update original message
.toList();
final newMessages = currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map(
(m) => m.id == editingTo.id ? updatedMessage : m,
) // update original message
.toList();
state = AsyncValue.data(newMessages);
} else {
final newMessages =
currentMessages.map((m) {
if (m.id == localMessage.id) {
return updatedMessage;
}
return m;
}).toList();
final newMessages = currentMessages.map((m) {
if (m.id == localMessage.id) {
return updatedMessage;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
}
talker.log('Message with nonce $nonce sent successfully');
@@ -638,13 +661,12 @@ class MessagesNotifier extends _$MessagesNotifier {
localMessage.id,
MessageStatus.failed,
);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == localMessage.id) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
final newMessages = (state.value ?? []).map((m) {
if (m.id == localMessage.id) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
showErrorAlert(e);
}
@@ -686,13 +708,12 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(pendingMessageId);
await _database.saveMessageWithSender(updatedMessage);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return updatedMessage;
}
return m;
}).toList();
final newMessages = (state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return updatedMessage;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
} catch (e, stackTrace) {
talker.log(
@@ -707,13 +728,12 @@ class MessagesNotifier extends _$MessagesNotifier {
pendingMessageId,
MessageStatus.failed,
);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
final newMessages = (state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
state = AsyncValue.data(_sortMessages(newMessages));
showErrorAlert(e);
}
@@ -865,8 +885,9 @@ class MessagesNotifier extends _$MessagesNotifier {
await _database.deleteMessage(messageId);
final currentMessages = state.value ?? [];
final newMessages =
currentMessages.where((m) => m.id != messageId).toList();
final newMessages = currentMessages
.where((m) => m.id != messageId)
.toList();
state = AsyncValue.data(newMessages);
return;
}
@@ -969,9 +990,9 @@ class MessagesNotifier extends _$MessagesNotifier {
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
talker.log('Fetching message by id $messageId');
try {
final localMessage =
await (_database.select(_database.chatMessages)
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
final localMessage = await (_database.select(
_database.chatMessages,
)..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
if (localMessage != null) {
return _database.companionToMessage(
localMessage,
@@ -1005,7 +1026,9 @@ class MessagesNotifier extends _$MessagesNotifier {
_isJumping = true;
// Clear flashing messages when starting a new jump
ref.read(flashingMessagesProvider.notifier).state = {};
if (!_disposed) {
ref.read(flashingMessagesProvider.notifier).state = {};
}
try {
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
const chunkSize = 100; // Load 100 messages around the target
final offset =
(newerCount - chunkSize ~/ 2).clamp(0, double.infinity).toInt();
final offset = (newerCount - chunkSize ~/ 2)
.clamp(0, double.infinity)
.toInt();
talker.log(
'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
final currentIds = currentMessages.map((m) => m.id).toSet();
final newMessages =
loadedMessages.where((m) => !currentIds.contains(m.id)).toList();
final newMessages = loadedMessages
.where((m) => !currentIds.contains(m.id))
.toList();
talker.log(
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
);

View File

@@ -26,13 +26,12 @@ class PostItemScreenshot extends ConsumerWidget {
final renderingPadding =
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
final mostReaction =
item.reactionsCount.isEmpty
? null
: item.reactionsCount.entries
.sortedBy((e) => e.value)
.map((e) => e.key)
.last;
final mostReaction = item.reactionsCount.isEmpty
? null
: item.reactionsCount.entries
.sortedBy((e) => e.value)
.map((e) => e.key)
.last;
final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;
@@ -51,27 +50,27 @@ class PostItemScreenshot extends ConsumerWidget {
isInteractive: false,
renderingPadding: renderingPadding,
isRelativeTime: false,
trailing:
mostReaction != null
? Row(
children: [
Text(
kReactionTemplates[mostReaction]?.icon ?? '',
style: const TextStyle(fontSize: 20),
),
const Gap(4),
Text(
'x${item.reactionsCount[mostReaction]}',
style: const TextStyle(fontSize: 11),
),
],
)
: null,
trailing: mostReaction != null
? Row(
children: [
Text(
kReactionTemplates[mostReaction]?.icon ?? '',
style: const TextStyle(fontSize: 20),
),
const Gap(4),
Text(
'x${item.reactionsCount[mostReaction]}',
style: const TextStyle(fontSize: 11),
),
],
)
: null,
),
PostBody(
item: item,
renderingPadding: renderingPadding,
isFullPost: isFullPost,
isRelativeTime: false,
isTextSelectable: false,
isInteractive: false,
hideOverlay: true,

View File

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

View File

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