Repository navigation
Conversation
A message's text now links an address such as youtube.com or getstream.io/chat when it ends in a known top-level domain, and opens it over https. File names such as config.json, email addresses and link labels are left as they are. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 59 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3125 +/- ##
==========================================
+ Coverage 80.28% 80.32% +0.04%
==========================================
Files 455 455
Lines 29614 29614
==========================================
+ Hits 23775 23787 +12
+ Misses 5839 5827 -12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| @@ -0,0 +1,289 @@ | |||
| // The patterns and top-level domains below are adapted from | |||
| // androidx.core.util.PatternsCompat (androidx core 1.18.0), Copyright (C) The | |||
| // Android Open Source Project, licensed under the Apache License, Version 2.0. | |||
There was a problem hiding this comment.
Not a blocker. Since this is adapted from AOSP's Apache-2.0 code, should we check with whoever handles licensing whether the header is enough, or whether the license text needs to ship with the package (§4(a))?
There was a problem hiding this comment.
The local port is gone in 6aaf35a: links now come from core (GetStream/stream-core-flutter#205), which has no AOSP code. It's our own scanner, with the top-level domain list generated from IANA's public root zone list.
| /// `getstream.io/chat`, and opens it over https. | ||
| /// | ||
| /// The address must end in a known top-level domain, so file names such as | ||
| /// `config.json` stay plain text. Addresses with a scheme or starting with |
There was a problem hiding this comment.
This holds for config.json, but not for extensions that are also TLDs. README.md, setup.py, main.rs, script.sh, archive.zip, user.id, user.email and ok.so all become https:// links. That's inherent to the TLD approach, and Android's Linkify does the same, so I'm not asking for a fix. But the dartdoc and the PR description promise more than the code does. Maybe: "The address must end in a known top-level domain, so most file names, such as config.json, stay plain text."
There was a problem hiding this comment.
Agreed. The core findLinks dartdoc now says exactly that: config.json stays plain and README.md links, and both are tests (GetStream/stream-core-flutter#205). The local syntax is gone in 6aaf35a.
| // opening bracket, a quote or an emphasis delimiter. Never inside a word, | ||
| // a link label or an address, so `a_b.com` and `[label](url)` stay as they | ||
| // are. | ||
| static const _validPrecedingChars = {'\n', ' ', '\t', '(', '"', "'", '*', '~'}; |
There was a problem hiding this comment.
nit: see [youtube.com] and a domain after a non-breaking space stay plain. Adding [ (the _isInLinkLabel check already covers real link labels) and \u00A0 would catch them.
There was a problem hiding this comment.
Both link now: see [youtube.com] and a domain after a non-breaking space, with tests in core (GetStream/stream-core-flutter#205). The local syntax is gone in 6aaf35a.
| // [url] without the punctuation that ends the sentence around it, as for | ||
| // the markdown autolinks. A closing bracket stays when it closes one in the | ||
| // link. | ||
| String _trimTrailing(String url) { |
There was a problem hiding this comment.
This recounts both brackets and makes a new substring for every character it trims, so it's quadratic in the trailing run. x.com/ followed by 20,000 ) takes 1.49 s to parse, against 11 ms without this syntax, and it runs again on every rebuild of the message. Counting once and walking an end index back gives the same results (youtube.com., (en.wikipedia.org/wiki/Foo_(bar)) and x.com/a)). are all unchanged) and takes 13 ms:
String _trimTrailing(String url) {
final opens = '('.allMatches(url).length;
var closes = ')'.allMatches(url).length;
var end = url.length;
while (end > 0) {
final last = url[end - 1];
final isUnclosedBracket = last == ')' && closes > opens;
if (!_trailingPunctuation.contains(last) && !isUnclosedBracket) break;
if (last == ')') closes--;
end--;
}
return url.substring(0, end);
}There was a problem hiding this comment.
The core finder trims in one backward pass and scans a path once, so 20,000 ) take about 0.09 ms (benchmark in GetStream/stream-core-flutter#205). The local syntax is gone in 6aaf35a.
| int? _matchRest(String source, int hostEnd) => _rest.matchAsPrefix(source, hostEnd)?.end; | ||
|
|
||
| bool _isTopLevelDomain(String label) { | ||
| return _topLevelDomains.contains(label) || _punycodeTld.hasMatch(label); |
There was a problem hiding this comment.
The TLD lookup is case-sensitive, so GOOGLE.COM and Example.Com stay plain while YouTube.com links:
| return _topLevelDomains.contains(label) || _punycodeTld.hasMatch(label); | |
| return _topLevelDomains.contains(label.toLowerCase()) || _punycodeTld.hasMatch(label); |
There was a problem hiding this comment.
Fixed in core: the lookup is case-insensitive, with GOOGLE.COM and Example.Com as tests (GetStream/stream-core-flutter#205). The local syntax is gone in 6aaf35a.
| } | ||
|
|
||
| // Non-ASCII characters allowed in a domain label or path, except spaces. | ||
| const _ucsChar = r'\u00A1-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u2FFF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF'; |
There was a problem hiding this comment.
_ucsChar also covers punctuation (“ ‘ « ¿ ¡ — –, CJK and fullwidth brackets), so a match that starts after a space takes the opening punctuation into the link and the href:
Watch “youtube.com” later→ links“youtube.com, openshttps://%E2%80%9Cyoutube.com«youtube.com»,¿youtube.com?andsee—youtube.combehave the same way.
On iOS this is what a user gets when typing "youtube.com". StreamMessageTextField enables smart quotes and smart dashes by default, so " becomes “ and -- becomes —. Tapping the link fails with "Safari can't open the page because the server can't be found".
Narrowing the label characters and allowing that punctuation before a link fixes it. With these two changes, “youtube.com”, «youtube.com», ¿youtube.com?, see—youtube.com, (youtube.com) and 「youtube.com」 all link just youtube.com. пример.рф, münchen.de and café.fr/menú still link:
static const _validPrecedingChars = {'\n', ' ', '\t', '(', '"', "'", '*', '~', '“', '‘', '«', '¿', '¡', '—', '–', '(', '「'};const _ucsChar = r'\u00C0-\u1FFF\u2070-\u2FFF\u3040-\uD7FF\uF900-\uFDCF\uFDF0-\uFEFF\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFEF';Could you add “youtube.com” and see—youtube.com as tests?
There was a problem hiding this comment.
Fixed in core: a domain label has to start with a letter or digit, so “youtube.com”, see—youtube.com, ¿youtube.com?, (youtube.com) and 「youtube.com」 all link just youtube.com. All five are tests in GetStream/stream-core-flutter#205. The local syntax is gone in 6aaf35a.
| } | ||
|
|
||
| // Non-ASCII characters allowed in a domain label or path, except spaces. | ||
| const _ucsChar = r'\u00A1-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u2FFF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF'; |
There was a problem hiding this comment.
A related case that the change above doesn't fix: Chinese, Japanese and Thai don't put spaces between words, so a domain written straight after them absorbs the preceding text. 请访问google.com了解更多 links 请访问google.com, which is a syntactically valid international domain, and it opens nothing.
Two ways to go: when a label switches from non-ASCII to ASCII, start the match at the ASCII part (that would drop CJK domains like 例子.中国, which I think is the better trade-off), or keep it as is and note the limitation in the PR. Either is fine, as long as it's a deliberate choice.
There was a problem hiding this comment.
Went with starting the match at the ASCII part: 请访问google.com了解更多 links google.com. A CJK domain that starts a word, such as 例子.中国, still links. Both are tests in GetStream/stream-core-flutter#205. The local syntax is gone in 6aaf35a.
* test(repo): assert the token date and signature policy (FLU-1083) The SDK refreshes the token on code 40 only. A token used before its issue date (42) is reconnected as-is until the server's clock catches up, and a bad signature (43) is not retried with a new token. The four tests now assert that instead of a refresh. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(repo): tighten the token date and signature tests (FLU-1083) Keep the invalid window open past the refresh so the refreshed token is always rejected, count token requests through an env assertion, and fix the step and file comments to match what the tests assert. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(repo): drop the token signature after login test (FLU-1083) A signing secret doesn't go bad mid-session, and the test only timed the SDK's reconnect backoff. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(repo): keep the login error across the loading screen (FLU-1083) The chooser page is replaced while connecting, so an error it stored itself was gone by the time the login failed. The auth state now carries it, and the rebuilt page shows it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ui): keep the footer of a sent message before a failed one (FLU-1090) A message that failed now starts its own group, so the sent message before it keeps its timestamp and delivery status. Consecutive failed messages still group together. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(ui): describe every failed message in the changelog (FLU-1090) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pin stream_core_flutter and stream_core to the stream-core-flutter commit that adds the link finder, and drop the local WebUrlSyntax: StreamMessageText from stream_core_flutter now links web addresses itself. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC
flu-1094-a-bare-domain-in-a-message-youtubecom-is-not-turned-into-a # Conflicts: # packages/stream_chat_flutter/CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC
Move stream_core_flutter and stream_core from the link finder PR's branch commit to its squash commit on main (GetStream/stream-core-flutter#205). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC
…re-domain-in-a-message-youtubecom-is-not-turned-into-a # Conflicts: # e2e_app/README.md # e2e_app/fastlane/Fastfile # e2e_app/integration_test/authentication_test.dart # e2e_app/integration_test/giphy_test.dart # e2e_app/integration_test/hyper_links_test.dart # e2e_app/integration_test/message_actions_test.dart # e2e_app/integration_test/message_delivery_status_test.dart # e2e_app/integration_test/message_list_test.dart # e2e_app/integration_test/pages/channel_list_page.dart # e2e_app/integration_test/robots/backend_robot.dart # e2e_app/integration_test/robots/participant_robot.dart # e2e_app/integration_test/robots/user_robot.dart # e2e_app/integration_test/robots/user_robot_message_list_asserts.dart # e2e_app/integration_test/support/stream_test_env.dart # e2e_app/lib/auth/auth_controller.dart # e2e_app/lib/pages/channel_list_tab.dart # e2e_app/lib/pages/home_page.dart # e2e_app/lib/pages/thread_list_tab.dart # e2e_app/lib/widgets/e2e_message_actions.dart # e2e_app/lib/widgets/search_text_field.dart # e2e_app/pubspec.yaml # packages/stream_chat_flutter/CHANGELOG.md # packages/stream_chat_flutter/test/src/message_list_view/message_list_failed_message_grouping_test.dart
What
A web address written without a scheme, such as
youtube.comorgetstream.io/chat, rendered as plain text even though the backend gives it a link preview. It now becomes a link that opens overhttps://.Link detection now lives in core (GetStream/stream-core-flutter#205):
findLinksinstream_core, used byStreamMessageTextinstream_core_flutter. That PR is merged; this PR pins both packages to its commit on core'smain(74a65f0) and drops the local syntax that the first version of this PR added. Because the publishedstream_corelacks the finder, each app that resolvesstream_core_flutteroverridesstream_coreto the same commit, following the pattern v11 uses.Behaviour in a message:
config.jsonstays plain.www.addresses now open overhttps://instead ofhttp://, the scheme the backend uses for the preview.[label](url)link keeps the label's target.mailto:link.Fixes FLU-1094.
Stacked on #3105 (the e2e suite); GitHub retargets it to
masteronce that merges.Verification
stream_message_text_test.darttaps the rendered link through chat'sStreamMessageText. It covers bare domains with and without a path, trailing punctuation, an unknown top-level domain,www.opening over https, an address inside a word or a link label, and emails.stream_chat_fluttersuite passes (1,664 passed); only the local macOS goldens fail.sample_app,e2e_app, the examples,docs_screenshots) resolvesstream_coreandstream_core_flutterfrom74a65f0aftermelos bootstrap.v11: see the "Merging master into v11" comment on FLU-1094.
🤖 Generated with Claude Code
https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC