💄 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,37 +121,92 @@ 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,
child: switch (settings.messageDisplayStyle) {
'irc' => MessageItemDisplayIRC(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
'column' => MessageItemDisplayDiscord(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
_ => MessageItemDisplayBubble(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
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,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
'column' => MessageItemDisplayDiscord(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
_ => MessageItemDisplayBubble(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
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,109 +377,98 @@ 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: [
if (remoteMessage.repliedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: true,
).padding(vertical: 4),
if (remoteMessage.forwardedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: false,
).padding(vertical: 4),
if (MessageContent.hasContent(remoteMessage))
MessageContent(
item: remoteMessage,
translatedText: translatedText,
),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
padding: EdgeInsets.symmetric(vertical: 4),
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
);
},
),
)
.toList()),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
if ((remoteMessage.content?.isNotEmpty ?? false))
const Gap(0),
for (var entry in progress!.entries)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
entry.value.toStringAsFixed(1),
],
),
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (remoteMessage.repliedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: true,
).padding(vertical: 4),
if (remoteMessage.forwardedMessageId != null)
MessageQuoteWidget(
message: message,
textColor: textColor,
isReply: false,
).padding(vertical: 4),
if (MessageContent.hasContent(remoteMessage))
MessageContent(
item: remoteMessage,
translatedText: translatedText,
),
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
padding: EdgeInsets.symmetric(vertical: 4),
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(
constraints.maxWidth,
480,
),
const Gap(4),
LinearProgressIndicator(
value: entry.value / 100,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
margin: const EdgeInsets.symmetric(
vertical: 4,
),
],
),
);
},
),
)
.toList()),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
if ((remoteMessage.content?.isNotEmpty ?? false))
const Gap(0),
],
),
],
),
for (var entry in progress!.entries)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
entry.value.toStringAsFixed(1),
],
),
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.8),
),
),
const Gap(4),
LinearProgressIndicator(
value: entry.value / 100,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const Gap(0),
],
),
],
),
),
MessageIndicators(
@@ -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(
file: sender.account.profile.picture,
radius: 8,
).padding(horizontal: 6, top: 2),
),
Text(
sender.account.nick,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ProfilePictureWidget(
file: sender.account.profile.picture,
radius: 8,
).padding(horizontal: 6, top: isMultiline ? 2 : 0),
Text(
sender.account.nick,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
const Gap(8),
Expanded(