💄 Optimize message flashing
This commit is contained in:
@@ -25,7 +25,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.delete,
|
Symbols.delete,
|
||||||
size: 14,
|
size: 16,
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||||
@@ -34,6 +34,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
item.content ?? 'Deleted a message',
|
item.content ?? 'Deleted a message',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
fontSize: 13,
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||||
@@ -59,7 +60,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.edit,
|
Symbols.edit,
|
||||||
size: 14,
|
size: 16,
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||||
@@ -71,7 +72,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
newText: item.content ?? 'Edited a message',
|
newText: item.content ?? 'Edited a message',
|
||||||
defaultTextStyle: Theme.of(
|
defaultTextStyle: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodySmall!.copyWith(
|
).textTheme.bodyMedium!.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
addedTextStyle: TextStyle(
|
addedTextStyle: TextStyle(
|
||||||
|
@@ -54,6 +54,8 @@ class MessageItem extends HookConsumerWidget {
|
|||||||
required this.onJump,
|
required this.onJump,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static const kFlashDuration = 300;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final remoteMessage = message.toRemoteMessage();
|
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(
|
return InkWell(
|
||||||
onLongPress: showActionMenu,
|
onLongPress: showActionMenu,
|
||||||
child: switch (settings.messageDisplayStyle) {
|
onSecondaryTap: showActionMenu,
|
||||||
'irc' => MessageItemDisplayIRC(
|
onTap: () {
|
||||||
message: message,
|
// Jump to related message
|
||||||
isCurrentUser: isCurrentUser,
|
if (['messages.update', 'messages.delete'].contains(message.type) &&
|
||||||
progress: progress,
|
message.meta['message_id'] is String &&
|
||||||
showAvatar: showAvatar,
|
message.meta['message_id'] != null) {
|
||||||
onJump: onJump,
|
onJump(message.meta['message_id']);
|
||||||
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,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
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
|
isCurrentUser
|
||||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||||
: Theme.of(context).colorScheme.onSurfaceVariant;
|
: Theme.of(context).colorScheme.onSurfaceVariant;
|
||||||
final containerColor =
|
|
||||||
isCurrentUser
|
|
||||||
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
|
|
||||||
: Theme.of(context).colorScheme.surfaceContainer;
|
|
||||||
|
|
||||||
final hasBackground =
|
final hasBackground =
|
||||||
ref.watch(backgroundImageFileProvider).valueOrNull != null;
|
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 remoteMessage = message.toRemoteMessage();
|
||||||
final sender = remoteMessage.sender;
|
final sender = remoteMessage.sender;
|
||||||
|
|
||||||
@@ -364,109 +377,98 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: AnimatedContainer(
|
child: Column(
|
||||||
duration: const Duration(milliseconds: 200),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
color: flashColor,
|
if (remoteMessage.repliedMessageId != null)
|
||||||
borderRadius: BorderRadius.circular(16),
|
MessageQuoteWidget(
|
||||||
),
|
message: message,
|
||||||
padding: const EdgeInsets.symmetric(
|
textColor: textColor,
|
||||||
horizontal: 12,
|
isReply: true,
|
||||||
vertical: 6,
|
).padding(vertical: 4),
|
||||||
),
|
if (remoteMessage.forwardedMessageId != null)
|
||||||
child: Column(
|
MessageQuoteWidget(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
message: message,
|
||||||
children: [
|
textColor: textColor,
|
||||||
if (remoteMessage.repliedMessageId != null)
|
isReply: false,
|
||||||
MessageQuoteWidget(
|
).padding(vertical: 4),
|
||||||
message: message,
|
if (MessageContent.hasContent(remoteMessage))
|
||||||
textColor: textColor,
|
MessageContent(
|
||||||
isReply: true,
|
item: remoteMessage,
|
||||||
).padding(vertical: 4),
|
translatedText: translatedText,
|
||||||
if (remoteMessage.forwardedMessageId != null)
|
),
|
||||||
MessageQuoteWidget(
|
if (remoteMessage.attachments.isNotEmpty)
|
||||||
message: message,
|
LayoutBuilder(
|
||||||
textColor: textColor,
|
builder: (context, constraints) {
|
||||||
isReply: false,
|
return CloudFileList(
|
||||||
).padding(vertical: 4),
|
files: remoteMessage.attachments,
|
||||||
if (MessageContent.hasContent(remoteMessage))
|
maxWidth: constraints.maxWidth,
|
||||||
MessageContent(
|
padding: EdgeInsets.symmetric(vertical: 4),
|
||||||
item: remoteMessage,
|
);
|
||||||
translatedText: translatedText,
|
},
|
||||||
),
|
),
|
||||||
if (remoteMessage.attachments.isNotEmpty)
|
if (remoteMessage.meta['embeds'] != null)
|
||||||
LayoutBuilder(
|
...((remoteMessage.meta['embeds'] as List<dynamic>)
|
||||||
builder: (context, constraints) {
|
.map((embed) => convertMapKeysToSnakeCase(embed))
|
||||||
return CloudFileList(
|
.where((embed) => embed['type'] == 'link')
|
||||||
files: remoteMessage.attachments,
|
.map((embed) => SnScrappedLink.fromJson(embed))
|
||||||
maxWidth: constraints.maxWidth,
|
.map(
|
||||||
padding: EdgeInsets.symmetric(vertical: 4),
|
(link) => LayoutBuilder(
|
||||||
);
|
builder: (context, constraints) {
|
||||||
},
|
return EmbedLinkWidget(
|
||||||
),
|
link: link,
|
||||||
if (remoteMessage.meta['embeds'] != null)
|
maxWidth: math.min(
|
||||||
...((remoteMessage.meta['embeds'] as List<dynamic>)
|
constraints.maxWidth,
|
||||||
.map((embed) => convertMapKeysToSnakeCase(embed))
|
480,
|
||||||
.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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(4),
|
margin: const EdgeInsets.symmetric(
|
||||||
LinearProgressIndicator(
|
vertical: 4,
|
||||||
value: entry.value / 100,
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surfaceVariant,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList()),
|
||||||
|
if (progress != null && progress!.isNotEmpty)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
if ((remoteMessage.content?.isNotEmpty ?? false))
|
||||||
const Gap(0),
|
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(
|
MessageIndicators(
|
||||||
@@ -510,25 +512,38 @@ class MessageItemDisplayIRC extends HookConsumerWidget {
|
|||||||
final sender = remoteMessage.sender;
|
final sender = remoteMessage.sender;
|
||||||
final textColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
final textColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||||
|
|
||||||
|
final isMultiline =
|
||||||
|
message.type == 'text' ||
|
||||||
|
message.repliedMessageId != null ||
|
||||||
|
message.forwardedMessageId != null;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
isMultiline ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
DateFormat('HH:mm').format(message.createdAt),
|
DateFormat('HH:mm').format(message.createdAt),
|
||||||
style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12),
|
style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12),
|
||||||
).padding(top: 2),
|
).padding(top: isMultiline ? 2 : 0),
|
||||||
AccountPfcGestureDetector(
|
AccountPfcGestureDetector(
|
||||||
uname: sender.account.name,
|
uname: sender.account.name,
|
||||||
child: ProfilePictureWidget(
|
child: Row(
|
||||||
file: sender.account.profile.picture,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
radius: 8,
|
children: [
|
||||||
).padding(horizontal: 6, top: 2),
|
ProfilePictureWidget(
|
||||||
),
|
file: sender.account.profile.picture,
|
||||||
Text(
|
radius: 8,
|
||||||
sender.account.nick,
|
).padding(horizontal: 6, top: isMultiline ? 2 : 0),
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
Text(
|
||||||
|
sender.account.nick,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
Reference in New Issue
Block a user