💄 Optimize chat room, chat input

💫 More animations in chat input
This commit is contained in:
2025-10-13 00:05:22 +08:00
parent 8a2b321701
commit 51b4754182
3 changed files with 279 additions and 160 deletions

View File

@@ -639,6 +639,10 @@
"chatNotJoined": "You have not joined this chat yet.", "chatNotJoined": "You have not joined this chat yet.",
"chatUnableJoin": "You can't join this chat due to it's access control settings.", "chatUnableJoin": "You can't join this chat due to it's access control settings.",
"chatJoin": "Join the Chat", "chatJoin": "Join the Chat",
"chatReplyingTo": "Replying to {}",
"chatForwarding": "Forwarding message",
"chatEditing": "Editing message",
"chatNoContent": "No content",
"realmJoin": "Join the Realm", "realmJoin": "Join the Realm",
"realmJoinSuccess": "Successfully joined the realm.", "realmJoinSuccess": "Successfully joined the realm.",
"search": "Search", "search": "Search",

View File

@@ -535,7 +535,7 @@ class ChatRoomScreen extends HookConsumerWidget {
listController: listController, listController: listController,
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 16, top: 16,
bottom: 80 + MediaQuery.of(context).padding.bottom, bottom: MediaQuery.of(context).padding.bottom + 16,
), ),
controller: scrollController, controller: scrollController,
reverse: true, // Show newest messages at the bottom reverse: true, // Show newest messages at the bottom
@@ -687,15 +687,19 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
body: Stack( body: Stack(
children: [ children: [
// Messages // Messages and Input in Column
Positioned.fill( Positioned.fill(
child: Column(
children: [
Expanded(
child: messages.when( child: messages.when(
data: data:
(messageList) => (messageList) =>
messageList.isEmpty messageList.isEmpty
? Center(child: Text('No messages yet'.tr())) ? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList), : chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()), loading:
() => const Center(child: CircularProgressIndicator()),
error: error:
(error, _) => ResponseErrorWidget( (error, _) => ResponseErrorWidget(
error: error, error: error,
@@ -703,12 +707,7 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
), ),
), ),
// Input chatRoom.when(
Positioned(
bottom: 0,
left: 0,
right: 0,
child: chatRoom.when(
data: data:
(room) => Column( (room) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -773,6 +772,8 @@ class ChatRoomScreen extends HookConsumerWidget {
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
), ),
],
),
), ),
Positioned( Positioned(
left: 0, left: 0,

View File

@@ -225,8 +225,30 @@ class ChatInput extends HookConsumerWidget {
key: ValueKey('typing-indicator-none'), key: ValueKey('typing-indicator-none'),
), ),
), ),
if (attachments.isNotEmpty) AnimatedSwitcher(
SizedBox( duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1.0,
child: child,
),
),
);
},
child:
attachments.isNotEmpty
? SizedBox(
key: ValueKey('attachments-${attachments.length}'),
height: 180, height: 180,
child: ListView.separated( child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12), padding: EdgeInsets.symmetric(horizontal: 12),
@@ -238,39 +260,86 @@ class ChatInput extends HookConsumerWidget {
child: AttachmentPreview( child: AttachmentPreview(
isCompact: true, isCompact: true,
item: attachments[idx], item: attachments[idx],
progress: attachmentProgress['chat-upload']?[idx], progress:
onRequestUpload: () => onUploadAttachment(idx), attachmentProgress['chat-upload']?[idx],
onRequestUpload:
() => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx), onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) { onUpdate: (value) {
attachments[idx] = value; attachments[idx] = value;
onAttachmentsChanged(attachments); onAttachmentsChanged(attachments);
}, },
onMove: (delta) => onMoveAttachment(idx, delta), onMove:
(delta) => onMoveAttachment(idx, delta),
), ),
); );
}, },
separatorBuilder: (_, _) => const Gap(8), separatorBuilder: (_, _) => const Gap(8),
), ),
).padding(vertical: 12), ).padding(vertical: 12)
if (messageReplyingTo != null || : const SizedBox.shrink(
key: ValueKey('no-attachments'),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.2),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1.0,
child: child,
),
),
);
},
child:
(messageReplyingTo != null ||
messageForwardingTo != null || messageForwardingTo != null ||
messageEditingTo != null) messageEditingTo != null)
Container( ? Container(
key: ValueKey(
messageReplyingTo?.id ??
messageForwardingTo?.id ??
messageEditingTo?.id ??
'action',
),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 4, vertical: 8,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color:
borderRadius: BorderRadius.circular(32), Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
), ),
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
left: 8, left: 8,
right: 8, right: 8,
top: 8, top: 8,
bottom: 4, bottom: 8,
), ),
child: Row( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Icon( Icon(
messageReplyingTo != null messageReplyingTo != null
@@ -278,32 +347,77 @@ class ChatInput extends HookConsumerWidget {
: messageForwardingTo != null : messageForwardingTo != null
? Symbols.forward ? Symbols.forward
: Symbols.edit, : Symbols.edit,
size: 20, size: 18,
color: Theme.of(context).colorScheme.primary, color:
Theme.of(context).colorScheme.primary,
), ),
const Gap(8), const Gap(8),
Expanded( Expanded(
child: Text( child: Text(
messageReplyingTo != null messageReplyingTo != null
? 'Replying to ${messageReplyingTo?.sender.account.nick}' ? 'chatReplyingTo'.tr(
args: [
messageReplyingTo
?.sender
.account
.nick ??
'unknown'.tr(),
],
)
: messageForwardingTo != null : messageForwardingTo != null
? 'Forwarding message' ? 'chatForwarding'.tr()
: 'Editing message', : 'chatEditing'.tr(),
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
SizedBox( SizedBox(
width: 28, width: 24,
height: 28, height: 24,
child: InkWell( child: IconButton(
onTap: onClear, padding: EdgeInsets.zero,
child: const Icon(Icons.close, size: 20).center(), icon: const Icon(Icons.close, size: 18),
onPressed: onClear,
tooltip: 'clear'.tr(),
), ),
), ),
], ],
), ),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Padding(
padding: const EdgeInsets.only(
top: 6,
left: 26,
),
child: Text(
(messageReplyingTo ??
messageForwardingTo ??
messageEditingTo)
?.content ??
'chatNoContent'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-action')),
), ),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,