From 4032a09c3ea0c44f0a423da5421293352c9327a2 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Sat, 1 Jun 2024 17:28:05 +0200 Subject: [PATCH 1/4] fix(nextcloud)!: Fix spreed chat mention suggestions compatibility Signed-off-by: provokateurin --- .../nextcloud/lib/src/api/spreed.openapi.dart | 2 +- .../nextcloud/lib/src/api/spreed.openapi.g.dart | 17 ++++++++++------- .../nextcloud/lib/src/api/spreed.openapi.json | 1 - .../src/patches/spreed/0-compatibility-18.json | 13 +++++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) 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 Date: Sat, 1 Jun 2024 17:28:29 +0200 Subject: [PATCH 2/4] test(nextcloud): Test spreed chat mention suggestions Signed-off-by: provokateurin --- .../spreed/chat/mention_suggestions.regexp | 12 ++++++++++ packages/nextcloud/test/spreed_test.dart | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/nextcloud/test/fixtures/spreed/chat/mention_suggestions.regexp diff --git a/packages/nextcloud/test/fixtures/spreed/chat/mention_suggestions.regexp b/packages/nextcloud/test/fixtures/spreed/chat/mention_suggestions.regexp new file mode 100644 index 00000000000..c48eca3cdd2 --- /dev/null +++ b/packages/nextcloud/test/fixtures/spreed/chat/mention_suggestions.regexp @@ -0,0 +1,12 @@ +POST http://localhost/ocs/v2\.php/apps/spreed/api/v4/room\?roomType=3&invite=&roomName=Test&source=&objectType=&objectId= +accept: application/json +authorization: Bearer mock +ocs-apirequest: true +POST http://localhost/ocs/v2\.php/apps/spreed/api/v4/room/[a-z0-9]{8}/participants\?newParticipant=user2&source=users +accept: application/json +authorization: Bearer mock +ocs-apirequest: true +GET http://localhost/ocs/v2\.php/apps/spreed/api/v1/chat/[a-z0-9]{8}/mentions\?search=user&limit=20&includeStatus=0 +accept: application/json +authorization: Bearer mock +ocs-apirequest: true \ No newline at end of file diff --git a/packages/nextcloud/test/spreed_test.dart b/packages/nextcloud/test/spreed_test.dart index 6d2aa6cc437..6f4a7f9a557 100644 --- a/packages/nextcloud/test/spreed_test.dart +++ b/packages/nextcloud/test/spreed_test.dart @@ -9,6 +9,7 @@ import 'package:nextcloud/src/utils/date_time.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; import 'package:test_api/src/backend/invoker.dart'; +import 'package:version/version.dart'; void main() { presets( @@ -347,6 +348,29 @@ void main() { expect(response.body.ocs.data[0].messageType, spreed.MessageType.comment); }); }); + + test('Mention suggestions', () async { + final room = await createTestRoom(); + + await client1.spreed.room.addParticipantToRoom( + newParticipant: 'user2', + token: room.token, + ); + + final response = await client1.spreed.chat.mentions( + search: 'user', + token: room.token, + ); + expect(response.body.ocs.data, hasLength(1)); + expect(response.body.ocs.data[0].id, 'user2'); + expect(response.body.ocs.data[0].label, 'User Two'); + expect(response.body.ocs.data[0].source, 'users'); + expect(response.body.ocs.data[0].mentionId, 'user2', skip: preset.version < Version(19, 0, 0)); + expect(response.body.ocs.data[0].status, null); + expect(response.body.ocs.data[0].statusClearAt, null); + expect(response.body.ocs.data[0].statusIcon, null); + expect(response.body.ocs.data[0].statusMessage, null); + }); }); group('Call', () { From db65d2bb7cc3451bc1396d30107eb6cd1925514e Mon Sep 17 00:00:00 2001 From: provokateurin Date: Sat, 1 Jun 2024 19:43:19 +0200 Subject: [PATCH 3/4] feat(neon_framework): Allow passing no refresh callback to NeonError Signed-off-by: provokateurin --- packages/neon_framework/lib/src/widgets/error.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. /// From 7c1fcf281deb92a4123205a6cf5254637379c9cf Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 23 May 2024 19:26:00 +0200 Subject: [PATCH 4/4] feat(neon_talk): Add mention autocompletion Signed-off-by: provokateurin --- packages/app/pubspec.lock | 88 ++++++++ .../neon/neon_talk/lib/src/pages/room.dart | 63 +----- .../lib/src/widgets/message_input.dart | 189 ++++++++++++++++++ packages/neon/neon_talk/pubspec.yaml | 2 + .../test/goldens/message_input_emoji.png | Bin 0 -> 3692 bytes .../message_input_mention_suggestions.png | Bin 0 -> 28487 bytes .../goldens/message_input_no_emoji_button.png | Bin 0 -> 24430 bytes .../goldens/room_page_message_input_emoji.png | Bin 4552 -> 0 bytes ...oom_page_message_input_no_emoji_button.png | Bin 26400 -> 0 bytes .../neon_talk/test/message_input_test.dart | 162 +++++++++++++++ .../neon/neon_talk/test/room_page_test.dart | 72 +------ 11 files changed, 450 insertions(+), 126 deletions(-) create mode 100644 packages/neon/neon_talk/lib/src/widgets/message_input.dart create mode 100644 packages/neon/neon_talk/test/goldens/message_input_emoji.png create mode 100644 packages/neon/neon_talk/test/goldens/message_input_mention_suggestions.png create mode 100644 packages/neon/neon_talk/test/goldens/message_input_no_emoji_button.png delete mode 100644 packages/neon/neon_talk/test/goldens/room_page_message_input_emoji.png delete mode 100644 packages/neon/neon_talk/test/goldens/room_page_message_input_no_emoji_button.png create mode 100644 packages/neon/neon_talk/test/message_input_test.dart 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 0000000000000000000000000000000000000000..cf223a087e05b4e931fbc9e64e0819d2493763f7 GIT binary patch literal 3692 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9jkefq_rV)5S5QV$Rz;dskmil{oP5w@k~MvySaWt1e#(8SfcYS+)+SX3~ zUHkh}OSNauo?r9x?4HjLpBKM7bnH*v%Tqu%=p4#5zi*PgyZr1zclkMczuz_1mY@5x z?DJCP_j=WO3=9d~)qj7gr{Df{^0Kk<=MT)^&t3lg?aZxrvEFjr3=B84&c)uCk$l|$ z?D6;-+x*%OH`mmCf2YI_bg@xh9CWK=cYaVw><9doS8r0oE4vL z3k>58uC`z9WE&?lGcho7OOg>Nm_m+VTIun%x6{*}-D&Mr?FSl?<66J{ zSYPz^^RHsd-^`gg6BMUEcy21+n8)^%BFY43I=KVMT*^6d86>3w^4KiLS3 zqqXmP#plhL>K$%oZvXeyn>F)oY}5Du&(w?imwdAr7=6)4-fqvCzxD3!XBU+D&&~UN z?H|{KRDMv}fc4fJjYc4HVkUHx3vIVCg!0G80b9{>OV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6165d9325b0d861c340369f98ed011d23a893b80 GIT binary patch literal 28487 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

Yi8o=*%W=*wRuNbl0*h^znr303Dxmk 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 literal 0 HcmV?d00001 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 547adf658ca63ba36b151376d3b343c7c799e362..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4552 zcmeHLeNalZN4p$zU0luA}U3X(`sm#!5@P;mJyu?gEDARq(?CRw$` zN+E7p#SkFtqNOS!Pzoso61oyTAEZkvgv9U>Fe(Isgd{u?lfAe*yY0-*&UF9W;r(;p zJNMpq&;6Zq?m55v>B$qvwr<(C1pu%$@;4Eu0Kj?xU>E1O2}&ySE}e%BCg;?#aL_R9 z`W$}P=Y&VbIKrpM@!VAawr5919FEDaT2OWJRi?zQ7t$8KTS%6j+_BL)eA6F4KEC*! zbL$0+A#%>0AdEPDx;vcx&aZt=?@l?)yZ`$Bw=dKyzWB?vm`yQ9?w$0!opHPS{+2su z)K@-{k4cNC`zXJ7G;}e@bWu8VvGY!C$ZFN3$umkU5Q&RRc`hZAb#2PrR$$Zhw>Jm? zFh2tr0Q~Lj0qApd2EfDf-$IT<%Tl!v7n?bhuBvIvC2K}iY!3CNEpK@h`ILzBE|XSG zR24j%MP8I%uKYOwaW_hrk6aPxhC7-L>Ww_Mi-2lWMsslF6T=q8J@)(N007SK_rheG z7m*u0b|7D>&21?y{qRavh;IY{Q;p+e;OvL_9kNbm)F8w4e(VT<>lSm~oah#NjPL8$ zreJlS^|{$gUr=QG0+8}@VQ^btwFF^JRF1c|%z4^U>!J|LuxCb$17PL~mffyP5X&>~ z#3ICJY>h9_UV)o#k1t>*&}nFzO3@NO*n$B}L(7PX*->y2ZR3`(#F<|69V=?nqR@)v z402@Dxxea?U+IrQ0736ec#Vl^2Hn6+tA%qh`} zqPhnD^xY32xPMv*CXAxhF7u%-5V|9;tg1ak7@c}@*|cuhlum6hA>hn`pTV(j!q-uh zGV{Z+>5C{3hW3kBSIx%u72ysK02-5wMc3#bNjEn$Mhl<;gRU7}J)F2dQi5jh&GN$s z>N?ES^Bse}KdKza2xrtu5a=74p?P!G3KxA8jT7gRdMNG?1gp`Puvnm9 zLs*j&Tp}0W{5p)4sq=CZV6)o_$eMvsB&xxWdeoRoB+;LIzx;{5yf^lM8vrLG?PhD@ zspN(P4m#U(hPjcq>bv02+IISv5fC=)`r7h?y&IeQo<*SC9Y2sZ%PaE6UQ*f<$B zPQ{Iku#pWnvf=+tHYDYECtJrDwr?#my)B?3snb6*QzhrKVL5n^s#(G2Ok3K}Zmx)7 zU2U3F8g&7!89uv|sG2IW4HRigl2ry+Eq2jllk_#BzMHta)u5v2n4xB#ZWuwzce_{D zg{^ca1TiwGGROvtC+^1b5uCL1ZQJt5px()5QMY2xDNRhvlsK8g-VO`muJyq+Yv1j} z;q(s&GtAOmbmOR}P`?Ex- zfzO)_Dt;tLE-VN$BY~R!64vlj?%L8}78Z0>1|5H@%p?43oqn%4d}f{Uod=2&cCc7Z zQAq`Q5WGLiW{~N%_%Px8Bm@0za!nL{u3Tt+*=y6z;Bd6cX&J-N)@HFxN@Hud=)i8P znm~$^*dEHbw8Am511$A(Hba{CQ;Q2ybJEgfcQmc@O-RS~&eKH{2S_&lLA1*gE*XaS z3|<_(H7L9D@WMa)($iIOKCOC5FWwLc!BtmSvy&avR?$mniE*;%KvYuEg)rOq#ZHB; z0LZhv?>-1=b@LFLIbQ)B#(NU35Xc!39KYk6TZ$2Y0dv)cICt9u6 ziT$~#I+hv;DbZ4mfy%Hpp|$vqMxHJsgh0BveOW6Ih43xX`5eRAP@>a1@y~F==YRB8 z6+V!=T5gi=WKsOWNO^l8%K^0MU&j5U<6UFJP=}13PZJ AWdHyG 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 2909d72430ead04576cf4b474a3317938c31cd3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26400 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 c8&nPYZn3u5v#Cg=b;EM?E*Rn+a 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); - }); }