From 99de670e04d05b2050ec4fb1d2bbb6e7d731674d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Tue, 2 Sep 2025 12:08:41 +0200 Subject: [PATCH 1/2] build: Upgrade to flutter 3.35.2 --- .github/workflows/versions.env | 2 +- .../chat_access_settings_page.dart | 96 +++++++++++-------- .../chat_encryption_settings_view.dart | 2 +- snap/snapcraft.yaml | 2 +- 4 files changed, 61 insertions(+), 41 deletions(-) diff --git a/.github/workflows/versions.env b/.github/workflows/versions.env index 660760f68..5a23ceb3e 100644 --- a/.github/workflows/versions.env +++ b/.github/workflows/versions.env @@ -1,2 +1,2 @@ -FLUTTER_VERSION=3.32.8 +FLUTTER_VERSION=3.35.2 JAVA_VERSION=17 diff --git a/lib/pages/chat_access_settings/chat_access_settings_page.dart b/lib/pages/chat_access_settings/chat_access_settings_page.dart index a316b809e..5f679cffb 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_page.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_page.dart @@ -45,19 +45,27 @@ class ChatAccessSettingsPageView extends StatelessWidget { ), ), ), - for (final historyVisibility in HistoryVisibility.values) - RadioListTile.adaptive( - title: Text( - historyVisibility - .getLocalizedString(MatrixLocals(L10n.of(context))), - ), - value: historyVisibility, - groupValue: room.historyVisibility, - onChanged: controller.historyVisibilityLoading || - !room.canChangeHistoryVisibility - ? null - : controller.setHistoryVisibility, + RadioGroup( + groupValue: room.historyVisibility, + onChanged: controller.historyVisibilityLoading || + !room.canChangeHistoryVisibility + ? (_) {} + : controller.setHistoryVisibility, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final historyVisibility in HistoryVisibility.values) + RadioListTile.adaptive( + title: Text( + historyVisibility.getLocalizedString( + MatrixLocals(L10n.of(context)), + ), + ), + value: historyVisibility, + ), + ], ), + ), Divider(color: theme.dividerColor), ListTile( title: Text( @@ -68,19 +76,25 @@ class ChatAccessSettingsPageView extends StatelessWidget { ), ), ), - for (final joinRule in controller.availableJoinRules) - if (joinRule != JoinRules.private) - RadioListTile.adaptive( - title: Text( - joinRule.localizedString(L10n.of(context)), - ), - value: joinRule, - groupValue: room.joinRules, - onChanged: controller.joinRulesLoading || - !room.canChangeJoinRules - ? null - : controller.setJoinRule, - ), + RadioGroup( + groupValue: room.joinRules, + onChanged: controller.setJoinRule, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final joinRule in controller.availableJoinRules) + if (joinRule != JoinRules.private) + RadioListTile.adaptive( + enabled: !controller.joinRulesLoading && + room.canChangeJoinRules, + title: Text( + joinRule.localizedString(L10n.of(context)), + ), + value: joinRule, + ), + ], + ), + ), Divider(color: theme.dividerColor), if ({JoinRules.public, JoinRules.knock} .contains(room.joinRules)) ...[ @@ -93,20 +107,26 @@ class ChatAccessSettingsPageView extends StatelessWidget { ), ), ), - for (final guestAccess in GuestAccess.values) - RadioListTile.adaptive( - title: Text( - guestAccess.getLocalizedString( - MatrixLocals(L10n.of(context)), - ), - ), - value: guestAccess, - groupValue: room.guestAccess, - onChanged: controller.guestAccessLoading || - !room.canChangeGuestAccess - ? null - : controller.setGuestAccess, + RadioGroup( + groupValue: room.guestAccess, + onChanged: controller.setGuestAccess, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final guestAccess in GuestAccess.values) + RadioListTile.adaptive( + enabled: !controller.guestAccessLoading && + room.canChangeGuestAccess, + title: Text( + guestAccess.getLocalizedString( + MatrixLocals(L10n.of(context)), + ), + ), + value: guestAccess, + ), + ], ), + ), Divider(color: theme.dividerColor), ListTile( title: Text( diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index 87dd999ce..f6adffd58 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -110,7 +110,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { itemBuilder: (BuildContext context, int i) => SwitchListTile( value: !deviceKeys[i].blocked, - activeColor: deviceKeys[i].verified + activeThumbColor: deviceKeys[i].verified ? Colors.green : Colors.orange, onChanged: (_) => diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index f21b452d0..3fe4d1e9e 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -53,7 +53,7 @@ platforms: parts: flutter-git: source: https://github.com/flutter/flutter.git - source-tag: 3.32.4 + source-tag: 3.35.2 source-depth: 1 plugin: nil override-build: | 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 2/2] 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