refactor: Easier shift enter logic for text input
This commit is contained in:
parent
8956f81e4e
commit
dda45f783f
2 changed files with 76 additions and 120 deletions
|
|
@ -110,7 +110,7 @@ class ChatController extends State<ChatPageWithRoom>
|
||||||
|
|
||||||
final AutoScrollController scrollController = AutoScrollController();
|
final AutoScrollController scrollController = AutoScrollController();
|
||||||
|
|
||||||
FocusNode inputFocus = FocusNode();
|
late final FocusNode inputFocus;
|
||||||
StreamSubscription<html.Event>? onFocusSub;
|
StreamSubscription<html.Event>? onFocusSub;
|
||||||
|
|
||||||
Timer? typingCoolDown;
|
Timer? typingCoolDown;
|
||||||
|
|
@ -275,8 +275,26 @@ class ChatController extends State<ChatPageWithRoom>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyEventResult _shiftEnterKeyHandling(FocusNode node, KeyEvent evt) {
|
||||||
|
if (!HardwareKeyboard.instance.isShiftPressed &&
|
||||||
|
evt.logicalKey.keyLabel == 'Enter') {
|
||||||
|
if (evt is KeyDownEvent) {
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
inputFocus = FocusNode(
|
||||||
|
onKeyEvent: (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile)
|
||||||
|
? _shiftEnterKeyHandling
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
scrollController.addListener(_updateScrollController);
|
scrollController.addListener(_updateScrollController);
|
||||||
inputFocus.addListener(_inputFocusListener);
|
inputFocus.addListener(_inputFocusListener);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,9 @@ import 'package:emojis/emoji.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
|
||||||
import 'package:slugify/slugify.dart';
|
import 'package:slugify/slugify.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/utils/markdown_context_builder.dart';
|
import 'package:fluffychat/utils/markdown_context_builder.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
|
||||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||||
import '../../widgets/avatar.dart';
|
import '../../widgets/avatar.dart';
|
||||||
import '../../widgets/matrix.dart';
|
import '../../widgets/matrix.dart';
|
||||||
|
|
@ -397,125 +394,66 @@ class InputBar extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final useShortCuts = (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile);
|
return TypeAheadField<Map<String, String?>>(
|
||||||
return Shortcuts(
|
direction: VerticalDirection.up,
|
||||||
shortcuts: !useShortCuts
|
hideOnEmpty: true,
|
||||||
? {}
|
hideOnLoading: true,
|
||||||
: {
|
controller: controller,
|
||||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.enter):
|
focusNode: focusNode,
|
||||||
NewLineIntent(),
|
hideOnSelect: false,
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): SubmitLineIntent(),
|
debounceDuration: const Duration(milliseconds: 50),
|
||||||
LogicalKeySet(
|
// show suggestions after 50ms idle time (default is 300)
|
||||||
LogicalKeyboardKey.controlLeft,
|
builder: (context, controller, focusNode) => TextField(
|
||||||
LogicalKeyboardKey.keyM,
|
controller: controller,
|
||||||
): PasteLineIntent(),
|
focusNode: focusNode,
|
||||||
},
|
contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
|
||||||
child: Actions(
|
contentInsertionConfiguration: ContentInsertionConfiguration(
|
||||||
actions: !useShortCuts
|
onContentInserted: (KeyboardInsertedContent content) {
|
||||||
? {}
|
final data = content.data;
|
||||||
: {
|
if (data == null) return;
|
||||||
NewLineIntent: CallbackAction(
|
|
||||||
onInvoke: (i) {
|
|
||||||
final val = controller!.value;
|
|
||||||
final selection = val.selection.start;
|
|
||||||
final messageWithoutNewLine =
|
|
||||||
'${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}';
|
|
||||||
controller!.value = TextEditingValue(
|
|
||||||
text: messageWithoutNewLine,
|
|
||||||
selection: TextSelection.fromPosition(
|
|
||||||
TextPosition(offset: selection + 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SubmitLineIntent: CallbackAction(
|
|
||||||
onInvoke: (i) {
|
|
||||||
onSubmitted!(controller!.text);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PasteLineIntent: CallbackAction(
|
|
||||||
onInvoke: (i) async {
|
|
||||||
final image = await Pasteboard.image;
|
|
||||||
if (image != null) {
|
|
||||||
onSubmitImage!(image);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
child: TypeAheadField<Map<String, String?>>(
|
|
||||||
direction: VerticalDirection.up,
|
|
||||||
hideOnEmpty: true,
|
|
||||||
hideOnLoading: true,
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
hideOnSelect: false,
|
|
||||||
debounceDuration: const Duration(milliseconds: 50),
|
|
||||||
// show suggestions after 50ms idle time (default is 300)
|
|
||||||
builder: (context, controller, focusNode) => TextField(
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
contextMenuBuilder: (c, e) =>
|
|
||||||
markdownContextBuilder(c, e, controller),
|
|
||||||
contentInsertionConfiguration: ContentInsertionConfiguration(
|
|
||||||
onContentInserted: (KeyboardInsertedContent content) {
|
|
||||||
final data = content.data;
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
final file = MatrixFile(
|
final file = MatrixFile(
|
||||||
mimeType: content.mimeType,
|
mimeType: content.mimeType,
|
||||||
bytes: data,
|
bytes: data,
|
||||||
name: content.uri.split('/').last,
|
name: content.uri.split('/').last,
|
||||||
);
|
);
|
||||||
room.sendFileEvent(
|
room.sendFileEvent(
|
||||||
file,
|
file,
|
||||||
shrinkImageMaxDimension: 1600,
|
shrinkImageMaxDimension: 1600,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
minLines: minLines,
|
|
||||||
maxLines: maxLines,
|
|
||||||
keyboardType: keyboardType!,
|
|
||||||
textInputAction: textInputAction,
|
|
||||||
autofocus: autofocus!,
|
|
||||||
inputFormatters: [
|
|
||||||
LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
|
|
||||||
],
|
|
||||||
onSubmitted: (text) {
|
|
||||||
// fix for library for now
|
|
||||||
// it sets the types for the callback incorrectly
|
|
||||||
onSubmitted!(text);
|
|
||||||
},
|
|
||||||
decoration: decoration!,
|
|
||||||
onChanged: (text) {
|
|
||||||
// fix for the library for now
|
|
||||||
// it sets the types for the callback incorrectly
|
|
||||||
onChanged!(text);
|
|
||||||
},
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
),
|
|
||||||
suggestionsCallback: getSuggestions,
|
|
||||||
itemBuilder: (c, s) =>
|
|
||||||
buildSuggestion(c, s, Matrix.of(context).client),
|
|
||||||
onSelected: (Map<String, String?> suggestion) =>
|
|
||||||
insertSuggestion(context, suggestion),
|
|
||||||
errorBuilder: (BuildContext context, Object? error) =>
|
|
||||||
const SizedBox.shrink(),
|
|
||||||
loadingBuilder: (BuildContext context) => const SizedBox.shrink(),
|
|
||||||
// fix loading briefly flickering a dark box
|
|
||||||
emptyBuilder: (BuildContext context) => const SizedBox
|
|
||||||
.shrink(), // fix loading briefly showing no suggestions
|
|
||||||
),
|
),
|
||||||
|
minLines: minLines,
|
||||||
|
maxLines: maxLines,
|
||||||
|
keyboardType: keyboardType!,
|
||||||
|
textInputAction: textInputAction,
|
||||||
|
autofocus: autofocus!,
|
||||||
|
inputFormatters: [
|
||||||
|
LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
|
||||||
|
],
|
||||||
|
onSubmitted: (text) {
|
||||||
|
// fix for library for now
|
||||||
|
// it sets the types for the callback incorrectly
|
||||||
|
onSubmitted!(text);
|
||||||
|
},
|
||||||
|
decoration: decoration!,
|
||||||
|
onChanged: (text) {
|
||||||
|
// fix for the library for now
|
||||||
|
// it sets the types for the callback incorrectly
|
||||||
|
onChanged!(text);
|
||||||
|
},
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
),
|
),
|
||||||
|
suggestionsCallback: getSuggestions,
|
||||||
|
itemBuilder: (c, s) => buildSuggestion(c, s, Matrix.of(context).client),
|
||||||
|
onSelected: (Map<String, String?> suggestion) =>
|
||||||
|
insertSuggestion(context, suggestion),
|
||||||
|
errorBuilder: (BuildContext context, Object? error) =>
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
loadingBuilder: (BuildContext context) => const SizedBox.shrink(),
|
||||||
|
// fix loading briefly flickering a dark box
|
||||||
|
emptyBuilder: (BuildContext context) =>
|
||||||
|
const SizedBox.shrink(), // fix loading briefly showing no suggestions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewLineIntent extends Intent {}
|
|
||||||
|
|
||||||
class SubmitLineIntent extends Intent {}
|
|
||||||
|
|
||||||
class PasteLineIntent extends Intent {}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue