diff --git a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/deck_card.dart b/packages/neon_framework/lib/src/widgets/rich_text/deck_card.dart similarity index 88% rename from packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/deck_card.dart rename to packages/neon_framework/lib/src/widgets/rich_text/deck_card.dart index c1ca12374b2..cff8e2602b6 100644 --- a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/deck_card.dart +++ b/packages/neon_framework/lib/src/widgets/rich_text/deck_card.dart @@ -5,9 +5,9 @@ import 'package:neon_framework/utils.dart'; import 'package:nextcloud/core.dart' as core; /// Widget to display a Deck card from a rich object. -class TalkRichObjectDeckCard extends StatelessWidget { - /// Creates a new Talk rich object Deck card. - const TalkRichObjectDeckCard({ +class NeonRichObjectDeckCard extends StatelessWidget { + /// Creates a new Neon rich object Deck card. + const NeonRichObjectDeckCard({ required this.parameter, super.key, }); diff --git a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/fallback.dart b/packages/neon_framework/lib/src/widgets/rich_text/fallback.dart similarity index 92% rename from packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/fallback.dart rename to packages/neon_framework/lib/src/widgets/rich_text/fallback.dart index a35a31482a0..6537d7c74c2 100644 --- a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/fallback.dart +++ b/packages/neon_framework/lib/src/widgets/rich_text/fallback.dart @@ -5,9 +5,9 @@ import 'package:neon_framework/widgets.dart'; import 'package:nextcloud/core.dart' as core; /// Widget used to render rich object parameters with unknown types. -class TalkRichObjectFallback extends StatelessWidget { - /// Creates a new Talk rich object fallback - const TalkRichObjectFallback({ +class NeonRichObjectFallback extends StatelessWidget { + /// Creates a new Neon rich object fallback + const NeonRichObjectFallback({ required this.parameter, required this.textStyle, super.key, diff --git a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/file.dart b/packages/neon_framework/lib/src/widgets/rich_text/file.dart similarity index 96% rename from packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/file.dart rename to packages/neon_framework/lib/src/widgets/rich_text/file.dart index de1c28fc63f..6c61e583b16 100644 --- a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/file.dart +++ b/packages/neon_framework/lib/src/widgets/rich_text/file.dart @@ -5,9 +5,9 @@ import 'package:neon_framework/widgets.dart'; import 'package:nextcloud/core.dart' as core; /// Displays a file from a rich object. -class TalkRichObjectFile extends StatelessWidget { - /// Creates a new Talk rich object file. - const TalkRichObjectFile({ +class NeonRichObjectFile extends StatelessWidget { + /// Creates a new Neon rich object file. + const NeonRichObjectFile({ required this.parameter, required this.textStyle, super.key, diff --git a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/mention.dart b/packages/neon_framework/lib/src/widgets/rich_text/mention.dart similarity index 95% rename from packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/mention.dart rename to packages/neon_framework/lib/src/widgets/rich_text/mention.dart index f260839b5ee..8a19b20aee0 100644 --- a/packages/neon_framework/packages/talk_app/lib/src/widgets/rich_object/mention.dart +++ b/packages/neon_framework/lib/src/widgets/rich_text/mention.dart @@ -8,9 +8,9 @@ import 'package:neon_framework/widgets.dart'; import 'package:nextcloud/core.dart' as core; /// Displays a mention chip from a rich object. -class TalkRichObjectMention extends StatelessWidget { - /// Create a new Talk rich object mention. - const TalkRichObjectMention({ +class NeonRichObjectMention extends StatelessWidget { + /// Create a new Neon rich object mention. + const NeonRichObjectMention({ required this.parameter, required this.textStyle, super.key, diff --git a/packages/neon_framework/lib/src/widgets/rich_text/rich_text.dart b/packages/neon_framework/lib/src/widgets/rich_text/rich_text.dart new file mode 100644 index 00000000000..e4263c229b6 --- /dev/null +++ b/packages/neon_framework/lib/src/widgets/rich_text/rich_text.dart @@ -0,0 +1,173 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intersperse/intersperse.dart'; +import 'package:neon_framework/src/widgets/rich_text/deck_card.dart'; +import 'package:neon_framework/src/widgets/rich_text/fallback.dart'; +import 'package:neon_framework/src/widgets/rich_text/file.dart'; +import 'package:neon_framework/src/widgets/rich_text/mention.dart'; +import 'package:nextcloud/core.dart' as core; + +export 'package:neon_framework/src/widgets/rich_text/deck_card.dart'; +export 'package:neon_framework/src/widgets/rich_text/fallback.dart'; +export 'package:neon_framework/src/widgets/rich_text/file.dart'; +export 'package:neon_framework/src/widgets/rich_text/mention.dart'; + +/// Renders the [text] as a rich [TextSpan]. +TextSpan buildRichTextSpan({ + required String text, + required BuiltMap> parameters, + required BuiltList references, + required TextStyle style, + required void Function(String reference) onReferenceClicked, + bool isPreview = false, +}) { + if (isPreview) { + text = text.replaceAll('\n', ' '); + } + + final unusedParameters = {}; + + var parts = [text]; + for (final entry in parameters.entries) { + final newParts = []; + + var found = false; + for (final part in parts) { + final p = part.split('{${entry.key}}'); + newParts.addAll(p.intersperse('{${entry.key}}')); + if (p.length > 1) { + found = true; + } + } + + if (!found) { + unusedParameters[entry.key] = core.RichObjectParameter.fromJson( + entry.value.map((key, value) => MapEntry(key, value.toString())).toMap(), + ); + } + + parts = newParts; + } + for (final reference in references) { + final newParts = []; + + for (final part in parts) { + final p = part.split(reference); + newParts.addAll(p.intersperse(reference)); + } + + parts = newParts; + } + + final children = []; + + for (final entry in unusedParameters.entries) { + if (entry.key == core.RichObjectParameter_Type.file.value) { + children + ..add( + buildRichObjectParameter( + parameter: entry.value, + textStyle: style, + isPreview: isPreview, + ), + ) + ..add(const TextSpan(text: '\n')); + } + } + + for (final part in parts) { + if (part.isEmpty) { + continue; + } + + var match = false; + for (final entry in parameters.entries) { + if ('{${entry.key}}' == part) { + children.add( + buildRichObjectParameter( + parameter: core.RichObjectParameter.fromJson( + entry.value.map((key, value) => MapEntry(key, value.toString())).toMap(), + ), + textStyle: style, + isPreview: isPreview, + ), + ); + match = true; + break; + } + } + for (final reference in references) { + if (reference == part) { + final gestureRecognizer = TapGestureRecognizer()..onTap = () => onReferenceClicked(reference); + + children.add( + TextSpan( + text: part, + style: style.copyWith( + decoration: TextDecoration.underline, + decorationThickness: 2, + ), + recognizer: gestureRecognizer, + ), + ); + match = true; + break; + } + } + + if (!match) { + children.add( + TextSpan( + style: style, + text: part, + ), + ); + } + } + + return TextSpan( + style: style, + children: children, + ); +} + +/// Renders a rich object [parameter] to be interactive. +InlineSpan buildRichObjectParameter({ + required core.RichObjectParameter parameter, + required TextStyle? textStyle, + required bool isPreview, +}) { + if (isPreview) { + return TextSpan( + text: parameter.name, + style: textStyle, + ); + } + + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: switch (parameter.type) { + core.RichObjectParameter_Type.user || + core.RichObjectParameter_Type.call || + core.RichObjectParameter_Type.guest || + core.RichObjectParameter_Type.userGroup => + NeonRichObjectMention( + parameter: parameter, + textStyle: textStyle, + ), + core.RichObjectParameter_Type.file => NeonRichObjectFile( + parameter: parameter, + textStyle: textStyle, + ), + core.RichObjectParameter_Type.deckCard => NeonRichObjectDeckCard( + parameter: parameter, + ), + _ => NeonRichObjectFallback( + parameter: parameter, + textStyle: textStyle, + ), + }, + ); +} diff --git a/packages/neon_framework/lib/widgets.dart b/packages/neon_framework/lib/widgets.dart index 7813ed5e50d..ba4913ca3a8 100644 --- a/packages/neon_framework/lib/widgets.dart +++ b/packages/neon_framework/lib/widgets.dart @@ -7,6 +7,7 @@ export 'package:neon_framework/src/widgets/image.dart' hide NeonImage; export 'package:neon_framework/src/widgets/linear_progress_indicator.dart'; export 'package:neon_framework/src/widgets/list_view.dart'; export 'package:neon_framework/src/widgets/relative_time.dart'; +export 'package:neon_framework/src/widgets/rich_text/rich_text.dart'; export 'package:neon_framework/src/widgets/server_icon.dart'; export 'package:neon_framework/src/widgets/user_avatar.dart' hide NeonUserStatusIndicator; export 'package:neon_framework/src/widgets/user_status_icon.dart'; diff --git a/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart b/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart index 92a84622f39..ebfa7fba949 100644 --- a/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart +++ b/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart @@ -1,3 +1,4 @@ +import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:intersperse/intersperse.dart'; import 'package:neon_framework/models.dart'; @@ -21,6 +22,34 @@ class NotificationsNotification extends StatelessWidget { @override Widget build(BuildContext context) { + final subject = notification.subjectRichParameters!.isNotEmpty + ? Text.rich( + buildRichTextSpan( + text: notification.subjectRich!, + parameters: notification.subjectRichParameters!, + references: BuiltList(), + style: Theme.of(context).textTheme.bodyLarge!, + onReferenceClicked: (_) {}, + ), + ) + : Text(notification.subject); + + final message = notification.messageRichParameters!.isNotEmpty + ? Text.rich( + buildRichTextSpan( + text: notification.messageRich!, + parameters: notification.messageRichParameters!, + references: BuiltList(), + style: Theme.of(context).textTheme.bodyMedium!, + onReferenceClicked: (_) {}, + ), + overflow: TextOverflow.ellipsis, + ) + : Text( + notification.message, + overflow: TextOverflow.ellipsis, + ); + return Dismissible( key: Key(notification.notificationId.toString()), direction: DismissDirection.startToEnd, @@ -30,15 +59,11 @@ class NotificationsNotification extends StatelessWidget { onDelete(); }, child: ListTile( - title: Text(notification.subject), + title: subject, subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (notification.message.isNotEmpty) - Text( - notification.message, - overflow: TextOverflow.ellipsis, - ), + if (notification.message.isNotEmpty) message, RelativeTime( date: tz.TZDateTime.parse(tz.UTC, notification.datetime), ), diff --git a/packages/neon_framework/packages/notifications_app/pubspec.yaml b/packages/neon_framework/packages/notifications_app/pubspec.yaml index 231ec8b1a14..ab654188b73 100644 --- a/packages/neon_framework/packages/notifications_app/pubspec.yaml +++ b/packages/neon_framework/packages/notifications_app/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: dev_dependencies: build_runner: ^2.4.12 + built_value: ^8.9.2 custom_lint: ^0.6.5 flutter_test: sdk: flutter diff --git a/packages/neon_framework/packages/notifications_app/test/goldens/notification.png b/packages/neon_framework/packages/notifications_app/test/goldens/notification_plain.png similarity index 100% rename from packages/neon_framework/packages/notifications_app/test/goldens/notification.png rename to packages/neon_framework/packages/notifications_app/test/goldens/notification_plain.png diff --git a/packages/neon_framework/packages/notifications_app/test/goldens/notification_rich.png b/packages/neon_framework/packages/notifications_app/test/goldens/notification_rich.png new file mode 100644 index 00000000000..50d62a45cc3 Binary files /dev/null and b/packages/neon_framework/packages/notifications_app/test/goldens/notification_rich.png differ diff --git a/packages/neon_framework/packages/notifications_app/test/main_page_test.dart b/packages/neon_framework/packages/notifications_app/test/main_page_test.dart index 5595b4f77fb..0f87a2e82e1 100644 --- a/packages/neon_framework/packages/notifications_app/test/main_page_test.dart +++ b/packages/neon_framework/packages/notifications_app/test/main_page_test.dart @@ -96,7 +96,9 @@ void main() { when(() => notification.notificationId).thenReturn(i); when(() => notification.app).thenReturn('app'); when(() => notification.subject).thenReturn('subject'); + when(() => notification.subjectRichParameters).thenReturn(BuiltMap()); when(() => notification.message).thenReturn('message'); + when(() => notification.messageRichParameters).thenReturn(BuiltMap()); when(() => notification.datetime).thenReturn(tz.TZDateTime.now(tz.UTC).toIso8601String()); when(() => notification.actions).thenReturn(BuiltList()); when(() => notification.icon).thenReturn(''); diff --git a/packages/neon_framework/packages/notifications_app/test/notification_test.dart b/packages/neon_framework/packages/notifications_app/test/notification_test.dart index 1881367fb70..a5773f42420 100644 --- a/packages/neon_framework/packages/notifications_app/test/notification_test.dart +++ b/packages/neon_framework/packages/notifications_app/test/notification_test.dart @@ -1,4 +1,5 @@ import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:neon_framework/models.dart'; @@ -45,7 +46,9 @@ void main() { when(() => notification.notificationId).thenReturn(0); when(() => notification.app).thenReturn('app'); when(() => notification.subject).thenReturn('subject'); + when(() => notification.subjectRichParameters).thenReturn(BuiltMap()); when(() => notification.message).thenReturn('message'); + when(() => notification.messageRichParameters).thenReturn(BuiltMap()); when(() => notification.datetime).thenReturn(tz.TZDateTime.now(tz.UTC).toIso8601String()); when(() => notification.actions).thenReturn(BuiltList([primaryAction, secondaryAction])); when(() => notification.icon).thenReturn(''); @@ -56,7 +59,7 @@ void main() { account = MockAccount(); }); - testWidgets('Notification', (tester) async { + testWidgets('Plain', (tester) async { await tester.pumpWidgetWithAccessibility( TestApp( localizationsDelegates: NotificationsLocalizations.localizationsDelegates, @@ -77,7 +80,7 @@ void main() { expect(find.text('subject'), findsOne); expect(find.text('now'), findsOne); expect(find.byType(NotificationsAction), findsExactly(2)); - await expectLater(find.byType(TestApp), matchesGoldenFile('goldens/notification.png')); + await expectLater(find.byType(TestApp), matchesGoldenFile('goldens/notification_plain.png')); await tester.tap(find.byType(NotificationsNotification)); verify(() => urlLauncher.launchUrl('https://cloud.example.com:8443/link', any())).called(1); @@ -86,4 +89,46 @@ void main() { await tester.pumpAndSettle(); verify(callback.call).called(1); }); + + testWidgets('Rich', (tester) async { + when(() => notification.subjectRich).thenReturn('subject {user}'); + when(() => notification.subjectRichParameters).thenReturn( + BuiltMap({ + 'user': BuiltMap({ + 'type': JsonObject('user'), + 'id': JsonObject('user'), + 'name': JsonObject('user'), + }), + }), + ); + when(() => notification.messageRich).thenReturn('message {call}'); + when(() => notification.messageRichParameters).thenReturn( + BuiltMap({ + 'call': BuiltMap({ + 'type': JsonObject('call'), + 'id': JsonObject('call'), + 'name': JsonObject('call'), + 'icon-url': JsonObject('call'), + }), + }), + ); + + await tester.pumpWidgetWithAccessibility( + TestApp( + localizationsDelegates: NotificationsLocalizations.localizationsDelegates, + supportedLocales: NotificationsLocalizations.supportedLocales, + providers: [ + Provider>.value(value: BuiltSet()), + Provider.value(value: account), + ], + child: NotificationsNotification( + notification: notification, + onDelete: callback, + ), + ), + ); + + expect(find.byType(NeonRichObjectMention), findsExactly(2)); + await expectLater(find.byType(TestApp), matchesGoldenFile('goldens/notification_rich.png')); + }); } diff --git a/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart b/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart index 7a617523733..6b2d88c7c07 100644 --- a/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart +++ b/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart @@ -1,5 +1,4 @@ import 'package:built_collection/built_collection.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:intersperse/intersperse.dart'; import 'package:intl/intl.dart'; @@ -8,7 +7,6 @@ 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:nextcloud/core.dart' as core; import 'package:nextcloud/spreed.dart' as spreed; import 'package:talk_app/l10n/localizations.dart'; import 'package:talk_app/src/blocs/message_bloc.dart'; @@ -18,10 +16,6 @@ import 'package:talk_app/src/widgets/actor_avatar.dart'; import 'package:talk_app/src/widgets/reactions.dart'; import 'package:talk_app/src/widgets/read_indicator.dart'; import 'package:talk_app/src/widgets/reference_preview.dart'; -import 'package:talk_app/src/widgets/rich_object/deck_card.dart'; -import 'package:talk_app/src/widgets/rich_object/fallback.dart'; -import 'package:talk_app/src/widgets/rich_object/file.dart'; -import 'package:talk_app/src/widgets/rich_object/mention.dart'; import 'package:timezone/timezone.dart' as tz; final _timeFormat = DateFormat.jm(); @@ -43,162 +37,6 @@ String getActorDisplayName(TalkLocalizations localizations, spreed.$ChatMessageI return actorDisplayName; } -/// Renders the [chatMessage] as a rich [TextSpan]. -TextSpan buildChatMessage({ - required spreed.$ChatMessageInterface chatMessage, - required BuiltList references, - required TextStyle style, - required void Function(String reference) onReferenceClicked, - bool isPreview = false, -}) { - var message = chatMessage.message; - if (isPreview) { - message = message.replaceAll('\n', ' '); - } - - final unusedParameters = {}; - - var parts = [message]; - for (final entry in chatMessage.messageParameters.entries) { - final newParts = []; - - var found = false; - for (final part in parts) { - final p = part.split('{${entry.key}}'); - newParts.addAll(p.intersperse('{${entry.key}}')); - if (p.length > 1) { - found = true; - } - } - - if (!found) { - unusedParameters[entry.key] = core.RichObjectParameter.fromJson( - entry.value.map((key, value) => MapEntry(key, value.toString())).toMap(), - ); - } - - parts = newParts; - } - for (final reference in references) { - final newParts = []; - - for (final part in parts) { - final p = part.split(reference); - newParts.addAll(p.intersperse(reference)); - } - - parts = newParts; - } - - final children = []; - - for (final entry in unusedParameters.entries) { - if (entry.key == 'actor' || entry.key == 'user') { - continue; - } - - children - ..add( - buildRichObjectParameter( - parameter: entry.value, - textStyle: style, - isPreview: isPreview, - ), - ) - ..add(const TextSpan(text: '\n')); - } - - for (final part in parts) { - var match = false; - for (final entry in chatMessage.messageParameters.entries) { - if ('{${entry.key}}' == part) { - children.add( - buildRichObjectParameter( - parameter: core.RichObjectParameter.fromJson( - entry.value.map((key, value) => MapEntry(key, value.toString())).toMap(), - ), - textStyle: style, - isPreview: isPreview, - ), - ); - match = true; - break; - } - } - for (final reference in references) { - if (reference == part) { - final gestureRecognizer = TapGestureRecognizer()..onTap = () => onReferenceClicked(reference); - - children.add( - TextSpan( - text: part, - style: style.copyWith( - decoration: TextDecoration.underline, - decorationThickness: 2, - ), - recognizer: gestureRecognizer, - ), - ); - match = true; - break; - } - } - - if (!match) { - children.add( - TextSpan( - style: style, - text: part, - ), - ); - } - } - - return TextSpan( - style: style, - children: children, - ); -} - -/// Renders a rich object [parameter] to be interactive. -InlineSpan buildRichObjectParameter({ - required core.RichObjectParameter parameter, - required TextStyle? textStyle, - required bool isPreview, -}) { - if (isPreview) { - return TextSpan( - text: parameter.name, - style: textStyle, - ); - } - - return WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: switch (parameter.type) { - core.RichObjectParameter_Type.user || - core.RichObjectParameter_Type.call || - core.RichObjectParameter_Type.guest || - core.RichObjectParameter_Type.userGroup => - TalkRichObjectMention( - parameter: parameter, - textStyle: textStyle, - ), - core.RichObjectParameter_Type.file => TalkRichObjectFile( - parameter: parameter, - textStyle: textStyle, - ), - core.RichObjectParameter_Type.deckCard => TalkRichObjectDeckCard( - parameter: parameter, - ), - _ => TalkRichObjectFallback( - parameter: parameter, - textStyle: textStyle, - ), - }, - ); -} - /// Displays a preview of the [chatMessage] including the display name of the sender. class TalkMessagePreview extends StatelessWidget { /// Creates a new Talk message preview. @@ -244,8 +82,9 @@ class TalkMessagePreview extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - buildChatMessage( - chatMessage: chatMessage, + buildRichTextSpan( + text: chatMessage.message, + parameters: chatMessage.messageParameters, references: BuiltList(), isPreview: true, style: Theme.of(context).textTheme.bodyMedium!, @@ -334,8 +173,9 @@ class TalkSystemMessage extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: groupMessages ? 2.5 : 10), child: Center( child: RichText( - text: buildChatMessage( - chatMessage: chatMessage, + text: buildRichTextSpan( + text: chatMessage.message, + parameters: chatMessage.messageParameters, references: BuiltList(), style: Theme.of(context).textTheme.labelSmall!, onReferenceClicked: (url) async => launchUrl(NeonProvider.of(context), url), @@ -536,8 +376,9 @@ class _TalkCommentMessageState extends State { } Widget text = Text.rich( - buildChatMessage( - chatMessage: widget.chatMessage, + buildRichTextSpan( + text: widget.chatMessage.message, + parameters: widget.chatMessage.messageParameters, isPreview: widget.isParent, references: references.keys.toBuiltList(), style: textTheme.bodyLarge!.copyWith( diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_file_without_preview.png b/packages/neon_framework/packages/talk_app/test/goldens/rich_object_file_without_preview.png deleted file mode 100644 index d3fc2f32fe8..00000000000 Binary files a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_file_without_preview.png and /dev/null differ diff --git a/packages/neon_framework/packages/talk_app/test/message_test.dart b/packages/neon_framework/packages/talk_app/test/message_test.dart index 4652415cd3f..dc618c20e3b 100644 --- a/packages/neon_framework/packages/talk_app/test/message_test.dart +++ b/packages/neon_framework/packages/talk_app/test/message_test.dart @@ -1,5 +1,4 @@ import 'package:built_collection/built_collection.dart'; -import 'package:built_value/json_object.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -23,19 +22,11 @@ import 'package:talk_app/src/widgets/message.dart'; import 'package:talk_app/src/widgets/reactions.dart'; import 'package:talk_app/src/widgets/read_indicator.dart'; import 'package:talk_app/src/widgets/reference_preview.dart'; -import 'package:talk_app/src/widgets/rich_object/deck_card.dart'; -import 'package:talk_app/src/widgets/rich_object/fallback.dart'; -import 'package:talk_app/src/widgets/rich_object/file.dart'; -import 'package:talk_app/src/widgets/rich_object/mention.dart'; import 'package:timezone/data/latest.dart' as tzdata; import 'package:timezone/timezone.dart' as tz; import 'testing.dart'; -class MockOnReferenceClickedCallback extends Mock { - void call(String reference); -} - Widget wrapWidget({ required Widget child, List providers = const [], @@ -1515,270 +1506,4 @@ void main() { }); }); }); - - group('buildRichObjectParameter', () { - for (final isPreview in [true, false]) { - group(isPreview ? 'As preview' : 'Complete', () { - group('Mention', () { - for (final type in [ - core.RichObjectParameter_Type.user, - core.RichObjectParameter_Type.call, - core.RichObjectParameter_Type.guest, - core.RichObjectParameter_Type.userGroup, - ]) { - testWidgets(type.value, (tester) async { - final userDetails = MockUserDetails(); - when(() => userDetails.groups).thenReturn(BuiltList()); - - final userDetailsBloc = MockUserDetailsBloc(); - when(() => userDetailsBloc.userDetails) - .thenAnswer((_) => BehaviorSubject.seeded(Result.success(userDetails))); - - final account = MockAccount(); - - await tester.pumpWidgetWithAccessibility( - TestApp( - providers: [ - Provider.value(value: account), - NeonProvider.value(value: userDetailsBloc), - ], - child: RichText( - text: buildRichObjectParameter( - parameter: core.RichObjectParameter( - (b) => b - ..type = type - ..id = '' - ..name = 'name' - ..iconUrl = '', - ), - textStyle: null, - isPreview: isPreview, - ), - ), - ), - ); - - expect(find.byType(TalkRichObjectMention), isPreview ? findsNothing : findsOne); - expect(find.text('name', findRichText: true), findsOne); - }); - } - }); - - testWidgets('File', (tester) async { - final account = MockAccount(); - - await tester.pumpWidgetWithAccessibility( - TestApp( - providers: [ - Provider.value(value: account), - ], - child: RichText( - text: buildRichObjectParameter( - parameter: core.RichObjectParameter( - (b) => b - ..type = core.RichObjectParameter_Type.file - ..id = '0' - ..name = 'name', - ), - textStyle: null, - isPreview: isPreview, - ), - ), - ), - ); - - expect(find.byType(TalkRichObjectFile), isPreview ? findsNothing : findsOne); - expect(find.text('name', findRichText: true), findsOne); - }); - - testWidgets('Deck card', (tester) async { - await tester.pumpWidgetWithAccessibility( - TestApp( - child: RichText( - text: buildRichObjectParameter( - parameter: core.RichObjectParameter( - (b) => b - ..type = core.RichObjectParameter_Type.deckCard - ..id = '' - ..name = 'name' - ..boardname = 'boardname' - ..stackname = 'stackname', - ), - textStyle: null, - isPreview: isPreview, - ), - ), - ), - ); - - expect(find.byType(TalkRichObjectDeckCard), isPreview ? findsNothing : findsOne); - expect(find.text('name', findRichText: true), findsOne); - }); - - testWidgets('Fallback', (tester) async { - await tester.pumpWidgetWithAccessibility( - TestApp( - child: RichText( - text: buildRichObjectParameter( - parameter: core.RichObjectParameter( - (b) => b - ..type = core.RichObjectParameter_Type.addressbook - ..id = '' - ..name = 'name', - ), - textStyle: null, - isPreview: isPreview, - ), - ), - ), - ); - - expect(find.byType(TalkRichObjectFallback), isPreview ? findsNothing : findsOne); - expect(find.text('name', findRichText: true), findsOne); - }); - }); - } - }); - - group('buildChatMessage', () { - test('Preview without newlines', () { - final chatMessage = MockChatMessage(); - when(() => chatMessage.message).thenReturn('123\n456'); - when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); - - var span = buildChatMessage( - chatMessage: chatMessage, - references: BuiltList(), - style: const TextStyle(), - onReferenceClicked: (_) {}, - ).children!.single as TextSpan; - expect(span.text, '123\n456'); - - span = buildChatMessage( - chatMessage: chatMessage, - references: BuiltList(), - style: const TextStyle(), - onReferenceClicked: (_) {}, - isPreview: true, - ).children!.single as TextSpan; - expect(span.text, '123 456'); - }); - - group('Unused parameters', () { - group('Discard', () { - for (final type in ['actor', 'user']) { - test(type, () { - final chatMessage = MockChatMessage(); - when(() => chatMessage.message).thenReturn('test'); - when(() => chatMessage.messageParameters).thenReturn( - BuiltMap({ - type: BuiltMap({ - 'type': JsonObject('user'), - 'id': JsonObject(''), - 'name': JsonObject(''), - }), - }), - ); - - final spans = buildChatMessage( - chatMessage: chatMessage, - references: BuiltList(), - style: const TextStyle(), - onReferenceClicked: (_) {}, - ).children!; - expect((spans.single as TextSpan).text, 'test'); - }); - } - }); - - test('Display', () { - final chatMessage = MockChatMessage(); - when(() => chatMessage.message).thenReturn('test'); - when(() => chatMessage.messageParameters).thenReturn( - BuiltMap({ - 'file': BuiltMap({ - 'type': JsonObject('file'), - 'id': JsonObject(''), - 'name': JsonObject(''), - }), - }), - ); - - final spans = buildChatMessage( - chatMessage: chatMessage, - references: BuiltList(), - style: const TextStyle(), - onReferenceClicked: (_) {}, - ).children!; - expect(spans, hasLength(3)); - expect((spans[0] as WidgetSpan).child, isA()); - expect((spans[1] as TextSpan).text, '\n'); - expect((spans[2] as TextSpan).text, 'test'); - }); - }); - - test('Used parameters', () { - final chatMessage = MockChatMessage(); - when(() => chatMessage.message).thenReturn('123 {actor1} 456 {actor2} 789'); - when(() => chatMessage.messageParameters).thenReturn( - BuiltMap({ - 'actor1': BuiltMap({ - 'type': JsonObject('user'), - 'id': JsonObject(''), - 'name': JsonObject(''), - }), - 'actor2': BuiltMap({ - 'type': JsonObject('user'), - 'id': JsonObject(''), - 'name': JsonObject(''), - }), - }), - ); - - final spans = buildChatMessage( - chatMessage: chatMessage, - references: BuiltList(), - style: const TextStyle(), - onReferenceClicked: (_) {}, - ).children!; - expect(spans, hasLength(5)); - expect((spans[0] as TextSpan).text, '123 '); - expect((spans[1] as WidgetSpan).child, isA()); - expect((spans[2] as TextSpan).text, ' 456 '); - expect((spans[3] as WidgetSpan).child, isA()); - expect((spans[4] as TextSpan).text, ' 789'); - }); - - test('References', () { - final chatMessage = MockChatMessage(); - when(() => chatMessage.message).thenReturn('a 123 b 456 c'); - when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); - - final callback = MockOnReferenceClickedCallback(); - - final spans = buildChatMessage( - chatMessage: chatMessage, - references: BuiltList(['123', '456']), - style: const TextStyle(), - onReferenceClicked: callback.call, - ).children!; - expect(spans, hasLength(5)); - - expect((spans[0] as TextSpan).text, 'a '); - - final link1 = spans[1] as TextSpan; - expect(link1.text, '123'); - (link1.recognizer! as TapGestureRecognizer).onTap!(); - verify(() => callback('123')).called(1); - - expect((spans[2] as TextSpan).text, ' b '); - - final link2 = spans[3] as TextSpan; - expect(link2.text, '456'); - (link2.recognizer! as TapGestureRecognizer).onTap!(); - verify(() => callback('456')).called(1); - - expect((spans[4] as TextSpan).text, ' c'); - }); - }); } diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_deck_card.png b/packages/neon_framework/test/goldens/rich_text_object_deck_card.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_deck_card.png rename to packages/neon_framework/test/goldens/rich_text_object_deck_card.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_fallback_with_icon.png b/packages/neon_framework/test/goldens/rich_text_object_fallback_with_icon.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_fallback_with_icon.png rename to packages/neon_framework/test/goldens/rich_text_object_fallback_with_icon.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_fallback_without_icon.png b/packages/neon_framework/test/goldens/rich_text_object_fallback_without_icon.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_fallback_without_icon.png rename to packages/neon_framework/test/goldens/rich_text_object_fallback_without_icon.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_call.png b/packages/neon_framework/test/goldens/rich_text_object_mention_call.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_call.png rename to packages/neon_framework/test/goldens/rich_text_object_mention_call.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_guest.png b/packages/neon_framework/test/goldens/rich_text_object_mention_guest.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_guest.png rename to packages/neon_framework/test/goldens/rich_text_object_mention_guest.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user-group_highlight.png b/packages/neon_framework/test/goldens/rich_text_object_mention_user-group_highlight.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user-group_highlight.png rename to packages/neon_framework/test/goldens/rich_text_object_mention_user-group_highlight.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user-group_other.png b/packages/neon_framework/test/goldens/rich_text_object_mention_user-group_other.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user-group_other.png rename to packages/neon_framework/test/goldens/rich_text_object_mention_user-group_other.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user_highlight.png b/packages/neon_framework/test/goldens/rich_text_object_mention_user_highlight.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user_highlight.png rename to packages/neon_framework/test/goldens/rich_text_object_mention_user_highlight.png diff --git a/packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user_other.png b/packages/neon_framework/test/goldens/rich_text_object_mention_user_other.png similarity index 100% rename from packages/neon_framework/packages/talk_app/test/goldens/rich_object_mention_user_other.png rename to packages/neon_framework/test/goldens/rich_text_object_mention_user_other.png diff --git a/packages/neon_framework/packages/talk_app/test/rich_object_test.dart b/packages/neon_framework/test/rich_text_test.dart similarity index 55% rename from packages/neon_framework/packages/talk_app/test/rich_object_test.dart rename to packages/neon_framework/test/rich_text_test.dart index 94cb7d15885..a4936449855 100644 --- a/packages/neon_framework/packages/talk_app/test/rich_object_test.dart +++ b/packages/neon_framework/test/rich_text_test.dart @@ -1,4 +1,6 @@ import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -11,12 +13,12 @@ import 'package:neon_framework/widgets.dart'; import 'package:nextcloud/core.dart' as core; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:talk_app/src/widgets/rich_object/deck_card.dart'; -import 'package:talk_app/src/widgets/rich_object/fallback.dart'; -import 'package:talk_app/src/widgets/rich_object/file.dart'; -import 'package:talk_app/src/widgets/rich_object/mention.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +class MockOnReferenceClickedCallback extends Mock { + void call(String reference); +} + void main() { late MockUrlLauncher urlLauncher; late Account account; @@ -43,7 +45,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectDeckCard( + child: NeonRichObjectDeckCard( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.deckCard @@ -59,11 +61,11 @@ void main() { expect(find.text('name'), findsOne); expect(find.text('boardname: stackname'), findsOne); await expectLater( - find.byType(TalkRichObjectDeckCard), - matchesGoldenFile('goldens/rich_object_deck_card.png'), + find.byType(NeonRichObjectDeckCard), + matchesGoldenFile('goldens/rich_text_object_deck_card.png'), ); - await tester.tap(find.byType(TalkRichObjectDeckCard)); + await tester.tap(find.byType(NeonRichObjectDeckCard)); verify(() => urlLauncher.launchUrl('https://cloud.example.com:8443/link', any())).called(1); }); @@ -75,7 +77,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectMention( + child: NeonRichObjectMention( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.user @@ -89,8 +91,8 @@ void main() { expect(find.byType(NeonUserAvatar), findsOne); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectMention), - matchesGoldenFile('goldens/rich_object_mention_user_highlight.png'), + find.byType(NeonRichObjectMention), + matchesGoldenFile('goldens/rich_text_object_mention_user_highlight.png'), ); }); @@ -100,7 +102,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectMention( + child: NeonRichObjectMention( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.user @@ -114,8 +116,8 @@ void main() { expect(find.byType(NeonUserAvatar), findsOne); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectMention), - matchesGoldenFile('goldens/rich_object_mention_user_other.png'), + find.byType(NeonRichObjectMention), + matchesGoldenFile('goldens/rich_text_object_mention_user_other.png'), ); }); }); @@ -126,7 +128,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectMention( + child: NeonRichObjectMention( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.call @@ -141,15 +143,15 @@ void main() { expect(find.byType(NeonUriImage), findsOne); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectMention), - matchesGoldenFile('goldens/rich_object_mention_call.png'), + find.byType(NeonRichObjectMention), + matchesGoldenFile('goldens/rich_text_object_mention_call.png'), ); }); testWidgets('guest', (tester) async { await tester.pumpWidgetWithAccessibility( TestApp( - child: TalkRichObjectMention( + child: NeonRichObjectMention( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.guest @@ -163,8 +165,8 @@ void main() { expect(find.byIcon(AdaptiveIcons.person), findsOne); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectMention), - matchesGoldenFile('goldens/rich_object_mention_guest.png'), + find.byType(NeonRichObjectMention), + matchesGoldenFile('goldens/rich_text_object_mention_guest.png'), ); }); @@ -180,7 +182,7 @@ void main() { providers: [ NeonProvider.value(value: userDetailsBloc), ], - child: TalkRichObjectMention( + child: NeonRichObjectMention( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.userGroup @@ -194,8 +196,8 @@ void main() { expect(find.byIcon(AdaptiveIcons.group), findsOne); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectMention), - matchesGoldenFile('goldens/rich_object_mention_user-group_highlight.png'), + find.byType(NeonRichObjectMention), + matchesGoldenFile('goldens/rich_text_object_mention_user-group_highlight.png'), ); await tester.pumpWidgetWithAccessibility( @@ -203,7 +205,7 @@ void main() { providers: [ NeonProvider.value(value: userDetailsBloc), ], - child: TalkRichObjectMention( + child: NeonRichObjectMention( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.userGroup @@ -217,8 +219,8 @@ void main() { expect(find.byIcon(AdaptiveIcons.group), findsOne); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectMention), - matchesGoldenFile('goldens/rich_object_mention_user-group_other.png'), + find.byType(NeonRichObjectMention), + matchesGoldenFile('goldens/rich_text_object_mention_user-group_other.png'), ); }); }); @@ -234,7 +236,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFile( + child: NeonRichObjectFile( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.file @@ -249,7 +251,7 @@ void main() { ), ); - await tester.tap(find.byType(TalkRichObjectFile)); + await tester.tap(find.byType(NeonRichObjectFile)); verify(() => urlLauncher.launchUrl('https://cloud.example.com:8443/link', any())).called(1); }); @@ -259,7 +261,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFile( + child: NeonRichObjectFile( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.file @@ -283,7 +285,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFile( + child: NeonRichObjectFile( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.file @@ -319,7 +321,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFile( + child: NeonRichObjectFile( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.file @@ -352,7 +354,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFile( + child: NeonRichObjectFile( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.file @@ -386,7 +388,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFile( + child: NeonRichObjectFile( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.file @@ -419,7 +421,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFallback( + child: NeonRichObjectFallback( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.calendarEvent @@ -432,14 +434,14 @@ void main() { ), ); - await tester.tap(find.byType(TalkRichObjectFallback)); + await tester.tap(find.byType(NeonRichObjectFallback)); verify(() => urlLauncher.launchUrl('https://cloud.example.com:8443/link', any())).called(1); }); testWidgets('Without icon', (tester) async { await tester.pumpWidgetWithAccessibility( TestApp( - child: TalkRichObjectFallback( + child: NeonRichObjectFallback( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.addressbook @@ -453,8 +455,8 @@ void main() { expect(find.byType(NeonUriImage), findsNothing); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectFallback), - matchesGoldenFile('goldens/rich_object_fallback_without_icon.png'), + find.byType(NeonRichObjectFallback), + matchesGoldenFile('goldens/rich_text_object_fallback_without_icon.png'), ); }); @@ -464,7 +466,7 @@ void main() { providers: [ Provider.value(value: account), ], - child: TalkRichObjectFallback( + child: NeonRichObjectFallback( parameter: core.RichObjectParameter( (b) => b ..type = core.RichObjectParameter_Type.addressbook @@ -479,9 +481,258 @@ void main() { expect(find.byType(NeonUriImage), findsOne); expect(find.text('name'), findsOne); await expectLater( - find.byType(TalkRichObjectFallback), - matchesGoldenFile('goldens/rich_object_fallback_with_icon.png'), + find.byType(NeonRichObjectFallback), + matchesGoldenFile('goldens/rich_text_object_fallback_with_icon.png'), ); }); }); + + group('buildRichObjectParameter', () { + for (final isPreview in [true, false]) { + group(isPreview ? 'As preview' : 'Complete', () { + group('Mention', () { + for (final type in [ + core.RichObjectParameter_Type.user, + core.RichObjectParameter_Type.call, + core.RichObjectParameter_Type.guest, + core.RichObjectParameter_Type.userGroup, + ]) { + testWidgets(type.value, (tester) async { + final userDetails = MockUserDetails(); + when(() => userDetails.groups).thenReturn(BuiltList()); + + final userDetailsBloc = MockUserDetailsBloc(); + when(() => userDetailsBloc.userDetails) + .thenAnswer((_) => BehaviorSubject.seeded(Result.success(userDetails))); + + final account = MockAccount(); + + await tester.pumpWidgetWithAccessibility( + TestApp( + providers: [ + Provider.value(value: account), + NeonProvider.value(value: userDetailsBloc), + ], + child: RichText( + text: buildRichObjectParameter( + parameter: core.RichObjectParameter( + (b) => b + ..type = type + ..id = '' + ..name = 'name' + ..iconUrl = '', + ), + textStyle: null, + isPreview: isPreview, + ), + ), + ), + ); + + expect(find.byType(NeonRichObjectMention), isPreview ? findsNothing : findsOne); + expect(find.text('name', findRichText: true), findsOne); + }); + } + }); + + testWidgets('File', (tester) async { + final account = MockAccount(); + + await tester.pumpWidgetWithAccessibility( + TestApp( + providers: [ + Provider.value(value: account), + ], + child: RichText( + text: buildRichObjectParameter( + parameter: core.RichObjectParameter( + (b) => b + ..type = core.RichObjectParameter_Type.file + ..id = '0' + ..name = 'name', + ), + textStyle: null, + isPreview: isPreview, + ), + ), + ), + ); + + expect(find.byType(NeonRichObjectFile), isPreview ? findsNothing : findsOne); + expect(find.text('name', findRichText: true), findsOne); + }); + + testWidgets('Deck card', (tester) async { + await tester.pumpWidgetWithAccessibility( + TestApp( + child: RichText( + text: buildRichObjectParameter( + parameter: core.RichObjectParameter( + (b) => b + ..type = core.RichObjectParameter_Type.deckCard + ..id = '' + ..name = 'name' + ..boardname = 'boardname' + ..stackname = 'stackname', + ), + textStyle: null, + isPreview: isPreview, + ), + ), + ), + ); + + expect(find.byType(NeonRichObjectDeckCard), isPreview ? findsNothing : findsOne); + expect(find.text('name', findRichText: true), findsOne); + }); + + testWidgets('Fallback', (tester) async { + await tester.pumpWidgetWithAccessibility( + TestApp( + child: RichText( + text: buildRichObjectParameter( + parameter: core.RichObjectParameter( + (b) => b + ..type = core.RichObjectParameter_Type.addressbook + ..id = '' + ..name = 'name', + ), + textStyle: null, + isPreview: isPreview, + ), + ), + ), + ); + + expect(find.byType(NeonRichObjectFallback), isPreview ? findsNothing : findsOne); + expect(find.text('name', findRichText: true), findsOne); + }); + }); + } + }); + + group('buildRichTextSpan', () { + test('Preview without newlines', () { + var span = buildRichTextSpan( + text: '123\n456', + parameters: BuiltMap(), + references: BuiltList(), + style: const TextStyle(), + onReferenceClicked: (_) {}, + ).children!.single as TextSpan; + expect(span.text, '123\n456'); + + span = buildRichTextSpan( + text: '123\n456', + parameters: BuiltMap(), + references: BuiltList(), + style: const TextStyle(), + onReferenceClicked: (_) {}, + isPreview: true, + ).children!.single as TextSpan; + expect(span.text, '123 456'); + }); + + group('Unused parameters', () { + for (final type in core.RichObjectParameter_Type.values) { + test(type, () { + final spans = buildRichTextSpan( + text: 'test', + parameters: BuiltMap({ + type.value: BuiltMap({ + 'type': JsonObject(type.value), + 'id': JsonObject(1), + 'name': JsonObject(''), + }), + }), + references: BuiltList(), + style: const TextStyle(), + onReferenceClicked: (_) {}, + ).children!; + if (type == core.RichObjectParameter_Type.file) { + expect(spans, hasLength(3)); + expect((spans[0] as WidgetSpan).child, isA()); + expect((spans[1] as TextSpan).text, '\n'); + expect((spans[2] as TextSpan).text, 'test'); + } else { + expect((spans.single as TextSpan).text, 'test'); + } + }); + } + }); + + test('Used parameters', () { + final spans = buildRichTextSpan( + text: '123 {actor1} 456 {actor2} 789', + parameters: BuiltMap({ + 'actor1': BuiltMap({ + 'type': JsonObject('user'), + 'id': JsonObject(''), + 'name': JsonObject(''), + }), + 'actor2': BuiltMap({ + 'type': JsonObject('user'), + 'id': JsonObject(''), + 'name': JsonObject(''), + }), + }), + references: BuiltList(), + style: const TextStyle(), + onReferenceClicked: (_) {}, + ).children!; + expect(spans, hasLength(5)); + expect((spans[0] as TextSpan).text, '123 '); + expect((spans[1] as WidgetSpan).child, isA()); + expect((spans[2] as TextSpan).text, ' 456 '); + expect((spans[3] as WidgetSpan).child, isA()); + expect((spans[4] as TextSpan).text, ' 789'); + }); + + test('References', () { + final callback = MockOnReferenceClickedCallback(); + + final spans = buildRichTextSpan( + text: 'a 123 b 456 c', + parameters: BuiltMap(), + references: BuiltList(['123', '456']), + style: const TextStyle(), + onReferenceClicked: callback.call, + ).children!; + expect(spans, hasLength(5)); + + expect((spans[0] as TextSpan).text, 'a '); + + final link1 = spans[1] as TextSpan; + expect(link1.text, '123'); + (link1.recognizer! as TapGestureRecognizer).onTap!(); + verify(() => callback('123')).called(1); + + expect((spans[2] as TextSpan).text, ' b '); + + final link2 = spans[3] as TextSpan; + expect(link2.text, '456'); + (link2.recognizer! as TapGestureRecognizer).onTap!(); + verify(() => callback('456')).called(1); + + expect((spans[4] as TextSpan).text, ' c'); + }); + + test('Skip empty parts', () { + final spans = buildRichTextSpan( + text: '{actor}', + parameters: BuiltMap({ + 'actor': BuiltMap({ + 'type': JsonObject(core.RichObjectParameter_Type.user.name), + 'id': JsonObject(''), + 'name': JsonObject(''), + }), + }), + references: BuiltList(), + style: const TextStyle(), + onReferenceClicked: (_) {}, + ).children!; + expect(spans, hasLength(1)); + expect((spans[0] as WidgetSpan).child, isA()); + }); + }); }