From 321ea4458b6d0a317ba53c27e24d76950ed26380 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 16 Jan 2026 00:21:07 +0800 Subject: [PATCH] :lipstick: Optimize the notification overlays --- lib/pods/chat/chat_subscribe.dart | 11 +- lib/pods/notification.dart | 40 ++++++- lib/widgets/notification_item.dart | 149 +++++++++++++++----------- lib/widgets/notification_overlay.dart | 27 ++--- 4 files changed, 143 insertions(+), 84 deletions(-) diff --git a/lib/pods/chat/chat_subscribe.dart b/lib/pods/chat/chat_subscribe.dart index 2f7d4bb7..a1c7f3e3 100644 --- a/lib/pods/chat/chat_subscribe.dart +++ b/lib/pods/chat/chat_subscribe.dart @@ -223,7 +223,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier { // Add new typing status _typingStatuses.add(sender); } - state = List.of(_typingStatuses); + if (ref.mounted) state = List.of(_typingStatuses); return; } @@ -243,11 +243,14 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier { // Play sound for new messages when app is unfocused if (pkt.type == 'messages.new' && message.senderId != _chatIdentity.id && - ref.read(appLifecycleStateProvider).value != AppLifecycleState.resumed && + ref.read(appLifecycleStateProvider).value != + AppLifecycleState.resumed && ref.read(appSettingsProvider).soundEffects) { final player = AudioPlayer(); await player.setVolume(0.75); - await player.setAudioSource(AudioSource.asset('assets/audio/messages.mp3')); + await player.setAudioSource( + AudioSource.asset('assets/audio/messages.mp3'), + ); await player.play(); player.dispose(); } @@ -288,4 +291,4 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier { _typingCooldownTimer = null; }); } -} \ No newline at end of file +} diff --git a/lib/pods/notification.dart b/lib/pods/notification.dart index c6eef2b7..ebb2f7d9 100644 --- a/lib/pods/notification.dart +++ b/lib/pods/notification.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:island/models/account.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:uuid/uuid.dart'; @@ -13,6 +15,7 @@ class NotificationItem { final DateTime createdAt; final int index; final Duration duration; + final bool dismissed; NotificationItem({ String? id, @@ -20,11 +23,30 @@ class NotificationItem { DateTime? createdAt, required this.index, Duration? duration, + this.dismissed = false, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? DateTime.now(), duration = duration ?? kNotificationBaseDuration + Duration(seconds: index); + NotificationItem copyWith({ + String? id, + SnNotification? notification, + DateTime? createdAt, + int? index, + Duration? duration, + bool? dismissed, + }) { + return NotificationItem( + id: id ?? this.id, + notification: notification ?? this.notification, + createdAt: createdAt ?? this.createdAt, + index: index ?? this.index, + duration: duration ?? this.duration, + dismissed: dismissed ?? this.dismissed, + ); + } + @override bool operator ==(Object other) { if (identical(this, other)) return true; @@ -37,6 +59,8 @@ class NotificationItem { @riverpod class NotificationState extends _$NotificationState { + final Map _timers = {}; + @override List build() { return []; @@ -49,6 +73,16 @@ class NotificationState extends _$NotificationState { duration: duration, ); state = [...state, newItem]; + _timers[newItem.id] = Timer(newItem.duration, () => dismiss(newItem.id)); + } + + void dismiss(String id) { + _timers[id]?.cancel(); + _timers.remove(id); + final index = state.indexWhere((item) => item.id == id); + if (index != -1) { + state = List.from(state)..[index] = state[index].copyWith(dismissed: true); + } } void remove(String id) { @@ -56,6 +90,10 @@ class NotificationState extends _$NotificationState { } void clear() { + for (final timer in _timers.values) { + timer.cancel(); + } + _timers.clear(); state = []; } -} +} \ No newline at end of file diff --git a/lib/widgets/notification_item.dart b/lib/widgets/notification_item.dart index f577dd16..6881e382 100644 --- a/lib/widgets/notification_item.dart +++ b/lib/widgets/notification_item.dart @@ -44,80 +44,103 @@ class NotificationItemWidget extends HookConsumerWidget { } } }, - onHorizontalDragEnd: isDesktop + onHorizontalDragEnd: (details) { + if (details.primaryVelocity! > 100) { + onDismiss(); + } + }, + onVerticalDragEnd: !isDesktop ? (details) { - if (details.primaryVelocity! > 100) { + if (details.primaryVelocity! < -100) { onDismiss(); } } : null, - child: Card( - elevation: (index == 0 && !isDesktop && totalNotifications > 1) ? 8 : 4, - margin: EdgeInsets.zero, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (item.notification.meta['pfp'] != null) - ProfilePictureWidget( - fileId: item.notification.meta['pfp'], - radius: 12, - ).padding(right: 12, top: 2) - else - Icon( - Symbols.info, - color: Theme.of(context).colorScheme.primary, - size: 24, - ).padding(right: 12), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - item.notification.title, - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.bold), + child: Stack( + children: [ + Card( + elevation: (index == 0 && !isDesktop && totalNotifications > 1) ? 8 : 4, + margin: EdgeInsets.zero, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (item.notification.meta['pfp'] != null) + ProfilePictureWidget( + fileId: item.notification.meta['pfp'], + radius: 12, + ).padding(right: 12, top: 2) + else + Icon( + Symbols.info, + color: Theme.of(context).colorScheme.primary, + size: 24, + ).padding(right: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item.notification.title, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + if (item.notification.content.isNotEmpty) + Text( + item.notification.content, + style: Theme.of(context).textTheme.bodyMedium, + ), + if (item.notification.subtitle.isNotEmpty) + Text( + item.notification.subtitle, + style: Theme.of(context).textTheme.bodySmall, + ), + ], ), - if (item.notification.content.isNotEmpty) - Text( - item.notification.content, - style: Theme.of(context).textTheme.bodyMedium, - ), - if (item.notification.subtitle.isNotEmpty) - Text( - item.notification.subtitle, - style: Theme.of(context).textTheme.bodySmall, - ), - ], + ), + ], + ), + ), + AnimatedBuilder( + animation: progress, + builder: (context, child) => LinearProgressIndicator( + value: progress.value, + minHeight: 2, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary.withOpacity(0.5), ), ), - ], - ), - ), - AnimatedBuilder( - animation: progress, - builder: (context, child) => LinearProgressIndicator( - value: progress.value, - minHeight: 2, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary.withOpacity(0.5), ), + ], + ).clipRRect(all: 8), + ), + Positioned( + top: 4, + right: 4, + child: IconButton( + icon: Icon( + Symbols.close, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), + onPressed: onDismiss, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), ), - ], - ).clipRRect(all: 8), + ), + ], ), ); } diff --git a/lib/widgets/notification_overlay.dart b/lib/widgets/notification_overlay.dart index bd3ce20a..a623c546 100644 --- a/lib/widgets/notification_overlay.dart +++ b/lib/widgets/notification_overlay.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -52,7 +51,7 @@ class NotificationOverlay extends HookConsumerWidget { index: index, totalNotifications: notifications.length, onDismiss: () { - ref.read(notificationStateProvider.notifier).remove(item.id); + ref.read(notificationStateProvider.notifier).dismiss(item.id); }, ); }).toList(), @@ -92,7 +91,7 @@ class NotificationOverlay extends HookConsumerWidget { onDismiss: () { ref .read(notificationStateProvider.notifier) - .remove(item.id); + .dismiss(item.id); }, ).clipRRect(all: 8), ); @@ -128,7 +127,6 @@ class AnimatedNotificationItem extends HookConsumerWidget { reverseDuration: const Duration(milliseconds: 250), ); final progressController = useAnimationController(duration: item.duration); - final isDismissed = useState(false); final curvedAnimation = CurvedAnimation( parent: animationController, @@ -152,16 +150,13 @@ class AnimatedNotificationItem extends HookConsumerWidget { }, []); useEffect(() { - if (isDismissed.value) return null; - final timer = Timer(item.duration, () async { - if (!isDismissed.value) { - isDismissed.value = true; - await animationController.reverse(); - onDismiss(); - } - }); - return () => timer.cancel(); - }, [item.duration, isDismissed.value]); + if (item.dismissed) { + animationController.reverse().then((_) { + ref.read(notificationStateProvider.notifier).remove(item.id); + }); + } + return null; + }, [item.dismissed]); return SlideTransition( position: slideTween.animate(curvedAnimation), @@ -173,10 +168,10 @@ class AnimatedNotificationItem extends HookConsumerWidget { isDesktop: isDesktop, index: index, totalNotifications: totalNotifications, - onDismiss: () {}, + onDismiss: onDismiss, progress: progressAnimation, ), ), ); } -} +} \ No newline at end of file