From a201f207936afb55dc9ce65fdd5750abf6a595ba Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 10 Oct 2025 21:38:53 +0800 Subject: [PATCH] :lipstick: Optimize message indicator --- lib/widgets/chat/message_indicators.dart | 63 ++- lib/widgets/chat/message_item.dart | 487 +++++++++++------------ lib/widgets/shared/upload_menu.dart | 2 +- 3 files changed, 271 insertions(+), 281 deletions(-) diff --git a/lib/widgets/chat/message_indicators.dart b/lib/widgets/chat/message_indicators.dart index 5813b410..f981f0fa 100644 --- a/lib/widgets/chat/message_indicators.dart +++ b/lib/widgets/chat/message_indicators.dart @@ -9,6 +9,7 @@ class MessageIndicators extends StatelessWidget { final MessageStatus? status; final bool isCurrentUser; final Color textColor; + final EdgeInsets padding; const MessageIndicators({ super.key, @@ -16,26 +17,43 @@ class MessageIndicators extends StatelessWidget { this.status, required this.isCurrentUser, required this.textColor, + this.padding = const EdgeInsets.only(left: 6), }); @override Widget build(BuildContext context) { - return Row( - spacing: 4, - mainAxisSize: MainAxisSize.min, - children: [ - if (editedAt != null) - Text( - 'edited'.tr().toLowerCase(), - style: TextStyle(fontSize: 11, color: textColor.withOpacity(0.7)), - ), - if (isCurrentUser && status != null) - _buildStatusIcon( - context, - status!, - textColor.withOpacity(0.7), - ).padding(bottom: 3), - ], + final children = []; + + if (editedAt != null) { + children.add( + Text( + 'edited'.tr().toLowerCase(), + style: TextStyle(fontSize: 11, color: textColor.withOpacity(0.7)), + ), + ); + } + + if (isCurrentUser && status != null && status != MessageStatus.sent) { + children.add( + _buildStatusIcon( + context, + status!, + textColor.withOpacity(0.7), + ).padding(bottom: 4), + ); + } + + if (children.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: padding, + child: Row( + spacing: 4, + mainAxisSize: MainAxisSize.min, + children: children, + ), ); } @@ -46,9 +64,18 @@ class MessageIndicators extends StatelessWidget { ) { switch (status) { case MessageStatus.pending: - return Icon(Icons.access_time, size: 12, color: textColor); + return SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator( + padding: EdgeInsets.zero, + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(textColor), + ), + ); case MessageStatus.sent: - return Icon(Icons.check, size: 12, color: textColor); + // Sent status is hidden + return const SizedBox.shrink(); case MessageStatus.failed: return Consumer( builder: diff --git a/lib/widgets/chat/message_item.dart b/lib/widgets/chat/message_item.dart index 92ac13e2..0f202e33 100644 --- a/lib/widgets/chat/message_item.dart +++ b/lib/widgets/chat/message_item.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:intl/intl.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -714,45 +715,12 @@ class MessageItemDisplayBubble extends HookConsumerWidget { renderingPadding: EdgeInsets.zero, maxWidth: 480, ), - 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( - Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - const Gap(0), - ], - ), + FileUploadProgressWidget( + progress: progress, + textColor: textColor, + hasContent: + remoteMessage.content?.isNotEmpty ?? false, + ), ], ), ), @@ -833,81 +801,64 @@ class MessageItemDisplayIRC extends HookConsumerWidget { ), const Gap(8), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, 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 && - kMessageEnableEmbedTypes.contains(message.type)) - EmbedListWidget( - embeds: remoteMessage.meta['embeds'] as List, - isInteractive: true, - isFullPost: false, - renderingPadding: EdgeInsets.zero, - maxWidth: 480, - ), - if (progress != null && progress!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if ((remoteMessage.content?.isNotEmpty ?? false)) - const SizedBox.shrink(), - 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( - Theme.of(context).colorScheme.primary, - ), - ), - ], + 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 && + kMessageEnableEmbedTypes.contains(message.type)) + EmbedListWidget( + embeds: remoteMessage.meta['embeds'] as List, + isInteractive: true, + isFullPost: false, + renderingPadding: EdgeInsets.zero, + maxWidth: 480, + ), + FileUploadProgressWidget( + progress: progress, + textColor: textColor, + hasContent: remoteMessage.content?.isNotEmpty ?? false, + ), ], ), + ), + MessageIndicators( + editedAt: remoteMessage.editedAt, + status: message.status, + isCurrentUser: isCurrentUser, + textColor: textColor, + ), ], ), ), @@ -971,175 +922,133 @@ class MessageItemDisplayDiscord extends HookConsumerWidget { ), ], ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, 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 && - kMessageEnableEmbedTypes.contains(message.type)) - EmbedListWidget( - embeds: remoteMessage.meta['embeds'] as List, - isInteractive: true, - isFullPost: false, - renderingPadding: EdgeInsets.zero, - maxWidth: 480, - ), - if (progress != null && progress!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if ((remoteMessage.content?.isNotEmpty ?? false)) - const SizedBox.shrink(), - 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( - Theme.of(context).colorScheme.primary, - ), - ), - ], + 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 && + kMessageEnableEmbedTypes.contains(message.type)) + EmbedListWidget( + embeds: + remoteMessage.meta['embeds'] + as List, + isInteractive: true, + isFullPost: false, + renderingPadding: EdgeInsets.zero, + maxWidth: 480, + ), + FileUploadProgressWidget( + progress: progress, + textColor: textColor, + hasContent: + remoteMessage.content?.isNotEmpty ?? false, + ), ], ), + ), + MessageIndicators( + editedAt: remoteMessage.editedAt, + status: message.status, + isCurrentUser: isCurrentUser, + textColor: textColor, + ), ], ).padding(left: kAvatarRadius * 2 + 8), ], ) : Padding( padding: EdgeInsets.only(left: kAvatarRadius * 2 + 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (showAvatar) - MessageSenderInfo( - sender: sender, - createdAt: message.createdAt, - textColor: textColor, - showAvatar: false, - isCompact: true, - ), - 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 && - kMessageEnableEmbedTypes.contains(message.type)) - EmbedListWidget( - embeds: remoteMessage.meta['embeds'] as List, - isInteractive: true, - isFullPost: false, - renderingPadding: EdgeInsets.zero, - maxWidth: 480, - ), - if (progress != null && progress!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, 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( - Theme.of(context).colorScheme.primary, - ), - ), - ], + 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, ), - const Gap(0), + 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 && + kMessageEnableEmbedTypes.contains(message.type)) + EmbedListWidget( + embeds: + remoteMessage.meta['embeds'] as List, + isInteractive: true, + isFullPost: false, + renderingPadding: EdgeInsets.zero, + maxWidth: 480, + ), + FileUploadProgressWidget( + progress: progress, + textColor: textColor, + hasContent: + remoteMessage.content?.isNotEmpty ?? false, + ), ], ), + ), + MessageIndicators( + editedAt: remoteMessage.editedAt, + status: message.status, + isCurrentUser: isCurrentUser, + textColor: textColor, + ), ], ), ), @@ -1249,3 +1158,57 @@ class MessageQuoteWidget extends HookConsumerWidget { ); } } + +class FileUploadProgressWidget extends StatelessWidget { + final Map? progress; + final Color textColor; + final bool hasContent; + + const FileUploadProgressWidget({ + super.key, + required this.progress, + required this.textColor, + required this.hasContent, + }); + + @override + Widget build(BuildContext context) { + if (progress == null || progress!.isEmpty) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + if (hasContent) 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, + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + trackGap: 0, + ), + ], + ), + const Gap(0), + ], + ); + } +} diff --git a/lib/widgets/shared/upload_menu.dart b/lib/widgets/shared/upload_menu.dart index 00d50ffe..7c086f87 100644 --- a/lib/widgets/shared/upload_menu.dart +++ b/lib/widgets/shared/upload_menu.dart @@ -52,7 +52,7 @@ class UploadMenu extends StatelessWidget { leadingIcon: Icon(item.icon), style: ButtonStyle( padding: WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 16, vertical: 20), + EdgeInsets.only(left: 12, right: 16, top: 20, bottom: 20), ), ), child: Text(item.textKey.tr()),