Skip to content

fix(ui): link web addresses written without a scheme - #3125

Open
xsahil03x wants to merge 26 commits into
masterfrom
sahil/flu-1094-a-bare-domain-in-a-message-youtubecom-is-not-turned-into-a
Open

xsahil03x wants to merge 26 commits into
masterfrom
sahil/flu-1094-a-bare-domain-in-a-message-youtubecom-is-not-turned-into-a

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

What

A web address written without a scheme, such as youtube.com or getstream.io/chat, rendered as plain text even though the backend gives it a link preview. It now becomes a link that opens over https://.

Link detection now lives in core (GetStream/stream-core-flutter#205): findLinks in stream_core, used by StreamMessageText in stream_core_flutter. That PR is merged; this PR pins both packages to its commit on core's main (74a65f0) and drops the local syntax that the first version of this PR added. Because the published stream_core lacks the finder, each app that resolves stream_core_flutter overrides stream_core to the same commit, following the pattern v11 uses.

Behaviour in a message:

  • An address without a scheme links only when it ends in a known top-level domain, so config.json stays plain.
  • www. addresses now open over https:// instead of http://, the scheme the backend uses for the preview.
  • Addresses right after a quote or Chinese/Japanese text now link.
  • An address inside a [label](url) link keeps the label's target.
  • Emails keep their mailto: link.

Fixes FLU-1094.

Stacked on #3105 (the e2e suite); GitHub retargets it to master once that merges.

Before After

Verification

  • stream_message_text_test.dart taps the rendered link through chat's StreamMessageText. 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.
  • The full stream_chat_flutter suite passes (1,664 passed); only the local macOS goldens fail.
  • Every app (sample_app, e2e_app, the examples, docs_screenshots) resolves stream_core and stream_core_flutter from 74a65f0 after melos bootstrap.
  • e2e: "message with a link without a scheme opens the browser" is un-skipped and runs in this PR's E2E workflow.
  • Matching, performance and markdown edge cases are covered in feat(ui): link web addresses written without a scheme stream-core-flutter#205.

v11: see the "Merging master into v11" comment on FLU-1094.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC

testableapple and others added 19 commits October 23, 2026 14:55
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
@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: be64b6df-eb30-4af1-8ecc-6ad97341a182
Commits

Reviewing files that changed from the base of the PR and between 81f5c19 and 85b61d0.

Files selected for processing: 11
  • docs/docs_screenshots/pubspec.yaml
  • e2e_app/integration_test/message_list_test.dart
  • e2e_app/pubspec.yaml
  • melos.yaml
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/example/pubspec.yaml
  • packages/stream_chat_flutter/pubspec.yaml
  • packages/stream_chat_flutter/test/src/message_widget/stream_message_text_test.dart
  • packages/stream_chat_localizations/example/pubspec.yaml
  • packages/stream_chat_localizations/pubspec.yaml
  • sample_app/pubspec.yaml
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.32%. Comparing base (018b487) to head (85b61d0).
⚠️ Report is 3 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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', '(', '"', "'", '*', '~'};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
  }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TLD lookup is case-sensitive, so GOOGLE.COM and Example.Com stay plain while YouTube.com links:

Suggested change
return _topLevelDomains.contains(label) || _punycodeTld.hasMatch(label);
return _topLevelDomains.contains(label.toLowerCase()) || _punycodeTld.hasMatch(label);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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, opens https://%E2%80%9Cyoutube.com
  • «youtube.com», ¿youtube.com? and see—youtube.com behave 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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

xsahil03x and others added 6 commits October 27, 2026 12:34
* 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
Base automatically changed from test/e2e-native-parity to master October 27, 2026 17:37
…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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants