diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index c5f626c1b8b..021eb4bdd22 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -435,6 +435,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0-beta.2" + 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_local_notifications: dependency: transitive description: @@ -509,6 +557,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: transitive + description: + name: flutter_typeahead + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d + url: "https://pub.dev" + source: hosted + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -1009,6 +1065,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: d0a8e660d1204eaec5bd34b34cc92174690e076d2e4f893d9d68c486a13b07c4 + url: "https://pub.dev" + source: hosted + version: "0.10.1+1" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: "2e73c39452830adc4695757130676a39412a3b7f3c34e3f752791b5384770877" + url: "https://pub.dev" + source: hosted + version: "0.10.0+2" + 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: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e + url: "https://pub.dev" + source: hosted + version: "0.10.2" pointycastle: dependency: transitive description: diff --git a/packages/neon/neon_talk/lib/src/pages/room.dart b/packages/neon/neon_talk/lib/src/pages/room.dart index c31c08a1e70..9cd05b7025f 100644 --- a/packages/neon/neon_talk/lib/src/pages/room.dart +++ b/packages/neon/neon_talk/lib/src/pages/room.dart @@ -4,13 +4,12 @@ import 'package:flutter/material.dart'; import 'package:intersperse/intersperse.dart'; import 'package:intl/intl.dart'; import 'package:neon_framework/blocs.dart'; -import 'package:neon_framework/theme.dart'; import 'package:neon_framework/utils.dart'; import 'package:neon_framework/widgets.dart'; -import 'package:neon_talk/l10n/localizations.dart'; import 'package:neon_talk/src/blocs/room.dart'; import 'package:neon_talk/src/theme.dart'; import 'package:neon_talk/src/widgets/message.dart'; +import 'package:neon_talk/src/widgets/message_input.dart'; import 'package:neon_talk/src/widgets/room_avatar.dart'; import 'package:nextcloud/utils.dart'; import 'package:timezone/timezone.dart' as tz; @@ -31,9 +30,6 @@ class TalkRoomPage extends StatefulWidget { class _TalkRoomPageState extends State { late final TalkRoomBloc bloc; late final StreamSubscription errorsSubscription; - final messageFormKey = GlobalKey(); - final messageController = TextEditingController(); - final messageFocus = FocusNode(); @override void initState() { @@ -49,21 +45,10 @@ class _TalkRoomPageState extends State { @override void dispose() { unawaited(errorsSubscription.cancel()); - messageController.dispose(); - messageFocus.dispose(); bloc.dispose(); super.dispose(); } - void sendMessage() { - final message = messageController.text; - if (messageFormKey.currentState!.validate()) { - bloc.sendMessage(message); - messageController.clear(); - } - messageFocus.requestFocus(); - } - @override Widget build(BuildContext context) { return ResultBuilder.behaviorSubject( @@ -186,32 +171,6 @@ class _TalkRoomPageState extends State { ); if (room.readOnly == 0) { - Widget? emojiButton; - // On cupertino the keyboard always has an option to insert emojis, so we don't need to add it - if (!isCupertino(context)) { - emojiButton = IconButton( - tooltip: TalkLocalizations.of(context).roomMessageAddEmoji, - onPressed: () async { - final emoji = await showDialog( - context: context, - builder: (context) => const NeonEmojiPickerDialog(), - ); - if (emoji != null) { - final text = messageController.text; - final textSelection = messageController.selection; - - messageController - ..text = text.replaceRange(textSelection.start, textSelection.end, emoji) - ..selection = textSelection.copyWith( - baseOffset: textSelection.start + emoji.length, - extentOffset: textSelection.start + emoji.length, - ); - } - }, - icon: const Icon(Icons.emoji_emotions_outlined), - ); - } - body = Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, @@ -224,25 +183,7 @@ class _TalkRoomPageState extends State { child: Center( child: ConstrainedBox( constraints: Theme.of(context).extension()!.messageConstraints, - child: Form( - key: messageFormKey, - child: TextFormField( - controller: messageController, - focusNode: messageFocus, - textInputAction: TextInputAction.send, - decoration: InputDecoration( - prefixIcon: emojiButton, - suffixIcon: IconButton( - tooltip: TalkLocalizations.of(context).roomMessageSend, - icon: Icon(AdaptiveIcons.send), - onPressed: sendMessage, - ), - hintText: TalkLocalizations.of(context).roomWriteMessage, - ), - validator: (input) => validateNotEmpty(context, input), - onFieldSubmitted: (_) => sendMessage(), - ), - ), + child: const TalkMessageInput(), ), ), ), diff --git a/packages/neon/neon_talk/lib/src/widgets/message_input.dart b/packages/neon/neon_talk/lib/src/widgets/message_input.dart new file mode 100644 index 00000000000..c2a6d09b87d --- /dev/null +++ b/packages/neon/neon_talk/lib/src/widgets/message_input.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:neon_framework/blocs.dart'; +import 'package:neon_framework/models.dart'; +import 'package:neon_framework/theme.dart'; +import 'package:neon_framework/utils.dart'; +import 'package:neon_framework/widgets.dart'; +import 'package:neon_talk/l10n/localizations.dart'; +import 'package:neon_talk/src/blocs/room.dart'; +import 'package:nextcloud/spreed.dart' as spreed; + +/// Widget for displaying the emoji button, text input and send button. +class TalkMessageInput extends StatefulWidget { + /// Creates a new Talk message input. + const TalkMessageInput({ + super.key, + }); + + @override + State createState() => _TalkMessageInputState(); +} + +class _TalkMessageInputState extends State { + final controller = TextEditingController(); + final focusNode = FocusNode(); + late TalkRoomBloc bloc; + + @override + void initState() { + super.initState(); + + bloc = NeonProvider.of(context); + } + + @override + void dispose() { + controller.dispose(); + focusNode.dispose(); + super.dispose(); + } + + void sendMessage() { + final message = controller.text; + if (message.isNotEmpty) { + bloc.sendMessage(message); + controller.clear(); + } + focusNode.requestFocus(); + } + + @override + Widget build(BuildContext context) { + Widget? emojiButton; + // On cupertino the keyboard always has an option to insert emojis, so we don't need to add it + if (!isCupertino(context)) { + emojiButton = IconButton( + tooltip: TalkLocalizations.of(context).roomMessageAddEmoji, + onPressed: () async { + final emoji = await showDialog( + context: context, + builder: (context) => const NeonEmojiPickerDialog(), + ); + if (emoji != null) { + final text = controller.text; + final textSelection = controller.selection; + + controller + ..text = text.replaceRange(textSelection.start, textSelection.end, emoji) + ..selection = textSelection.copyWith( + baseOffset: textSelection.start + emoji.length, + extentOffset: textSelection.start + emoji.length, + ); + } + }, + icon: const Icon(Icons.emoji_emotions_outlined), + ); + } + + return TypeAheadField<_Suggestion>( + direction: VerticalDirection.up, + hideOnEmpty: true, + debounceDuration: const Duration(milliseconds: 50), + controller: controller, + focusNode: focusNode, + suggestionsCallback: (_) async { + final cursor = controller.selection.start; + if (controller.text.isEmpty || cursor != controller.selection.end) { + return []; + } + + String? matchingPart = controller.text.substring(0, cursor); + final index = matchingPart.lastIndexOf(' ') + 1; + matchingPart = matchingPart.substring(index); + if (!matchingPart.startsWith('@') || matchingPart.isEmpty) { + return []; + } + + final account = NeonProvider.of(context); + final response = await account.client.spreed.chat.mentions( + search: matchingPart.substring(1), + token: bloc.room.value.requireData.token, + limit: 5, + ); + + return response.body.ocs.data + .map( + (mention) => _Suggestion( + start: cursor - matchingPart!.length, + end: cursor, + mention: mention, + ), + ) + .toList(); + }, + onSelected: (suggestion) { + final value = '@"${suggestion.mention.id}"'; + final cursor = suggestion.start + value.length; + + controller + ..text = controller.text.replaceRange(suggestion.start, suggestion.end, value) + ..selection = controller.selection.copyWith( + baseOffset: cursor, + extentOffset: cursor, + ); + }, + itemBuilder: buildResult, + builder: (context, controller, focusNode) => TextFormField( + controller: controller, + focusNode: focusNode, + textInputAction: TextInputAction.send, + decoration: InputDecoration( + prefixIcon: emojiButton, + suffixIcon: IconButton( + tooltip: TalkLocalizations.of(context).roomMessageSend, + icon: Icon(AdaptiveIcons.send), + onPressed: sendMessage, + ), + hintText: TalkLocalizations.of(context).roomWriteMessage, + ), + onFieldSubmitted: (_) { + sendMessage(); + }, + ), + errorBuilder: (context, error) => IntrinsicHeight( + child: NeonError( + error, + onRetry: null, + ), + ), + loadingBuilder: (context) => const NeonLinearProgressIndicator(), + ); + } + + Widget buildResult(BuildContext context, _Suggestion suggestion) { + final icon = switch (suggestion.mention.source) { + 'users' => NeonUserAvatar( + account: NeonProvider.of(context), + userStatusBloc: NeonProvider.of(context), + username: suggestion.mention.id, + ), + 'groups' || 'calls' => CircleAvatar( + child: Icon( + AdaptiveIcons.group, + ), + ), + // coverage:ignore-start + _ => throw UnimplementedError('Chat mention suggestion source ${suggestion.mention.source} has no icon'), + // coverage:ignore-end + }; + + return ListTile( + title: Text(suggestion.mention.label), + subtitle: Text(suggestion.mention.id), + leading: icon, + ); + } +} + +class _Suggestion { + _Suggestion({ + required this.start, + required this.end, + required this.mention, + }); + + final int start; + final int end; + final spreed.ChatMentionSuggestion mention; +} diff --git a/packages/neon/neon_talk/pubspec.yaml b/packages/neon/neon_talk/pubspec.yaml index c99e8b4bd20..b3248d2290a 100644 --- a/packages/neon/neon_talk/pubspec.yaml +++ b/packages/neon/neon_talk/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: flutter_localizations: sdk: flutter flutter_material_design_icons: ^1.0.0 + flutter_typeahead: ^5.2.0 go_router: ^14.0.0 intersperse: ^2.0.0 intl: ^0.19.0 @@ -34,6 +35,7 @@ dependencies: dev_dependencies: build_runner: ^2.4.11 custom_lint: ^0.6.4 + flutter_keyboard_visibility: ^6.0.0 flutter_test: sdk: flutter go_router_builder: ^2.7.0 diff --git a/packages/neon/neon_talk/test/goldens/message_input_emoji.png b/packages/neon/neon_talk/test/goldens/message_input_emoji.png new file mode 100644 index 00000000000..cf223a087e0 Binary files /dev/null and b/packages/neon/neon_talk/test/goldens/message_input_emoji.png differ diff --git a/packages/neon/neon_talk/test/goldens/message_input_mention_suggestions.png b/packages/neon/neon_talk/test/goldens/message_input_mention_suggestions.png new file mode 100644 index 00000000000..6165d9325b0 Binary files /dev/null and b/packages/neon/neon_talk/test/goldens/message_input_mention_suggestions.png differ diff --git a/packages/neon/neon_talk/test/goldens/message_input_no_emoji_button.png b/packages/neon/neon_talk/test/goldens/message_input_no_emoji_button.png new file mode 100644 index 00000000000..ae3f87f685e Binary files /dev/null and b/packages/neon/neon_talk/test/goldens/message_input_no_emoji_button.png differ diff --git a/packages/neon/neon_talk/test/goldens/room_page_message_input_emoji.png b/packages/neon/neon_talk/test/goldens/room_page_message_input_emoji.png deleted file mode 100644 index 547adf658ca..00000000000 Binary files a/packages/neon/neon_talk/test/goldens/room_page_message_input_emoji.png and /dev/null differ diff --git a/packages/neon/neon_talk/test/goldens/room_page_message_input_no_emoji_button.png b/packages/neon/neon_talk/test/goldens/room_page_message_input_no_emoji_button.png deleted file mode 100644 index 2909d72430e..00000000000 Binary files a/packages/neon/neon_talk/test/goldens/room_page_message_input_no_emoji_button.png and /dev/null differ diff --git a/packages/neon/neon_talk/test/message_input_test.dart b/packages/neon/neon_talk/test/message_input_test.dart new file mode 100644 index 00000000000..72fbf5ef028 --- /dev/null +++ b/packages/neon/neon_talk/test/message_input_test.dart @@ -0,0 +1,162 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:neon_framework/blocs.dart'; +import 'package:neon_framework/models.dart'; +import 'package:neon_framework/testing.dart'; +import 'package:neon_framework/theme.dart'; +import 'package:neon_framework/utils.dart'; +import 'package:neon_talk/l10n/localizations.dart'; +import 'package:neon_talk/src/blocs/room.dart'; +import 'package:neon_talk/src/widgets/message_input.dart'; +import 'package:provider/provider.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'testing.dart'; + +Account mockTalkAccount() { + return mockServer({ + RegExp(r'/ocs/v2\.php/apps/spreed/api/v1/chat/.*/mentions'): { + 'get': (match, queryParameters) async { + await Future.delayed(const Duration(seconds: 1)); + + return Response( + json.encode({ + 'ocs': { + 'meta': {'status': '', 'statuscode': 0}, + 'data': [ + { + 'id': 'id', + 'label': 'label', + 'source': 'groups', + }, + ], + }, + }), + 200, + ); + }, + }, + }); +} + +void main() { + late TalkRoomBloc bloc; + + setUpAll(() { + KeyboardVisibilityTesting.setVisibilityForTesting(true); + }); + + setUp(() { + FakeNeonStorage.setup(); + + bloc = MockRoomBloc(); + }); + + testWidgets('Cupertino no emoji button', (tester) async { + await tester.pumpWidgetWithAccessibility( + TestApp( + localizationsDelegates: TalkLocalizations.localizationsDelegates, + supportedLocales: TalkLocalizations.supportedLocales, + providers: [ + NeonProvider.value(value: bloc), + ], + platform: TargetPlatform.iOS, + child: const Material( + child: TalkMessageInput(), + ), + ), + ); + + expect(find.byType(TextFormField), findsOne); + expect(find.byIcon(Icons.emoji_emotions_outlined), findsNothing); + await expectLater(find.byType(TestApp), matchesGoldenFile('goldens/message_input_no_emoji_button.png')); + }); + + testWidgets('Emoji button', (tester) async { + SharedPreferences.setMockInitialValues({}); + + await tester.pumpWidgetWithAccessibility( + TestApp( + localizationsDelegates: TalkLocalizations.localizationsDelegates, + supportedLocales: TalkLocalizations.supportedLocales, + providers: [ + NeonProvider.value(value: bloc), + ], + child: const TalkMessageInput(), + ), + ); + + expect(find.byType(TextFormField), findsOne); + expect(find.byIcon(Icons.emoji_emotions_outlined), findsOne); + + await tester.enterText(find.byType(TextField), '123456'); + for (var i = 0; i < 3; i++) { + await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); + await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft); + } + + await tester.runAsync(() async { + await tester.tap(find.byIcon(Icons.emoji_emotions_outlined)); + }); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.tag_faces)); + await tester.pumpAndSettle(); + await tester.tap(find.text('😀')); + await tester.pumpAndSettle(); + + await expectLater(find.byType(TextField), matchesGoldenFile('goldens/message_input_emoji.png')); + + await tester.testTextInput.receiveAction(TextInputAction.send); + verify(() => bloc.sendMessage('123😀456')).called(1); + }); + + testWidgets('Mention suggestions', (tester) async { + final account = mockTalkAccount(); + + final room = MockRoom(); + when(() => room.token).thenReturn('token'); + when(() => bloc.room).thenAnswer((_) => BehaviorSubject.seeded(Result.success(room))); + + await tester.pumpWidgetWithAccessibility( + TestApp( + localizationsDelegates: TalkLocalizations.localizationsDelegates, + supportedLocales: TalkLocalizations.supportedLocales, + providers: [ + NeonProvider.value(value: bloc), + Provider.value(value: account), + ], + child: const Align( + alignment: Alignment.bottomCenter, + child: TalkMessageInput(), + ), + ), + ); + + await tester.enterText(find.byType(TextField), '123 @gr 456'); + + for (var i = 0; i < 4; i++) { + await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); + await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft); + } + + await tester.pumpAndSettle(); + + expect(find.byType(ListTile), findsOne); + expect(find.byIcon(AdaptiveIcons.group), findsOne); + expect(find.text('label'), findsOne); + expect(find.text('id'), findsOne); + + await expectLater(find.byType(TestApp), matchesGoldenFile('goldens/message_input_mention_suggestions.png')); + + await tester.tap(find.byType(ListTile)); + await tester.testTextInput.receiveAction(TextInputAction.send); + verify(() => bloc.sendMessage('123 @"id" 456')).called(1); + }); +} diff --git a/packages/neon/neon_talk/test/room_page_test.dart b/packages/neon/neon_talk/test/room_page_test.dart index cd54b54d117..02ecaa174b2 100644 --- a/packages/neon/neon_talk/test/room_page_test.dart +++ b/packages/neon/neon_talk/test/room_page_test.dart @@ -2,8 +2,9 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:mocktail/mocktail.dart'; import 'package:neon_framework/blocs.dart'; import 'package:neon_framework/models.dart'; @@ -18,7 +19,6 @@ import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/spreed.dart' as spreed; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'testing.dart'; @@ -26,6 +26,10 @@ void main() { late spreed.Room room; late TalkRoomBloc bloc; + setUpAll(() { + KeyboardVisibilityTesting.setVisibilityForTesting(true); + }); + setUp(() { FakeNeonStorage.setup(); }); @@ -197,70 +201,8 @@ void main() { ), ); - expect(find.byType(TextField), findsNothing); + expect(find.byType(TypeAheadField), findsNothing); expect(find.byIcon(Icons.emoji_emotions_outlined), findsNothing); await expectLater(find.byType(TestApp), matchesGoldenFile('goldens/room_page_read_only.png')); }); - - testWidgets('Cupertino no emoji button', (tester) async { - await tester.pumpWidgetWithAccessibility( - TestApp( - localizationsDelegates: TalkLocalizations.localizationsDelegates, - supportedLocales: TalkLocalizations.supportedLocales, - appThemes: const [ - TalkTheme(), - ], - platform: TargetPlatform.iOS, - providers: [ - NeonProvider.value(value: bloc), - ], - child: const TalkRoomPage(), - ), - ); - - expect(find.byType(TextField), findsOne); - expect(find.byIcon(Icons.emoji_emotions_outlined), findsNothing); - await expectLater(find.byType(TestApp), matchesGoldenFile('goldens/room_page_message_input_no_emoji_button.png')); - }); - - testWidgets('Emoji button', (tester) async { - SharedPreferences.setMockInitialValues({}); - - await tester.pumpWidgetWithAccessibility( - TestApp( - localizationsDelegates: TalkLocalizations.localizationsDelegates, - supportedLocales: TalkLocalizations.supportedLocales, - appThemes: const [ - TalkTheme(), - ], - providers: [ - NeonProvider.value(value: bloc), - ], - child: const TalkRoomPage(), - ), - ); - - expect(find.byType(TextField), findsOne); - expect(find.byIcon(Icons.emoji_emotions_outlined), findsOne); - - await tester.enterText(find.byType(TextField), '123456'); - for (var i = 0; i < 3; i++) { - await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); - await simulateKeyUpEvent(LogicalKeyboardKey.arrowLeft); - } - - await tester.runAsync(() async { - await tester.tap(find.byIcon(Icons.emoji_emotions_outlined)); - }); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.tag_faces)); - await tester.pumpAndSettle(); - await tester.tap(find.text('😀')); - await tester.pumpAndSettle(); - - await expectLater(find.byType(TextField), matchesGoldenFile('goldens/room_page_message_input_emoji.png')); - - await tester.testTextInput.receiveAction(TextInputAction.send); - verify(() => bloc.sendMessage('123😀456')).called(1); - }); } diff --git a/packages/neon_framework/lib/src/widgets/error.dart b/packages/neon_framework/lib/src/widgets/error.dart index 262682e4a62..c78b0df36e4 100644 --- a/packages/neon_framework/lib/src/widgets/error.dart +++ b/packages/neon_framework/lib/src/widgets/error.dart @@ -50,7 +50,7 @@ class NeonError extends StatelessWidget { final Object? error; /// A function that's called when the user decides to retry the action that lead to the error. - final VoidCallback onRetry; + final VoidCallback? onRetry; /// The size of the icon in logical pixels. /// diff --git a/packages/nextcloud/lib/src/api/spreed.openapi.dart b/packages/nextcloud/lib/src/api/spreed.openapi.dart index 45c44339f69..7fe6d008344 100644 --- a/packages/nextcloud/lib/src/api/spreed.openapi.dart +++ b/packages/nextcloud/lib/src/api/spreed.openapi.dart @@ -24644,7 +24644,7 @@ abstract interface class $ChatMentionSuggestionInterface { String get id; String get label; String get source; - String get mentionId; + String? get mentionId; String? get status; int? get statusClearAt; String? get statusIcon; diff --git a/packages/nextcloud/lib/src/api/spreed.openapi.g.dart b/packages/nextcloud/lib/src/api/spreed.openapi.g.dart index e40dfe896a0..1ca83c2d9cc 100644 --- a/packages/nextcloud/lib/src/api/spreed.openapi.g.dart +++ b/packages/nextcloud/lib/src/api/spreed.openapi.g.dart @@ -8664,10 +8664,14 @@ class _$ChatMentionSuggestionSerializer implements StructuredSerializer