diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 79619ade41..0630fa0c80 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -12,6 +12,7 @@ 🐞 Fixed +- Fixed link preview enrichment failing for uppercase URL schemes (e.g. `HTTPS://`) by normalizing the scheme before enriching. - Fixed last-message preview flicker during channel-state reloads. - Fixed shadowed messages not hidden in channel list items. - Fixed `StreamMessageListView` firing `markThreadRead` on a reply-less parent, which produced a guaranteed 404 every time the thread view was opened before the first reply. diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart index 961df67283..2b58cd8ba2 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer.dart @@ -1293,20 +1293,23 @@ class DefaultStreamMessageComposerState extends State RecordPlatform.instance = FakeRecordPlatform()); + tearDown(() => RecordPlatform.instance = originalRecordPlatform); + + late MockClient client; + late MockClientState clientState; + late MockChannel channel; + late MockChannelState channelState; + + setUp(() { + registerFallbackValue(Message()); + + client = MockClient(); + clientState = MockClientState(); + channel = MockChannel( + ownCapabilities: const [ + ChannelCapability.sendMessage, + ChannelCapability.sendLinks, + ], + ); + channelState = MockChannelState(); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUserStream).thenAnswer( + (_) => Stream.value(OwnUser(id: 'user-id')), + ); + + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(channel.getRemainingCooldown).thenReturn(0); + + when(() => client.enrichUrl(any())).thenAnswer( + (invocation) async => OGAttachmentResponse()..ogScrapeUrl = invocation.positionalArguments.first as String, + ); + }); + + Future enrichUrlFrom(WidgetTester tester, String text) async { + // Enrichment runs behind a real-clock debounce, so drive the flow with + // real timers via runAsync. + await tester.runAsync(() async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + channel: channel, + child: Scaffold(body: StreamMessageComposer()), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), text); + await Future.delayed(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + }); + + return verify(() => client.enrichUrl(captureAny())).captured.single; + } + + // The scheme (and host) are normalized to lowercase before enriching so a + // backend that only handles lowercase schemes receives a consistent url. + // Case-sensitive path and query parts must be preserved as-is. + final cases = { + 'uppercase HTTPS scheme': ('HTTPS://example.com', 'https://example.com'), + 'uppercase HTTP scheme': ('HTTP://example.com', 'http://example.com'), + 'mixed-case scheme': ('HtTpS://example.com', 'https://example.com'), + 'mixed-case host': ('HTTPS://Example.COM', 'https://example.com'), + 'preserves case-sensitive path and query': ( + 'HTTPS://example.com/Path?Q=AbC', + 'https://example.com/Path?Q=AbC', + ), + 'uppercase scheme with www and path': ( + 'HTTPS://www.example.com/foo', + 'https://www.example.com/foo', + ), + 'url embedded in surrounding text': ( + 'look at HTTPS://example.com now', + 'https://example.com', + ), + 'lowercase https scheme unchanged': ( + 'https://example.com', + 'https://example.com', + ), + }; + + for (final entry in cases.entries) { + final (input, expected) = entry.value; + testWidgets('enriches ${entry.key}', (tester) async { + expect(await enrichUrlFrom(tester, input), expected); + }); + } + }); +}