💄 Optimize chat input a step further
This commit is contained in:
@@ -541,7 +541,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||||
SuperListView.builder(
|
SuperListView.builder(
|
||||||
listController: listController,
|
listController: listController,
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: EdgeInsets.only(
|
||||||
|
top: 16,
|
||||||
|
bottom: 96 + MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
reverse: true, // Show newest messages at the bottom
|
reverse: true, // Show newest messages at the bottom
|
||||||
itemCount: messageList.length,
|
itemCount: messageList.length,
|
||||||
@@ -735,157 +738,160 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
// Messages
|
||||||
children: [
|
Positioned.fill(
|
||||||
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:
|
error:
|
||||||
() => const Center(child: CircularProgressIndicator()),
|
(error, _) => ResponseErrorWidget(
|
||||||
error:
|
error: error,
|
||||||
(error, _) => ResponseErrorWidget(
|
onRetry: () => messagesNotifier.loadInitial(),
|
||||||
error: error,
|
),
|
||||||
onRetry: () => messagesNotifier.loadInitial(),
|
),
|
||||||
),
|
),
|
||||||
),
|
// Input
|
||||||
),
|
Positioned(
|
||||||
chatRoom.when(
|
bottom: 0,
|
||||||
data:
|
left: 0,
|
||||||
(room) => Column(
|
right: 0,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: chatRoom.when(
|
||||||
children: [
|
data:
|
||||||
AnimatedSwitcher(
|
(room) => Column(
|
||||||
duration: const Duration(milliseconds: 150),
|
mainAxisSize: MainAxisSize.min,
|
||||||
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
children: [
|
||||||
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
AnimatedSwitcher(
|
||||||
transitionBuilder: (
|
duration: const Duration(milliseconds: 150),
|
||||||
Widget child,
|
switchInCurve: Curves.fastEaseInToSlowEaseOut,
|
||||||
Animation<double> animation,
|
switchOutCurve: Curves.fastEaseInToSlowEaseOut,
|
||||||
) {
|
transitionBuilder: (
|
||||||
return SlideTransition(
|
Widget child,
|
||||||
position: Tween<Offset>(
|
Animation<double> animation,
|
||||||
begin: const Offset(0, -0.3),
|
) {
|
||||||
end: Offset.zero,
|
return SlideTransition(
|
||||||
).animate(
|
position: Tween<Offset>(
|
||||||
CurvedAnimation(
|
begin: const Offset(0, -0.3),
|
||||||
parent: animation,
|
end: Offset.zero,
|
||||||
curve: Curves.easeOutCubic,
|
).animate(
|
||||||
),
|
CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
),
|
),
|
||||||
child: SizeTransition(
|
),
|
||||||
sizeFactor: animation,
|
child: SizeTransition(
|
||||||
axisAlignment: -1.0,
|
sizeFactor: animation,
|
||||||
child: FadeTransition(
|
axisAlignment: -1.0,
|
||||||
opacity: animation,
|
child: FadeTransition(
|
||||||
child: child,
|
opacity: animation,
|
||||||
),
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
child:
|
},
|
||||||
typingStatuses.value.isNotEmpty
|
child:
|
||||||
? Container(
|
typingStatuses.value.isNotEmpty
|
||||||
key: const ValueKey('typing-indicator'),
|
? Container(
|
||||||
width: double.infinity,
|
key: const ValueKey('typing-indicator'),
|
||||||
padding: const EdgeInsets.symmetric(
|
width: double.infinity,
|
||||||
horizontal: 16,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 4,
|
horizontal: 16,
|
||||||
),
|
vertical: 4,
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Symbols.more_horiz,
|
|
||||||
size: 16,
|
|
||||||
).padding(horizontal: 8),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'typingHint'.plural(
|
|
||||||
typingStatuses.value.length,
|
|
||||||
args: [
|
|
||||||
typingStatuses.value
|
|
||||||
.map(
|
|
||||||
(x) =>
|
|
||||||
x.nick ??
|
|
||||||
x.account.nick,
|
|
||||||
)
|
|
||||||
.join(', '),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
style:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(
|
|
||||||
key: ValueKey('typing-indicator-none'),
|
|
||||||
),
|
),
|
||||||
),
|
child: Row(
|
||||||
ChatInput(
|
children: [
|
||||||
messageController: messageController,
|
const Icon(
|
||||||
chatRoom: room!,
|
Symbols.more_horiz,
|
||||||
onSend: sendMessage,
|
size: 16,
|
||||||
onClear: () {
|
).padding(horizontal: 8),
|
||||||
if (messageEditingTo.value != null) {
|
const Gap(8),
|
||||||
attachments.value.clear();
|
Expanded(
|
||||||
messageController.clear();
|
child: Text(
|
||||||
}
|
'typingHint'.plural(
|
||||||
messageEditingTo.value = null;
|
typingStatuses.value.length,
|
||||||
messageReplyingTo.value = null;
|
args: [
|
||||||
messageForwardingTo.value = null;
|
typingStatuses.value
|
||||||
},
|
.map(
|
||||||
messageEditingTo: messageEditingTo.value,
|
(x) =>
|
||||||
messageReplyingTo: messageReplyingTo.value,
|
x.nick ??
|
||||||
messageForwardingTo: messageForwardingTo.value,
|
x.account.nick,
|
||||||
onPickFile: (bool isPhoto) {
|
)
|
||||||
if (isPhoto) {
|
.join(', '),
|
||||||
pickPhotoMedia();
|
],
|
||||||
} else {
|
),
|
||||||
pickVideoMedia();
|
style:
|
||||||
}
|
Theme.of(
|
||||||
},
|
context,
|
||||||
attachments: attachments.value,
|
).textTheme.bodySmall,
|
||||||
onUploadAttachment: uploadAttachment,
|
),
|
||||||
onDeleteAttachment: (index) async {
|
),
|
||||||
final attachment = attachments.value[index];
|
],
|
||||||
if (attachment.isOnCloud) {
|
),
|
||||||
final client = ref.watch(apiClientProvider);
|
)
|
||||||
await client.delete(
|
: const SizedBox.shrink(
|
||||||
'/drive/files/${attachment.data.id}',
|
key: ValueKey('typing-indicator-none'),
|
||||||
);
|
),
|
||||||
}
|
),
|
||||||
final clone = List.of(attachments.value);
|
ChatInput(
|
||||||
clone.removeAt(index);
|
messageController: messageController,
|
||||||
attachments.value = clone;
|
chatRoom: room!,
|
||||||
},
|
onSend: sendMessage,
|
||||||
onMoveAttachment: (idx, delta) {
|
onClear: () {
|
||||||
if (idx + delta < 0 ||
|
if (messageEditingTo.value != null) {
|
||||||
idx + delta >= attachments.value.length) {
|
attachments.value.clear();
|
||||||
return;
|
messageController.clear();
|
||||||
}
|
}
|
||||||
final clone = List.of(attachments.value);
|
messageEditingTo.value = null;
|
||||||
clone.insert(idx + delta, clone.removeAt(idx));
|
messageReplyingTo.value = null;
|
||||||
attachments.value = clone;
|
messageForwardingTo.value = null;
|
||||||
},
|
},
|
||||||
onAttachmentsChanged: (newAttachments) {
|
messageEditingTo: messageEditingTo.value,
|
||||||
attachments.value = newAttachments;
|
messageReplyingTo: messageReplyingTo.value,
|
||||||
},
|
messageForwardingTo: messageForwardingTo.value,
|
||||||
attachmentProgress: attachmentProgress.value,
|
onPickFile: (bool isPhoto) {
|
||||||
),
|
if (isPhoto) {
|
||||||
],
|
pickPhotoMedia();
|
||||||
),
|
} else {
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
pickVideoMedia();
|
||||||
loading: () => const SizedBox.shrink(),
|
}
|
||||||
),
|
},
|
||||||
],
|
attachments: attachments.value,
|
||||||
|
onUploadAttachment: uploadAttachment,
|
||||||
|
onDeleteAttachment: (index) async {
|
||||||
|
final attachment = attachments.value[index];
|
||||||
|
if (attachment.isOnCloud) {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
await client.delete(
|
||||||
|
'/drive/files/${attachment.data.id}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final clone = List.of(attachments.value);
|
||||||
|
clone.removeAt(index);
|
||||||
|
attachments.value = clone;
|
||||||
|
},
|
||||||
|
onMoveAttachment: (idx, delta) {
|
||||||
|
if (idx + delta < 0 ||
|
||||||
|
idx + delta >= attachments.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final clone = List.of(attachments.value);
|
||||||
|
clone.insert(idx + delta, clone.removeAt(idx));
|
||||||
|
attachments.value = clone;
|
||||||
|
},
|
||||||
|
onAttachmentsChanged: (newAttachments) {
|
||||||
|
attachments.value = newAttachments;
|
||||||
|
},
|
||||||
|
attachmentProgress: attachmentProgress.value,
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@@ -115,200 +115,212 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
};
|
};
|
||||||
|
|
||||||
return Material(
|
return Container(
|
||||||
elevation: 8,
|
margin: const EdgeInsets.all(16),
|
||||||
color: Theme.of(context).colorScheme.surface,
|
child: Material(
|
||||||
child: Column(
|
elevation: 2,
|
||||||
children: [
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
if (attachments.isNotEmpty)
|
borderRadius: BorderRadius.circular(32),
|
||||||
SizedBox(
|
child: Padding(
|
||||||
height: 280,
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
child: ListView.separated(
|
child: Column(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
children: [
|
||||||
scrollDirection: Axis.horizontal,
|
if (attachments.isNotEmpty)
|
||||||
itemCount: attachments.length,
|
SizedBox(
|
||||||
itemBuilder: (context, idx) {
|
height: 280,
|
||||||
return SizedBox(
|
child: ListView.separated(
|
||||||
height: 280,
|
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||||
width: 280,
|
scrollDirection: Axis.horizontal,
|
||||||
child: AttachmentPreview(
|
itemCount: attachments.length,
|
||||||
item: attachments[idx],
|
itemBuilder: (context, idx) {
|
||||||
progress: attachmentProgress['chat-upload']?[idx],
|
return SizedBox(
|
||||||
onRequestUpload: () => onUploadAttachment(idx),
|
height: 280,
|
||||||
onDelete: () => onDeleteAttachment(idx),
|
width: 280,
|
||||||
onUpdate: (value) {
|
child: AttachmentPreview(
|
||||||
attachments[idx] = value;
|
item: attachments[idx],
|
||||||
onAttachmentsChanged(attachments);
|
progress: attachmentProgress['chat-upload']?[idx],
|
||||||
},
|
onRequestUpload: () => onUploadAttachment(idx),
|
||||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
onDelete: () => onDeleteAttachment(idx),
|
||||||
),
|
onUpdate: (value) {
|
||||||
);
|
attachments[idx] = value;
|
||||||
},
|
onAttachmentsChanged(attachments);
|
||||||
separatorBuilder: (_, _) => const Gap(8),
|
},
|
||||||
),
|
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||||
).padding(top: 12),
|
),
|
||||||
if (messageReplyingTo != null ||
|
);
|
||||||
messageForwardingTo != null ||
|
},
|
||||||
messageEditingTo != null)
|
separatorBuilder: (_, _) => const Gap(8),
|
||||||
Container(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
).padding(top: 12),
|
||||||
decoration: BoxDecoration(
|
if (messageReplyingTo != null ||
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
messageForwardingTo != null ||
|
||||||
borderRadius: BorderRadius.circular(8),
|
messageEditingTo != null)
|
||||||
),
|
Container(
|
||||||
margin: const EdgeInsets.only(left: 8, right: 8, top: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
child: Row(
|
horizontal: 16,
|
||||||
children: [
|
vertical: 4,
|
||||||
Icon(
|
),
|
||||||
messageReplyingTo != null
|
decoration: BoxDecoration(
|
||||||
? Symbols.reply
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
: messageForwardingTo != null
|
borderRadius: BorderRadius.circular(32),
|
||||||
? Symbols.forward
|
),
|
||||||
: Symbols.edit,
|
margin: const EdgeInsets.only(
|
||||||
size: 20,
|
left: 8,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
messageReplyingTo != null
|
||||||
|
? Symbols.reply
|
||||||
|
: messageForwardingTo != null
|
||||||
|
? Symbols.forward
|
||||||
|
: Symbols.edit,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
messageReplyingTo != null
|
||||||
|
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
|
||||||
|
: messageForwardingTo != null
|
||||||
|
? 'Forwarding message'
|
||||||
|
: 'Editing message',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onClear,
|
||||||
|
child: const Icon(Icons.close, size: 20).center(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'stickers'.tr(),
|
||||||
|
icon: const Icon(Symbols.add_reaction),
|
||||||
|
onPressed: () {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
showStickerPickerPopover(
|
||||||
|
context,
|
||||||
|
Offset(
|
||||||
|
20,
|
||||||
|
size.height -
|
||||||
|
480 -
|
||||||
|
MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
|
onPick: (placeholder) {
|
||||||
|
// Insert placeholder at current cursor position
|
||||||
|
final text = messageController.text;
|
||||||
|
final selection = messageController.selection;
|
||||||
|
final start =
|
||||||
|
selection.start >= 0
|
||||||
|
? selection.start
|
||||||
|
: text.length;
|
||||||
|
final end =
|
||||||
|
selection.end >= 0
|
||||||
|
? selection.end
|
||||||
|
: text.length;
|
||||||
|
final newText = text.replaceRange(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
placeholder,
|
||||||
|
);
|
||||||
|
messageController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: start + placeholder.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
icon: const Icon(Symbols.photo_library),
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () => onPickFile(true),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.photo),
|
||||||
|
Text('addPhoto').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () => onPickFile(false),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.video_call),
|
||||||
|
Text('addVideo').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: TextField(
|
||||||
messageReplyingTo != null
|
focusNode: inputFocusNode,
|
||||||
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
|
controller: messageController,
|
||||||
: messageForwardingTo != null
|
keyboardType: TextInputType.multiline,
|
||||||
? 'Forwarding message'
|
decoration: InputDecoration(
|
||||||
: 'Editing message',
|
hintText:
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
(chatRoom.type == 1 && chatRoom.name == null)
|
||||||
maxLines: 1,
|
? 'chatDirectMessageHint'.tr(
|
||||||
overflow: TextOverflow.ellipsis,
|
args: [
|
||||||
|
chatRoom.members!
|
||||||
|
.map((e) => e.account.nick)
|
||||||
|
.join(', '),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
|
||||||
|
border: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
counterText:
|
||||||
|
messageController.text.length > 1024
|
||||||
|
? '${messageController.text.length}/4096'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
minLines: 1,
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, size: 20),
|
icon: const Icon(Icons.send),
|
||||||
onPressed: onClear,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
padding: EdgeInsets.zero,
|
onPressed: send,
|
||||||
style: ButtonStyle(
|
|
||||||
minimumSize: WidgetStatePropertyAll(Size(28, 28)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
tooltip: 'stickers'.tr(),
|
|
||||||
icon: const Icon(Symbols.add_reaction),
|
|
||||||
onPressed: () {
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
showStickerPickerPopover(
|
|
||||||
context,
|
|
||||||
Offset(
|
|
||||||
20,
|
|
||||||
size.height -
|
|
||||||
480 -
|
|
||||||
MediaQuery.of(context).padding.bottom,
|
|
||||||
),
|
|
||||||
onPick: (placeholder) {
|
|
||||||
// Insert placeholder at current cursor position
|
|
||||||
final text = messageController.text;
|
|
||||||
final selection = messageController.selection;
|
|
||||||
final start =
|
|
||||||
selection.start >= 0
|
|
||||||
? selection.start
|
|
||||||
: text.length;
|
|
||||||
final end =
|
|
||||||
selection.end >= 0
|
|
||||||
? selection.end
|
|
||||||
: text.length;
|
|
||||||
final newText = text.replaceRange(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
placeholder,
|
|
||||||
);
|
|
||||||
messageController.value = TextEditingValue(
|
|
||||||
text: newText,
|
|
||||||
selection: TextSelection.collapsed(
|
|
||||||
offset: start + placeholder.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuButton(
|
|
||||||
icon: const Icon(Symbols.photo_library),
|
|
||||||
itemBuilder:
|
|
||||||
(context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
onTap: () => onPickFile(true),
|
|
||||||
child: Row(
|
|
||||||
spacing: 12,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.photo),
|
|
||||||
Text('addPhoto').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
onTap: () => onPickFile(false),
|
|
||||||
child: Row(
|
|
||||||
spacing: 12,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.video_call),
|
|
||||||
Text('addVideo').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
focusNode: inputFocusNode,
|
|
||||||
controller: messageController,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText:
|
|
||||||
(chatRoom.type == 1 && chatRoom.name == null)
|
|
||||||
? 'chatDirectMessageHint'.tr(
|
|
||||||
args: [
|
|
||||||
chatRoom.members!
|
|
||||||
.map((e) => e.account.nick)
|
|
||||||
.join(', '),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
|
|
||||||
border: InputBorder.none,
|
|
||||||
isDense: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
counterText:
|
|
||||||
messageController.text.length > 1024
|
|
||||||
? '${messageController.text.length}/4096'
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
maxLines: 3,
|
|
||||||
minLines: 1,
|
|
||||||
onTapOutside:
|
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.send),
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
onPressed: send,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -56,7 +56,7 @@ class MessageContent extends StatelessWidget {
|
|||||||
case 'messages.update.links':
|
case 'messages.update.links':
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.edit,
|
Symbols.edit,
|
||||||
@@ -64,27 +64,29 @@ class MessageContent extends StatelessWidget {
|
|||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||||
),
|
).padding(top: 2),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
if (item.meta['previous_content'] is String)
|
if (item.meta['previous_content'] is String)
|
||||||
PrettyDiffText(
|
Flexible(
|
||||||
oldText: item.meta['previous_content'],
|
child: PrettyDiffText(
|
||||||
newText: item.content ?? 'Edited a message',
|
oldText: item.meta['previous_content'],
|
||||||
defaultTextStyle: Theme.of(
|
newText: item.content ?? 'Edited a message',
|
||||||
context,
|
defaultTextStyle: Theme.of(
|
||||||
).textTheme.bodyMedium!.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
addedTextStyle: TextStyle(
|
|
||||||
backgroundColor: Theme.of(
|
|
||||||
context,
|
context,
|
||||||
).colorScheme.primaryFixedDim.withOpacity(0.4),
|
).textTheme.bodyMedium!.copyWith(
|
||||||
),
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
deletedTextStyle: TextStyle(
|
),
|
||||||
decoration: TextDecoration.lineThrough,
|
addedTextStyle: TextStyle(
|
||||||
color: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withOpacity(0.7),
|
).colorScheme.primaryFixedDim.withOpacity(0.4),
|
||||||
|
),
|
||||||
|
deletedTextStyle: TextStyle(
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant.withOpacity(0.7),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -104,10 +106,12 @@ class MessageContent extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MarkdownTextContent(
|
Flexible(
|
||||||
content: item.content ?? '*${item.type} has no content*',
|
child: MarkdownTextContent(
|
||||||
isSelectable: true,
|
content: item.content ?? '*${item.type} has no content*',
|
||||||
linesMargin: EdgeInsets.zero,
|
isSelectable: true,
|
||||||
|
linesMargin: EdgeInsets.zero,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (translatedText?.isNotEmpty ?? false)
|
if (translatedText?.isNotEmpty ?? false)
|
||||||
...([
|
...([
|
||||||
|
Reference in New Issue
Block a user