feat(chat): Improve chat input semantics and keyboard interactions

This commit is contained in:
cogwheel0
2025-12-03 15:04:53 +05:30
parent 8fa839f84e
commit 0daf840a03

View File

@@ -1293,157 +1293,163 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
} catch (_) {} } catch (_) {}
_ensureFocusedIfEnabled(); _ensureFocusedIfEnabled();
}, },
child: Semantics( child: MergeSemantics(
textField: true, child: Semantics(
label: AppLocalizations.of(context)!.messageInputLabel, label: AppLocalizations.of(context)!.messageInputLabel,
hint: AppLocalizations.of(context)!.messageInputHint, hint: AppLocalizations.of(context)!.messageInputHint,
child: Shortcuts( child: Shortcuts(
shortcuts: () { shortcuts: () {
final map = <LogicalKeySet, Intent>{ final map = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.enter): LogicalKeySet(
const _SendMessageIntent(), LogicalKeyboardKey.meta,
LogicalKeySet( LogicalKeyboardKey.enter,
LogicalKeyboardKey.control, ): const _SendMessageIntent(),
LogicalKeyboardKey.enter, LogicalKeySet(
): const _SendMessageIntent(), LogicalKeyboardKey.control,
}; LogicalKeyboardKey.enter,
if (sendOnEnter) { ): const _SendMessageIntent(),
map[LogicalKeySet(LogicalKeyboardKey.enter)] = };
const _SendMessageIntent(); if (sendOnEnter) {
map[LogicalKeySet( map[LogicalKeySet(LogicalKeyboardKey.enter)] =
LogicalKeyboardKey.shift, const _SendMessageIntent();
LogicalKeyboardKey.enter, map[LogicalKeySet(
)] = LogicalKeyboardKey.shift,
const _InsertNewlineIntent(); LogicalKeyboardKey.enter,
} )] =
if (_showPromptOverlay) { const _InsertNewlineIntent();
map[LogicalKeySet(LogicalKeyboardKey.arrowDown)] = }
const _SelectNextPromptIntent(); if (_showPromptOverlay) {
map[LogicalKeySet(LogicalKeyboardKey.arrowUp)] = map[LogicalKeySet(LogicalKeyboardKey.arrowDown)] =
const _SelectPreviousPromptIntent(); const _SelectNextPromptIntent();
map[LogicalKeySet(LogicalKeyboardKey.escape)] = map[LogicalKeySet(LogicalKeyboardKey.arrowUp)] =
const _DismissPromptIntent(); const _SelectPreviousPromptIntent();
} map[LogicalKeySet(LogicalKeyboardKey.escape)] =
return map; const _DismissPromptIntent();
}(), }
child: Actions( return map;
actions: <Type, Action<Intent>>{ }(),
_SendMessageIntent: CallbackAction<_SendMessageIntent>( child: Actions(
onInvoke: (intent) { actions: <Type, Action<Intent>>{
if (_showPromptOverlay) { _SendMessageIntent: CallbackAction<_SendMessageIntent>(
_confirmPromptSelection(); onInvoke: (intent) {
return null; if (_showPromptOverlay) {
} _confirmPromptSelection();
_sendMessage();
return null;
},
),
_InsertNewlineIntent: CallbackAction<_InsertNewlineIntent>(
onInvoke: (intent) {
_insertNewline();
return null;
},
),
_SelectNextPromptIntent: CallbackAction<_SelectNextPromptIntent>(
onInvoke: (intent) {
_movePromptSelection(1);
return null;
},
),
_SelectPreviousPromptIntent:
CallbackAction<_SelectPreviousPromptIntent>(
onInvoke: (intent) {
_movePromptSelection(-1);
return null; return null;
}, }
), _sendMessage();
_DismissPromptIntent: CallbackAction<_DismissPromptIntent>( return null;
onInvoke: (intent) { },
_hidePromptOverlay(); ),
return null; _InsertNewlineIntent: CallbackAction<_InsertNewlineIntent>(
}, onInvoke: (intent) {
), _insertNewline();
}, return null;
child: Builder( },
builder: (context) { ),
final double factor = isActive ? 1.0 : 0.0; _SelectNextPromptIntent:
final Color animatedPlaceholder = Color.lerp( CallbackAction<_SelectNextPromptIntent>(
placeholderBase, onInvoke: (intent) {
placeholderFocused, _movePromptSelection(1);
factor, return null;
)!; },
final Color animatedTextColor = Color.lerp( ),
context.conduitTheme.inputText.withValues(alpha: 0.88), _SelectPreviousPromptIntent:
context.conduitTheme.inputText, CallbackAction<_SelectPreviousPromptIntent>(
factor, onInvoke: (intent) {
)!; _movePromptSelection(-1);
return null;
},
),
_DismissPromptIntent: CallbackAction<_DismissPromptIntent>(
onInvoke: (intent) {
_hidePromptOverlay();
return null;
},
),
},
child: Builder(
builder: (context) {
final double factor = isActive ? 1.0 : 0.0;
final Color animatedPlaceholder = Color.lerp(
placeholderBase,
placeholderFocused,
factor,
)!;
final Color animatedTextColor = Color.lerp(
context.conduitTheme.inputText.withValues(alpha: 0.88),
context.conduitTheme.inputText,
factor,
)!;
final FontWeight recordingWeight = _isRecording final FontWeight recordingWeight = _isRecording
? FontWeight.w500 ? FontWeight.w500
: FontWeight.w400; : FontWeight.w400;
final TextStyle baseChatStyle = AppTypography.chatMessageStyle; final TextStyle baseChatStyle =
AppTypography.chatMessageStyle;
return TextField( return TextField(
controller: _controller, controller: _controller,
focusNode: _focusNode, focusNode: _focusNode,
enabled: widget.enabled, enabled: widget.enabled,
autofocus: false, autofocus: false,
minLines: 1, minLines: 1,
maxLines: null, maxLines: null,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
textInputAction: sendOnEnter textInputAction: sendOnEnter
? TextInputAction.send ? TextInputAction.send
: TextInputAction.newline, : TextInputAction.newline,
autofillHints: const <String>[], autofillHints: const <String>[],
showCursor: true, showCursor: true,
scrollPadding: const EdgeInsets.only(bottom: 80), scrollPadding: const EdgeInsets.only(bottom: 80),
keyboardAppearance: brightness, keyboardAppearance: brightness,
cursorColor: animatedTextColor, cursorColor: animatedTextColor,
style: baseChatStyle.copyWith( style: baseChatStyle.copyWith(
color: animatedTextColor, color: animatedTextColor,
fontStyle: _isRecording
? FontStyle.italic
: FontStyle.normal,
fontWeight: recordingWeight,
),
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.messageHintText,
hintStyle: baseChatStyle.copyWith(
color: animatedPlaceholder,
fontWeight: recordingWeight,
fontStyle: _isRecording fontStyle: _isRecording
? FontStyle.italic ? FontStyle.italic
: FontStyle.normal, : FontStyle.normal,
fontWeight: recordingWeight,
), ),
filled: false, decoration: InputDecoration(
border: InputBorder.none, hintText: AppLocalizations.of(context)!.messageHintText,
enabledBorder: InputBorder.none, hintStyle: baseChatStyle.copyWith(
focusedBorder: InputBorder.none, color: animatedPlaceholder,
errorBorder: InputBorder.none, fontWeight: recordingWeight,
disabledBorder: InputBorder.none, fontStyle: _isRecording
contentPadding: contentPadding, ? FontStyle.italic
isDense: true, : FontStyle.normal,
alignLabelWithHint: true, ),
), filled: false,
// Enable pasting images and files from clipboard border: InputBorder.none,
contentInsertionConfiguration: ContentInsertionConfiguration( enabledBorder: InputBorder.none,
allowedMimeTypes: ClipboardAttachmentService focusedBorder: InputBorder.none,
.supportedImageMimeTypes errorBorder: InputBorder.none,
.toList(), disabledBorder: InputBorder.none,
onContentInserted: _handleContentInserted, contentPadding: contentPadding,
), isDense: true,
onSubmitted: (_) { alignLabelWithHint: true,
if (sendOnEnter) { ),
_sendMessage(); // Enable pasting images and files from clipboard
} contentInsertionConfiguration:
}, ContentInsertionConfiguration(
onTap: () { allowedMimeTypes: ClipboardAttachmentService
if (!widget.enabled) return; .supportedImageMimeTypes
_ensureFocusedIfEnabled(); .toList(),
}, onContentInserted: _handleContentInserted,
); ),
}, onSubmitted: (_) {
if (sendOnEnter) {
_sendMessage();
}
},
onTap: () {
if (!widget.enabled) return;
_ensureFocusedIfEnabled();
},
);
},
),
), ),
), ),
), ),