💄 Optimize the notification overlays
This commit is contained in:
@@ -223,7 +223,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
|||||||
// Add new typing status
|
// Add new typing status
|
||||||
_typingStatuses.add(sender);
|
_typingStatuses.add(sender);
|
||||||
}
|
}
|
||||||
state = List.of(_typingStatuses);
|
if (ref.mounted) state = List.of(_typingStatuses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,11 +243,14 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
|||||||
// Play sound for new messages when app is unfocused
|
// Play sound for new messages when app is unfocused
|
||||||
if (pkt.type == 'messages.new' &&
|
if (pkt.type == 'messages.new' &&
|
||||||
message.senderId != _chatIdentity.id &&
|
message.senderId != _chatIdentity.id &&
|
||||||
ref.read(appLifecycleStateProvider).value != AppLifecycleState.resumed &&
|
ref.read(appLifecycleStateProvider).value !=
|
||||||
|
AppLifecycleState.resumed &&
|
||||||
ref.read(appSettingsProvider).soundEffects) {
|
ref.read(appSettingsProvider).soundEffects) {
|
||||||
final player = AudioPlayer();
|
final player = AudioPlayer();
|
||||||
await player.setVolume(0.75);
|
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();
|
await player.play();
|
||||||
player.dispose();
|
player.dispose();
|
||||||
}
|
}
|
||||||
@@ -288,4 +291,4 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
|||||||
_typingCooldownTimer = null;
|
_typingCooldownTimer = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -13,6 +15,7 @@ class NotificationItem {
|
|||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final int index;
|
final int index;
|
||||||
final Duration duration;
|
final Duration duration;
|
||||||
|
final bool dismissed;
|
||||||
|
|
||||||
NotificationItem({
|
NotificationItem({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -20,11 +23,30 @@ class NotificationItem {
|
|||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
required this.index,
|
required this.index,
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
|
this.dismissed = false,
|
||||||
}) : id = id ?? const Uuid().v4(),
|
}) : id = id ?? const Uuid().v4(),
|
||||||
createdAt = createdAt ?? DateTime.now(),
|
createdAt = createdAt ?? DateTime.now(),
|
||||||
duration =
|
duration =
|
||||||
duration ?? kNotificationBaseDuration + Duration(seconds: index);
|
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
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
@@ -37,6 +59,8 @@ class NotificationItem {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class NotificationState extends _$NotificationState {
|
class NotificationState extends _$NotificationState {
|
||||||
|
final Map<String, Timer> _timers = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<NotificationItem> build() {
|
List<NotificationItem> build() {
|
||||||
return [];
|
return [];
|
||||||
@@ -49,6 +73,16 @@ class NotificationState extends _$NotificationState {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
);
|
);
|
||||||
state = [...state, newItem];
|
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) {
|
void remove(String id) {
|
||||||
@@ -56,6 +90,10 @@ class NotificationState extends _$NotificationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
for (final timer in _timers.values) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
_timers.clear();
|
||||||
state = [];
|
state = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,80 +44,103 @@ class NotificationItemWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHorizontalDragEnd: isDesktop
|
onHorizontalDragEnd: (details) {
|
||||||
|
if (details.primaryVelocity! > 100) {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onVerticalDragEnd: !isDesktop
|
||||||
? (details) {
|
? (details) {
|
||||||
if (details.primaryVelocity! > 100) {
|
if (details.primaryVelocity! < -100) {
|
||||||
onDismiss();
|
onDismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Card(
|
child: Stack(
|
||||||
elevation: (index == 0 && !isDesktop && totalNotifications > 1) ? 8 : 4,
|
children: [
|
||||||
margin: EdgeInsets.zero,
|
Card(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
elevation: (index == 0 && !isDesktop && totalNotifications > 1) ? 8 : 4,
|
||||||
shape: const RoundedRectangleBorder(
|
margin: EdgeInsets.zero,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
),
|
shape: const RoundedRectangleBorder(
|
||||||
child: Column(
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Padding(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
padding: const EdgeInsets.all(12),
|
children: [
|
||||||
child: Row(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(12),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (item.notification.meta['pfp'] != null)
|
mainAxisSize: MainAxisSize.min,
|
||||||
ProfilePictureWidget(
|
children: [
|
||||||
fileId: item.notification.meta['pfp'],
|
if (item.notification.meta['pfp'] != null)
|
||||||
radius: 12,
|
ProfilePictureWidget(
|
||||||
).padding(right: 12, top: 2)
|
fileId: item.notification.meta['pfp'],
|
||||||
else
|
radius: 12,
|
||||||
Icon(
|
).padding(right: 12, top: 2)
|
||||||
Symbols.info,
|
else
|
||||||
color: Theme.of(context).colorScheme.primary,
|
Icon(
|
||||||
size: 24,
|
Symbols.info,
|
||||||
).padding(right: 12),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Flexible(
|
size: 24,
|
||||||
child: Column(
|
).padding(right: 12),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Flexible(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
item.notification.title,
|
children: [
|
||||||
style: Theme.of(context).textTheme.titleMedium
|
Text(
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
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,
|
),
|
||||||
),
|
AnimatedBuilder(
|
||||||
if (item.notification.subtitle.isNotEmpty)
|
animation: progress,
|
||||||
Text(
|
builder: (context, child) => LinearProgressIndicator(
|
||||||
item.notification.subtitle,
|
value: progress.value,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
minHeight: 2,
|
||||||
),
|
backgroundColor: Colors.transparent,
|
||||||
],
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
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<Color>(
|
|
||||||
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),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -52,7 +51,7 @@ class NotificationOverlay extends HookConsumerWidget {
|
|||||||
index: index,
|
index: index,
|
||||||
totalNotifications: notifications.length,
|
totalNotifications: notifications.length,
|
||||||
onDismiss: () {
|
onDismiss: () {
|
||||||
ref.read(notificationStateProvider.notifier).remove(item.id);
|
ref.read(notificationStateProvider.notifier).dismiss(item.id);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -92,7 +91,7 @@ class NotificationOverlay extends HookConsumerWidget {
|
|||||||
onDismiss: () {
|
onDismiss: () {
|
||||||
ref
|
ref
|
||||||
.read(notificationStateProvider.notifier)
|
.read(notificationStateProvider.notifier)
|
||||||
.remove(item.id);
|
.dismiss(item.id);
|
||||||
},
|
},
|
||||||
).clipRRect(all: 8),
|
).clipRRect(all: 8),
|
||||||
);
|
);
|
||||||
@@ -128,7 +127,6 @@ class AnimatedNotificationItem extends HookConsumerWidget {
|
|||||||
reverseDuration: const Duration(milliseconds: 250),
|
reverseDuration: const Duration(milliseconds: 250),
|
||||||
);
|
);
|
||||||
final progressController = useAnimationController(duration: item.duration);
|
final progressController = useAnimationController(duration: item.duration);
|
||||||
final isDismissed = useState(false);
|
|
||||||
|
|
||||||
final curvedAnimation = CurvedAnimation(
|
final curvedAnimation = CurvedAnimation(
|
||||||
parent: animationController,
|
parent: animationController,
|
||||||
@@ -152,16 +150,13 @@ class AnimatedNotificationItem extends HookConsumerWidget {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (isDismissed.value) return null;
|
if (item.dismissed) {
|
||||||
final timer = Timer(item.duration, () async {
|
animationController.reverse().then((_) {
|
||||||
if (!isDismissed.value) {
|
ref.read(notificationStateProvider.notifier).remove(item.id);
|
||||||
isDismissed.value = true;
|
});
|
||||||
await animationController.reverse();
|
}
|
||||||
onDismiss();
|
return null;
|
||||||
}
|
}, [item.dismissed]);
|
||||||
});
|
|
||||||
return () => timer.cancel();
|
|
||||||
}, [item.duration, isDismissed.value]);
|
|
||||||
|
|
||||||
return SlideTransition(
|
return SlideTransition(
|
||||||
position: slideTween.animate(curvedAnimation),
|
position: slideTween.animate(curvedAnimation),
|
||||||
@@ -173,10 +168,10 @@ class AnimatedNotificationItem extends HookConsumerWidget {
|
|||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
index: index,
|
index: index,
|
||||||
totalNotifications: totalNotifications,
|
totalNotifications: totalNotifications,
|
||||||
onDismiss: () {},
|
onDismiss: onDismiss,
|
||||||
progress: progressAnimation,
|
progress: progressAnimation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user