💄 Optimize message flashing

This commit is contained in:
2025-09-23 20:27:18 +08:00
parent e68c5f4f92
commit f760b85186
2 changed files with 201 additions and 185 deletions

View File

@@ -25,7 +25,7 @@ class MessageContent extends StatelessWidget {
children: [
Icon(
Symbols.delete,
size: 14,
size: 16,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
@@ -34,6 +34,7 @@ class MessageContent extends StatelessWidget {
Text(
item.content ?? 'Deleted a message',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 13,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
@@ -59,7 +60,7 @@ class MessageContent extends StatelessWidget {
children: [
Icon(
Symbols.edit,
size: 14,
size: 16,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
@@ -71,7 +72,7 @@ class MessageContent extends StatelessWidget {
newText: item.content ?? 'Edited a message',
defaultTextStyle: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
addedTextStyle: TextStyle(

View File

@@ -54,6 +54,8 @@ class MessageItem extends HookConsumerWidget {
required this.onJump,
});
static const kFlashDuration = 300;
@override
Widget build(BuildContext context, WidgetRef ref) {
final remoteMessage = message.toRemoteMessage();
@@ -119,8 +121,62 @@ class MessageItem extends HookConsumerWidget {
);
}
final flashing = ref.watch(
flashingMessagesProvider.select((set) => set.contains(message.id)),
);
final isFlashing = useState(false);
final flashTimer = useState<Timer?>(null);
useEffect(() {
if (flashing) {
if (flashTimer.value != null) return null;
isFlashing.value = true;
flashTimer.value = Timer.periodic(
const Duration(milliseconds: kFlashDuration),
(timer) {
isFlashing.value = !isFlashing.value;
if (timer.tick >= 6) {
// 6 ticks: 1, 0, 1, 0, 1, 0
timer.cancel();
flashTimer.value = null;
isFlashing.value = false;
ref
.read(flashingMessagesProvider.notifier)
.update((set) => set.difference({message.id}));
}
},
);
} else {
flashTimer.value?.cancel();
flashTimer.value = null;
isFlashing.value = false;
}
return () {
flashTimer.value?.cancel();
};
}, [flashing]);
final flashColor =
isFlashing.value
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8)
: Colors.transparent;
return InkWell(
onLongPress: showActionMenu,
onSecondaryTap: showActionMenu,
onTap: () {
// Jump to related message
if (['messages.update', 'messages.delete'].contains(message.type) &&
message.meta['message_id'] is String &&
message.meta['message_id'] != null) {
onJump(message.meta['message_id']);
}
},
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: kFlashDuration),
decoration: BoxDecoration(color: flashColor),
child: switch (settings.messageDisplayStyle) {
'irc' => MessageItemDisplayIRC(
message: message,
@@ -150,6 +206,7 @@ class MessageItem extends HookConsumerWidget {
translating: translating.value,
),
},
),
);
}
}
@@ -286,54 +343,10 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
isCurrentUser
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant;
final containerColor =
isCurrentUser
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
: Theme.of(context).colorScheme.surfaceContainer;
final hasBackground =
ref.watch(backgroundImageFileProvider).valueOrNull != null;
final flashing = ref.watch(
flashingMessagesProvider.select((set) => set.contains(message.id)),
);
final isFlashing = useState(false);
final flashTimer = useState<Timer?>(null);
useEffect(() {
if (flashing) {
if (flashTimer.value != null) return null;
isFlashing.value = true;
flashTimer.value = Timer.periodic(const Duration(milliseconds: 200), (
timer,
) {
isFlashing.value = !isFlashing.value;
if (timer.tick >= 4) {
// 4 ticks: true, false, true, false
timer.cancel();
flashTimer.value = null;
isFlashing.value = false;
ref
.read(flashingMessagesProvider.notifier)
.update((set) => set.difference({message.id}));
}
});
} else {
flashTimer.value?.cancel();
flashTimer.value = null;
isFlashing.value = false;
}
return () {
flashTimer.value?.cancel();
};
}, [flashing]);
final flashColor =
isFlashing.value
? Theme.of(context).colorScheme.primary.withOpacity(0.8)
: containerColor;
final remoteMessage = message.toRemoteMessage();
final sender = remoteMessage.sender;
@@ -364,16 +377,6 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: flashColor,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -468,7 +471,6 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
],
),
),
),
MessageIndicators(
editedAt: remoteMessage.editedAt,
status: message.status,
@@ -510,25 +512,38 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
final sender = remoteMessage.sender;
final textColor = Theme.of(context).colorScheme.onSurfaceVariant;
final isMultiline =
message.type == 'text' ||
message.repliedMessageId != null ||
message.forwardedMessageId != null;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
isMultiline ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
Text(
DateFormat('HH:mm').format(message.createdAt),
style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12),
).padding(top: 2),
).padding(top: isMultiline ? 2 : 0),
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ProfilePictureWidget(
file: sender.account.profile.picture,
radius: 8,
).padding(horizontal: 6, top: 2),
),
).padding(horizontal: 6, top: isMultiline ? 2 : 0),
Text(
sender.account.nick,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
const Gap(8),
Expanded(