From 2e9d61bcfafbffb38be4867a29b70912c8b41b3b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Nov 2025 12:40:52 +0800 Subject: [PATCH] :sparkles: Chat unread indicator across all chat --- lib/pods/chat/call.g.dart | 2 +- lib/pods/chat/chat_summary.dart | 62 ++++++++++++++++++++++++++ lib/pods/chat/chat_summary.g.dart | 18 ++++++++ lib/pods/chat/messages_notifier.g.dart | 2 +- lib/screens/tabs.dart | 9 +++- 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/lib/pods/chat/call.g.dart b/lib/pods/chat/call.g.dart index 1b07c32b..be1ee608 100644 --- a/lib/pods/chat/call.g.dart +++ b/lib/pods/chat/call.g.dart @@ -6,7 +6,7 @@ part of 'call.dart'; // RiverpodGenerator // ************************************************************************** -String _$callNotifierHash() => r'2caee30f42315e539cb4df17c0d464ceed41ffa0'; +String _$callNotifierHash() => r'ef4e3e9c9d411cf9dce1ceb456a3b866b2c87db3'; /// See also [CallNotifier]. @ProviderFor(CallNotifier) diff --git a/lib/pods/chat/chat_summary.dart b/lib/pods/chat/chat_summary.dart index a101ed44..f2c4ba7b 100644 --- a/lib/pods/chat/chat_summary.dart +++ b/lib/pods/chat/chat_summary.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:math' as math; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:island/models/chat.dart'; import 'package:island/pods/network.dart'; @@ -6,6 +8,58 @@ import 'package:island/pods/chat/chat_subscribe.dart'; part 'chat_summary.g.dart'; +@riverpod +class ChatUnreadCountNotifier extends _$ChatUnreadCountNotifier { + StreamSubscription? _subscription; + + @override + Future build() async { + // Subscribe to websocket events when this provider is built + _subscribeToWebSocket(); + + // Dispose the subscription when this provider is disposed + ref.onDispose(() { + _subscription?.cancel(); + }); + + try { + final client = ref.read(apiClientProvider); + final response = await client.get('/sphere/chat/unread'); + return (response.data as num).toInt(); + } catch (_) { + return 0; + } + } + + void _subscribeToWebSocket() { + final webSocketService = ref.read(websocketProvider); + _subscription = webSocketService.dataStream.listen((packet) { + if (packet.type == 'messages.new' && packet.data != null) { + final message = SnChatMessage.fromJson(packet.data!); + final currentSubscribed = ref.read(currentSubscribedChatIdProvider); + // Only increment if the message is not from the currently subscribed chat + if (message.chatRoomId != currentSubscribed) { + _incrementCounter(); + } + } + }); + } + + Future _incrementCounter() async { + final current = await future; + state = AsyncData(current + 1); + } + + Future decrement(int count) async { + final current = await future; + state = AsyncData(math.max(current - count, 0)); + } + + void clear() async { + state = AsyncData(0); + } +} + @riverpod class ChatSummary extends _$ChatSummary { @override @@ -41,6 +95,14 @@ class ChatSummary extends _$ChatSummary { state.whenData((summaries) { final summary = summaries[chatId]; if (summary != null) { + // Decrement global unread count + final unreadToDecrement = summary.unreadCount; + if (unreadToDecrement > 0) { + ref + .read(chatUnreadCountNotifierProvider.notifier) + .decrement(unreadToDecrement); + } + state = AsyncData({ ...summaries, chatId: SnChatSummary( diff --git a/lib/pods/chat/chat_summary.g.dart b/lib/pods/chat/chat_summary.g.dart index 34a83065..c51a7276 100644 --- a/lib/pods/chat/chat_summary.g.dart +++ b/lib/pods/chat/chat_summary.g.dart @@ -6,6 +6,24 @@ part of 'chat_summary.dart'; // RiverpodGenerator // ************************************************************************** +String _$chatUnreadCountNotifierHash() => + r'b8d93589dc37f772d4c3a07d9afd81c37026e57d'; + +/// See also [ChatUnreadCountNotifier]. +@ProviderFor(ChatUnreadCountNotifier) +final chatUnreadCountNotifierProvider = + AutoDisposeAsyncNotifierProvider.internal( + ChatUnreadCountNotifier.new, + name: r'chatUnreadCountNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatUnreadCountNotifierHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$ChatUnreadCountNotifier = AutoDisposeAsyncNotifier; String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4'; /// See also [ChatSummary]. diff --git a/lib/pods/chat/messages_notifier.g.dart b/lib/pods/chat/messages_notifier.g.dart index a469efce..9ae8989b 100644 --- a/lib/pods/chat/messages_notifier.g.dart +++ b/lib/pods/chat/messages_notifier.g.dart @@ -6,7 +6,7 @@ part of 'messages_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$messagesNotifierHash() => r'27e5d686d9204ba39adbd1838cf4a6eaea0ac85f'; +String _$messagesNotifierHash() => r'27ce32c54e317a04e1d554ed4a70a24e4503fdd1'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/screens/tabs.dart b/lib/screens/tabs.dart index 5ddb0084..347ca559 100644 --- a/lib/screens/tabs.dart +++ b/lib/screens/tabs.dart @@ -14,6 +14,7 @@ import 'package:island/widgets/navigation/fab_menu.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:island/pods/config.dart'; +import 'package:island/pods/chat/chat_summary.dart'; final currentRouteProvider = StateProvider((ref) => null); @@ -50,6 +51,8 @@ class TabsScreen extends HookConsumerWidget { notificationUnreadCountNotifierProvider, ); + final chatUnreadCount = ref.watch(chatUnreadCountNotifierProvider); + final wideScreen = isWideScreen(context); final destinations = [ @@ -59,7 +62,11 @@ class TabsScreen extends HookConsumerWidget { ), NavigationDestination( label: 'chat'.tr(), - icon: const Icon(Symbols.forum_rounded), + icon: Badge.count( + count: chatUnreadCount.value ?? 0, + isLabelVisible: (chatUnreadCount.value ?? 0) > 0, + child: const Icon(Symbols.forum_rounded), + ), ), NavigationDestination( label: 'realms'.tr(),