diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 9d938e0af..af43d5ce1 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -44,6 +44,7 @@ abstract class SettingKeys { } enum AppSettings { + textMessageMaxLength('textMessageMaxLength', 16384), audioRecordingNumChannels('audioRecordingNumChannels', 1), audioRecordingAutoGain('audioRecordingAutoGain', true), audioRecordingEchoCancel('audioRecordingEchoCancel', false), diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 97dbc47b3..5f2f8b3b2 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -289,6 +289,7 @@ // bottom: 6.0, // top: 3.0, // ), +// counter: const SizedBox.shrink(), // hintText: L10n.of(context).writeAMessage, // hintMaxLines: 1, // border: InputBorder.none, diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 77d0bab05..d89cee360 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:emojis/emoji.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:matrix/matrix.dart'; import 'package:slugify/slugify.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/choreographer/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart'; @@ -34,14 +34,16 @@ class InputBar extends StatelessWidget { final FocusNode? focusNode; // #Pangea // final TextEditingController? controller; - final PangeaTextController? controller; - final Choreographer choreographer; - final VoidCallback showNextMatch; // Pangea# final InputDecoration decoration; final ValueChanged? onChanged; final bool? autofocus; final bool readOnly; + // #Pangea + final PangeaTextController? controller; + final Choreographer choreographer; + final VoidCallback showNextMatch; + // Pangea# const InputBar({ required this.room, @@ -64,12 +66,14 @@ class InputBar extends StatelessWidget { super.key, }); - List> getSuggestions(TextEditingValue text) { - if (text.selection.baseOffset != text.selection.extentOffset || - text.selection.baseOffset < 0) { + List> getSuggestions(String text) { + if (controller!.selection.baseOffset != + controller!.selection.extentOffset || + controller!.selection.baseOffset < 0) { return []; // no entries if there is selected text } - final searchText = text.text.substring(0, text.selection.baseOffset); + final searchText = + controller!.text.substring(0, controller!.selection.baseOffset); final ret = >[]; const maxResults = 30; @@ -236,28 +240,33 @@ class InputBar extends StatelessWidget { Widget buildSuggestion( BuildContext context, Map suggestion, - void Function(Map) onSelected, Client? client, ) { final theme = Theme.of(context); const size = 30.0; + const padding = EdgeInsets.all(4.0); if (suggestion['type'] == 'command') { final command = suggestion['name']!; final hint = commandHint(L10n.of(context), command); return Tooltip( message: hint, waitDuration: const Duration(days: 1), // don't show on hover - child: ListTile( - onTap: () => onSelected(suggestion), - title: Text( - commandExample(command), - style: const TextStyle(fontFamily: 'RobotoMono'), - ), - subtitle: Text( - hint, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, + child: Container( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + commandExample(command), + style: const TextStyle(fontFamily: 'RobotoMono'), + ), + Text( + hint, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ), + ], ), ), ); @@ -267,28 +276,29 @@ class InputBar extends StatelessWidget { return Tooltip( message: label, waitDuration: const Duration(days: 1), // don't show on hover - child: ListTile( - onTap: () => onSelected(suggestion), - title: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')), + child: Container( + padding: padding, + child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')), ), ); } if (suggestion['type'] == 'emote') { - return ListTile( - onTap: () => onSelected(suggestion), - leading: MxcImage( - // ensure proper ordering ... - key: ValueKey(suggestion['name']), - uri: suggestion['mxc'] is String - ? Uri.parse(suggestion['mxc'] ?? '') - : null, - width: size, - height: size, - isThumbnail: false, - ), - title: Row( + return Container( + padding: padding, + child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ + MxcImage( + // ensure proper ordering ... + key: ValueKey(suggestion['name']), + uri: suggestion['mxc'] is String + ? Uri.parse(suggestion['mxc'] ?? '') + : null, + width: size, + height: size, + isThumbnail: false, + ), + const SizedBox(width: 6), Text(suggestion['name']!), Expanded( child: Align( @@ -314,22 +324,28 @@ class InputBar extends StatelessWidget { } if (suggestion['type'] == 'user' || suggestion['type'] == 'room') { final url = Uri.parse(suggestion['avatar_url'] ?? ''); - return ListTile( - onTap: () => onSelected(suggestion), - leading: Avatar( - mxContent: url, - name: suggestion.tryGet('displayname') ?? - suggestion.tryGet('mxid'), - size: size, - client: client, + return Container( + padding: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Avatar( + mxContent: url, + name: suggestion.tryGet('displayname') ?? + suggestion.tryGet('mxid'), + size: size, + client: client, + ), + const SizedBox(width: 6), + Text(suggestion['displayname'] ?? suggestion['mxid']!), + ], ), - title: Text(suggestion['displayname'] ?? suggestion['mxid']!), ); } return const SizedBox.shrink(); } - String insertSuggestion(Map suggestion) { + void insertSuggestion(_, Map suggestion) { final replaceText = controller!.text.substring(0, controller!.selection.baseOffset); var startText = ''; @@ -393,8 +409,13 @@ class InputBar extends StatelessWidget { (Match m) => '${m[1]}$insertText', ); } - - return startText + afterText; + if (insertText.isNotEmpty && startText.isNotEmpty) { + controller!.text = startText + afterText; + controller!.selection = TextSelection( + baseOffset: startText.length, + extentOffset: startText.length, + ); + } } // #Pangea @@ -463,116 +484,105 @@ class InputBar extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Autocomplete>( + return TypeAheadField>( + direction: VerticalDirection.up, + hideOnEmpty: true, + hideOnLoading: true, + controller: controller, focusNode: focusNode, - textEditingController: controller, - optionsBuilder: getSuggestions, - fieldViewBuilder: (context, __, focusNode, _) => ValueListenableBuilder( - valueListenable: choreographer.itController.open, - builder: (context, _, __) { - return TextField( - controller: controller, - focusNode: focusNode, - // #Pangea - // readOnly: readOnly, - // contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller), - contextMenuBuilder: (c, e) => - markdownContextBuilder(c, e, controller!), - onTap: () => _onInputTap(context), - readOnly: choreographer.choreoMode == ChoreoModeEnum.it, - autocorrect: MatrixState.pangeaController.userController - .isToolEnabled(ToolSetting.enableAutocorrect), - // Pangea# - contentInsertionConfiguration: ContentInsertionConfiguration( - onContentInserted: (KeyboardInsertedContent content) { - final data = content.data; - if (data == null) return; + hideOnSelect: false, + debounceDuration: const Duration(milliseconds: 50), + // show suggestions after 50ms idle time (default is 300) + // #Pangea + // builder: (context, controller, focusNode) => TextField( + builder: (context, _, focusNode) => TextField( + // Pangea# + controller: controller, + focusNode: focusNode, + // #Pangea + // readOnly: readOnly, + // contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller), + contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller!), + onTap: () => _onInputTap(context), + readOnly: choreographer.choreoMode == ChoreoModeEnum.it, + autocorrect: MatrixState.pangeaController.userController + .isToolEnabled(ToolSetting.enableAutocorrect), + // Pangea# + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (KeyboardInsertedContent content) { + final data = content.data; + if (data == null) return; - final file = MatrixFile( - mimeType: content.mimeType, - bytes: data, - name: content.uri.split('/').last, - ); - room.sendFileEvent( - file, - shrinkImageMaxDimension: 1600, - ); - }, - ), - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - inputFormatters: [ - // #Pangea - //LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), - //setting max character count to 1000 - //after max, nothing else can be typed - LengthLimitingTextInputFormatter(1000), - // Pangea# - ], - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); - }, - // #Pangea - // maxLength: AppSettings.textMessageMaxLength.value, - // decoration: decoration!, - // Pangea# - decoration: decoration.copyWith( - // #Pangea - // hint: ShrinkableText( - hint: StreamBuilder( - stream: MatrixState - .pangeaController.userController.languageStream.stream, - builder: (context, _) => SizedBox( - height: 24, - child: ShrinkableText( - // Pangea# - text: choreographer.itController.open.value - ? L10n.of(context).buildTranslation - : _defaultHintText(context), - maxWidth: double.infinity, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).disabledColor, - ), - ), - ), + final file = MatrixFile( + mimeType: content.mimeType, + bytes: data, + name: content.uri.split('/').last, + ); + room.sendFileEvent( + file, + shrinkImageMaxDimension: 1600, + ); + }, + ), + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + inputFormatters: [ + // #Pangea + //LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()), + //setting max character count to 1000 + //after max, nothing else can be typed + LengthLimitingTextInputFormatter(1000), + // Pangea# + ], + onSubmitted: (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + }, + // #Pangea + // maxLength: + // AppSettings.textMessageMaxLength.getItem(Matrix.of(context).store), + // decoration: decoration, + decoration: decoration.copyWith( + hint: StreamBuilder( + stream: MatrixState + .pangeaController.userController.languageStream.stream, + builder: (context, _) => SizedBox( + height: 24, + child: ShrinkableText( + text: choreographer.itController.open.value + ? L10n.of(context).buildTranslation + : _defaultHintText(context), + maxWidth: double.infinity, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).disabledColor, + ), ), ), - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); - }, - textCapitalization: TextCapitalization.sentences, - ); - }, - ), - optionsViewBuilder: (c, onSelected, s) { - final suggestions = s.toList(); - return Material( - elevation: theme.appBarTheme.scrolledUnderElevation ?? 4, - shadowColor: theme.appBarTheme.shadowColor, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListView.builder( - shrinkWrap: true, - itemCount: suggestions.length, - itemBuilder: (context, i) => buildSuggestion( - c, - suggestions[i], - onSelected, - Matrix.of(context).client, - ), ), - ); - }, - displayStringForOption: insertSuggestion, - optionsViewOpenDirection: OptionsViewOpenDirection.up, + ), + // Pangea# + 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 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 ); } } diff --git a/lib/widgets/config_viewer.dart b/lib/widgets/config_viewer.dart index 5026ec4cc..adfb34407 100644 --- a/lib/widgets/config_viewer.dart +++ b/lib/widgets/config_viewer.dart @@ -7,18 +7,22 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class ConfigViewer extends StatelessWidget { +class ConfigViewer extends StatefulWidget { const ConfigViewer({super.key}); + @override + State createState() => _ConfigViewerState(); +} + +class _ConfigViewerState extends State { void _changeSetting( - BuildContext context, AppSettings appSetting, SharedPreferences store, - Function setState, String initialValue, ) async { if (appSetting is AppSettings) { - appSetting.setItem(store, !(initialValue == 'true')); + await appSetting.setItem(store, !(initialValue == 'true')); + setState(() {}); return; } @@ -31,13 +35,13 @@ class ConfigViewer extends StatelessWidget { if (value == null) return; if (appSetting is AppSettings) { - appSetting.setItem(store, value); + await appSetting.setItem(store, value); } if (appSetting is AppSettings) { - appSetting.setItem(store, int.parse(value)); + await appSetting.setItem(store, int.parse(value)); } if (appSetting is AppSettings) { - appSetting.setItem(store, double.parse(value)); + await appSetting.setItem(store, double.parse(value)); } setState(() {}); @@ -67,38 +71,28 @@ class ConfigViewer extends StatelessWidget { ), ), Expanded( - child: StatefulBuilder( - builder: (context, setState) { - return ListView.builder( - itemCount: AppSettings.values.length, - itemBuilder: (context, i) { - final store = Matrix.of(context).store; - final appSetting = AppSettings.values[i]; - var value = ''; - if (appSetting is AppSettings) { - value = appSetting.getItem(store); - } - if (appSetting is AppSettings) { - value = appSetting.getItem(store).toString(); - } - if (appSetting is AppSettings) { - value = appSetting.getItem(store).toString(); - } - if (appSetting is AppSettings) { - value = appSetting.getItem(store).toString(); - } - return ListTile( - title: Text(appSetting.name), - subtitle: Text(value), - onTap: () => _changeSetting( - context, - appSetting, - store, - setState, - value, - ), - ); - }, + child: ListView.builder( + itemCount: AppSettings.values.length, + itemBuilder: (context, i) { + final store = Matrix.of(context).store; + final appSetting = AppSettings.values[i]; + var value = ''; + if (appSetting is AppSettings) { + value = appSetting.getItem(store); + } + if (appSetting is AppSettings) { + value = appSetting.getItem(store).toString(); + } + if (appSetting is AppSettings) { + value = appSetting.getItem(store).toString(); + } + if (appSetting is AppSettings) { + value = appSetting.getItem(store).toString(); + } + return ListTile( + title: Text(appSetting.name), + subtitle: Text(value), + onTap: () => _changeSetting(appSetting, store, value), ); }, ),