From 34492bd45e74c00740503249373e53563cb0dbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Tue, 2 Sep 2025 14:57:10 +0200 Subject: [PATCH] refactor: Replace flutter typeahead with autocomplete to fix --- lib/pages/chat/input_bar.dart | 155 ++++++++++++++++------------------ pubspec.lock | 89 ------------------- pubspec.yaml | 4 - 3 files changed, 71 insertions(+), 177 deletions(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 010450067..cfb4bb16e 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/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/markdown_context_builder.dart'; @@ -46,14 +46,12 @@ class InputBar extends StatelessWidget { super.key, }); - List> getSuggestions(String text) { - if (controller!.selection.baseOffset != - controller!.selection.extentOffset || - controller!.selection.baseOffset < 0) { + List> getSuggestions(TextEditingValue text) { + if (text.selection.baseOffset != text.selection.extentOffset || + text.selection.baseOffset < 0) { return []; // no entries if there is selected text } - final searchText = - controller!.text.substring(0, controller!.selection.baseOffset); + final searchText = text.text.substring(0, text.selection.baseOffset); final ret = >[]; const maxResults = 30; @@ -218,33 +216,28 @@ 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: 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, - ), - ], + 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, ), ), ); @@ -254,29 +247,28 @@ class InputBar extends StatelessWidget { return Tooltip( message: label, waitDuration: const Duration(days: 1), // don't show on hover - child: Container( - padding: padding, - child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')), + child: ListTile( + onTap: () => onSelected(suggestion), + title: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')), ), ); } if (suggestion['type'] == 'emote') { - return Container( - padding: padding, - child: Row( + 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( 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( @@ -302,28 +294,22 @@ class InputBar extends StatelessWidget { } if (suggestion['type'] == 'user' || suggestion['type'] == 'room') { final url = Uri.parse(suggestion['avatar_url'] ?? ''); - 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']!), - ], + return ListTile( + onTap: () => onSelected(suggestion), + leading: Avatar( + mxContent: url, + name: suggestion.tryGet('displayname') ?? + suggestion.tryGet('mxid'), + size: size, + client: client, ), + title: Text(suggestion['displayname'] ?? suggestion['mxid']!), ); } return const SizedBox.shrink(); } - void insertSuggestion(_, Map suggestion) { + String insertSuggestion(Map suggestion) { final replaceText = controller!.text.substring(0, controller!.selection.baseOffset); var startText = ''; @@ -384,27 +370,18 @@ class InputBar extends StatelessWidget { (Match m) => '${m[1]}$insertText', ); } - if (insertText.isNotEmpty && startText.isNotEmpty) { - controller!.text = startText + afterText; - controller!.selection = TextSelection( - baseOffset: startText.length, - extentOffset: startText.length, - ); - } + + return startText + afterText; } @override Widget build(BuildContext context) { - return TypeAheadField>( - direction: VerticalDirection.up, - hideOnEmpty: true, - hideOnLoading: true, - controller: controller, + final theme = Theme.of(context); + return Autocomplete>( focusNode: focusNode, - hideOnSelect: false, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - builder: (context, controller, focusNode) => TextField( + textEditingController: controller, + optionsBuilder: getSuggestions, + fieldViewBuilder: (context, controller, focusNode, _) => TextField( controller: controller, focusNode: focusNode, readOnly: readOnly, @@ -448,17 +425,27 @@ class InputBar extends StatelessWidget { }, 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 + 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, ); } } diff --git a/pubspec.lock b/pubspec.lock index daf256d25..80a0a6207 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -507,54 +507,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.1" - flutter_keyboard_visibility: - dependency: transitive - description: - name: flutter_keyboard_visibility - sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_keyboard_visibility_linux: - dependency: transitive - description: - name: flutter_keyboard_visibility_linux - sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_macos: - dependency: transitive - description: - name: flutter_keyboard_visibility_macos - sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_windows: - dependency: transitive - description: - name: flutter_keyboard_visibility_windows - sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 - url: "https://pub.dev" - source: hosted - version: "1.0.0" flutter_linkify: dependency: "direct main" description: @@ -718,15 +670,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_typeahead: - dependency: "direct main" - description: - path: "." - ref: main - resolved-ref: "3e209e67aa6e780cba61ced06cf49d2babbbcaa4" - url: "https://github.com/famedly/flutter_typeahead.git" - source: git - version: "5.2.0" flutter_vodozemac: dependency: "direct main" description: @@ -1415,38 +1358,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointer_interceptor: - dependency: transitive - description: - name: pointer_interceptor - sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" - url: "https://pub.dev" - source: hosted - version: "0.10.1+2" - pointer_interceptor_ios: - dependency: transitive - description: - name: pointer_interceptor_ios - sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - pointer_interceptor_platform_interface: - dependency: transitive - description: - name: pointer_interceptor_platform_interface - sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" - url: "https://pub.dev" - source: hosted - version: "0.10.0+1" - pointer_interceptor_web: - dependency: transitive - description: - name: pointer_interceptor_web - sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" - url: "https://pub.dev" - source: hosted - version: "0.10.2+1" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e5df801e8..ffb29ab9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,10 +39,6 @@ dependencies: flutter_openssl_crypto: ^0.5.0 flutter_secure_storage: ^9.2.4 flutter_shortcuts_new: ^2.0.0 - flutter_typeahead: ## Custom fork from flutter_typeahead since the package is not maintain well. - git: - url: https://github.com/famedly/flutter_typeahead.git - ref: main flutter_vodozemac: ^0.2.2 flutter_web_auth_2: ^3.1.1 # Version 4 blocked by https://github.com/MixinNetwork/flutter-plugins/issues/379 flutter_webrtc: ^1.0.0