💄 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.",
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
"chatJoin": "Join the Chat",
"chatReplyingTo": "Replying to {}",
"chatForwarding": "Forwarding message",
"chatEditing": "Editing message",
"chatNoContent": "No content",
"realmJoin": "Join the Realm",
"realmJoinSuccess": "Successfully joined the realm.",
"search": "Search",

View File

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

View File

@@ -225,8 +225,30 @@ class ChatInput extends HookConsumerWidget {
key: ValueKey('typing-indicator-none'),
),
),
if (attachments.isNotEmpty)
SizedBox(
AnimatedSwitcher(
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,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
@@ -238,39 +260,86 @@ class ChatInput extends HookConsumerWidget {
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress: attachmentProgress['chat-upload']?[idx],
onRequestUpload: () => onUploadAttachment(idx),
progress:
attachmentProgress['chat-upload']?[idx],
onRequestUpload:
() => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
onMove:
(delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(vertical: 12),
if (messageReplyingTo != null ||
).padding(vertical: 12)
: 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 ||
messageEditingTo != null)
Container(
? Container(
key: ValueKey(
messageReplyingTo?.id ??
messageForwardingTo?.id ??
messageEditingTo?.id ??
'action',
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
vertical: 8,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(32),
color:
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(
left: 8,
right: 8,
top: 8,
bottom: 4,
bottom: 8,
),
child: Row(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
messageReplyingTo != null
@@ -278,32 +347,77 @@ class ChatInput extends HookConsumerWidget {
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 20,
color: Theme.of(context).colorScheme.primary,
size: 18,
color:
Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
? 'chatReplyingTo'.tr(
args: [
messageReplyingTo
?.sender
.account
.nick ??
'unknown'.tr(),
],
)
: messageForwardingTo != null
? 'Forwarding message'
: 'Editing message',
style: Theme.of(context).textTheme.bodySmall,
? 'chatForwarding'.tr()
: 'chatEditing'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 28,
height: 28,
child: InkWell(
onTap: onClear,
child: const Icon(Icons.close, size: 20).center(),
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
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(
crossAxisAlignment: CrossAxisAlignment.start,