diff --git a/lib/pods/chat/messages_notifier.dart b/lib/pods/chat/messages_notifier.dart index 595bf620..18a45347 100644 --- a/lib/pods/chat/messages_notifier.dart +++ b/lib/pods/chat/messages_notifier.dart @@ -48,6 +48,9 @@ class MessagesNotifier extends _$MessagesNotifier { late Future Function(String) _fetchAccount; + // Disposal handling + bool _disposed = false; + @override FutureOr> 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 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 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 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', ); diff --git a/lib/widgets/post/post_item_screenshot.dart b/lib/widgets/post/post_item_screenshot.dart index 01081b8d..5b6cdb9f 100644 --- a/lib/widgets/post/post_item_screenshot.dart +++ b/lib/widgets/post/post_item_screenshot.dart @@ -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, diff --git a/lib/widgets/post/post_shared.dart b/lib/widgets/post/post_shared.dart index 0970d210..4bf14732 100644 --- a/lib/widgets/post/post_shared.dart +++ b/lib/widgets/post/post_shared.dart @@ -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], ), ); } diff --git a/lib/widgets/posts/post_subscription_filter.dart b/lib/widgets/posts/post_subscription_filter.dart index f3e51449..2dc4a8b3 100644 --- a/lib/widgets/posts/post_subscription_filter.dart +++ b/lib/widgets/posts/post_subscription_filter.dart @@ -240,7 +240,7 @@ class PostSubscriptionFilterWidget extends HookConsumerWidget { horizontal: 16, ), ); - }).toList(), + }), ], ); },