Skip to content

feat(ui): link web addresses written without a scheme - #205

Merged
xsahil03x merged 5 commits into
ci/pana-sibling-pathsfrom
feat/link-finder
Oct 27, 2026
Merged

xsahil03x merged 5 commits into
ci/pana-sibling-pathsfrom
feat/link-finder

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

StreamMessageText now links web addresses written without a scheme, such as youtube.com, as the iOS, Android and React Native chat SDKs already do (FLU-1094).

What changed

stream_core

  • Adds findLinks, findLinkAt and LinkMatch. They find web links with or without a scheme. An address without one counts only when it ends in an IANA top-level domain, so config.json stays plain text.
  • The domain list is generated from IANA by melos run generate:tlds, a part of link_finder.dart that only the finder uses. The update_top_level_domains workflow regenerates it monthly and opens a PR when it changes.
  • This brings back a bundled top-level domain list on purpose, after chore(ui): drop the bundled IANA TLD list stream-chat-flutter#2654 (FLU-490) removed one. That list only double-checked addresses that already had a scheme. Here the list is the only thing that tells youtube.com apart from config.json, and the monthly refresh deals with it going stale.
  • It lives in stream_core rather than stream_core_flutter so that v11's stream_chat and stream_chat_flutter_core, which depend only on stream_core, can use the same matcher.

stream_core_flutter (now depends on stream_core)

  • StreamMessageText links every web address through findLinkAt, using one LinkSyntax subclass.
  • Addresses without a scheme, www. ones included, open over https://, the scheme the backend's preview scraper and stream-chat-js's composer already use. Before, www. opened over http://.
  • Addresses right after a quote or Chinese/Japanese text are now linked.
  • An address inside a link label stays part of that link. This hooks into markdown's own createNode, so inline, reference, nested and escaped labels all behave.
  • Custom inlineSyntaxes still take priority. Email and ftp:// links still come from markdown's autolinks.

How it was verified

  • Tests: 66 finder tests in stream_core and 18 widget tests that tap the rendered link. Every guard in the finder (21) and in the syntax (6) was broken one at a time and caught by a test.

  • Analyze, format and check:barrels are clean. The full stream_core_flutter suite fails only its 57 goldens locally, the same count as on main.

  • packages/stream_core/benchmark/link_finder_benchmark.dart (benchmark_harness, AOT):

    Input Time per call
    Typical message (~200 chars) 29 µs
    3.6k prose with links 141 µs
    5k Cyrillic 1.6 ms
    10k crafted CJK + dots 4.8 ms
    20k ) or . 0.09 ms

    Every case scales linearly with length, including inputs built to slow a link scanner down.

  • Markdown parse with the syntax compared to without (AOT): messages with links parse faster, because it takes over from the GFM autolinker. Link-free text costs +5 µs on a typical message and +53 µs per 1k characters. A widget pump comparison shows no measurable difference for messages with links.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC

Add findLinks and findLinkAt to stream_core, matching web links with or
without a scheme against the IANA top-level domains, and link every web
address in StreamMessageText through them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC
@xsahil03x
xsahil03x requested a review from a team as a code owner October 27, 2026 14:42
@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

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 39 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review 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: 52f97a6e-de08-4601-a7ad-1dc2f0af05c0
Commits

Reviewing files that changed from the base of the PR and between 3fdd421 and 66884fb.

Files selected for processing: 15
  • .github/workflows/update_top_level_domains.yml
  • STYLE_GUIDE.md
  • melos.yaml
  • packages/stream_core/CHANGELOG.md
  • packages/stream_core/benchmark/link_finder_benchmark.dart
  • packages/stream_core/lib/src/utils.dart
  • packages/stream_core/lib/src/utils/link_finder.dart
  • packages/stream_core/lib/src/utils/link_finder.tlds.g.dart
  • packages/stream_core/pubspec.yaml
  • packages/stream_core/test/utils/link_finder_test.dart
  • packages/stream_core_flutter/CHANGELOG.md
  • packages/stream_core_flutter/lib/src/components/message/stream_message_text.dart
  • packages/stream_core_flutter/pubspec.yaml
  • packages/stream_core_flutter/test/components/message/stream_message_text_links_test.dart
  • scripts/generate_top_level_domains.dart
  • 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

❌ Patch coverage is 97.80220% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.98%. Comparing base (3fdd421) to head (66884fb).

Files with missing lines Patch % Lines
...ackages/stream_core/lib/src/utils/link_finder.dart 97.45% 4 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                    @@
##           ci/pana-sibling-paths     #205      +/-   ##
=========================================================
+ Coverage                  71.46%   71.98%   +0.51%     
=========================================================
  Files                        215      216       +1     
  Lines                       8884     9066     +182     
=========================================================
+ Hits                        6349     6526     +177     
- Misses                      2535     2540       +5     

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC
Comment thread scripts/generate_top_level_domains.dart Outdated
/// Or directly, from the repository root:
/// dart run scripts/generate_top_level_domains.dart
///
/// Run it when a link with a new top-level domain is not found, or

@VelikovPetar VelikovPetar Oct 27, 2026 •

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 generator looks solid (it validates the download, writes nothing on failure, and skips no-op writes). My concern is that nothing ever runs it. It isn't part of generate:all and isn't in CI, so "periodically" in practice means "when someone reports a link that doesn't work". That's the same staleness problem that got the old list removed in GetStream/stream-chat-flutter#2654.

Since the script already makes no change when the set is unchanged, a scheduled GitHub Action (monthly, say) could run melos run generate:tlds and open a PR whenever the file changes. Updates would cost nothing and stay reviewable. If that's out of scope for this PR, could we at least add a ticket or a line in the release checklist so it doesn't depend on someone remembering?

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.

Added in 66884fb. The update_top_level_domains workflow runs the generator on the 1st of every month and opens a chore(repo) PR with the bot's token, so its checks run. Since the script leaves the file alone when the set hasn't changed, a quiet month opens nothing. It can also be triggered by hand.

@xsahil03x
xsahil03x changed the base branch from main to ci/pana-sibling-paths October 27, 2026 15:26
@xsahil03x
xsahil03x added this pull request to stack #207 October 27, 2026 15:32
xsahil03x and others added 2 commits October 27, 2026 16:36
Run the generator on the first of every month and open a pull request when
the list has changed, so it doesn't depend on someone remembering to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC
@xsahil03x
xsahil03x merged commit 74a65f0 into main Oct 27, 2026
17 of 20 checks passed
@xsahil03x
xsahil03x deleted the feat/link-finder branch October 27, 2026 15:57
xsahil03x added a commit to GetStream/stream-chat-flutter that referenced this pull request Oct 27, 2026
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
xsahil03x added a commit to GetStream/stream-chat-flutter that referenced this pull request Oct 28, 2026
* test(e2e): add e2e_app, port native e2e suites, one app process per CI batch

* chore(repo): ignore e2e mock server, Allure results and allurectl in any app

* test(repo): link every e2e test to its Allure TestOps case

* test(repo): unmount the e2e app before resetting the client, verify clearComposer

* ci(repo): run PR e2e on API 36, use a preinstalled NDK, retry failed builds whole

* test(repo): retype into a field that ignored the text

* test(repo): keep the copied message id in the e2e app, bound the video step

* test(repo): scroll until a message is on screen, stop when the emulator dies

* ci(repo): give the e2e app's Gradle the sample app's memory settings

* Bump timeout

* test(repo): restart a dead emulator during the e2e run

* test(repo): time-limit e2e runs so a dead emulator can't hang the job

* fix(repo): look up timeout through sh so the lane runs on Linux

* ci(repo): split the e2e coverage nightly into 4 batches

* ci(repo): give the e2e runners memory headroom

* test(repo): retry the tap that opens the image gallery

* fix(ui): link web addresses written without a scheme (FLU-1094)

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

* test(repo): assert the token date and signature policy (#3123)

* 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 (#3122)

* 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>

* fix(ui): link web addresses through the core link finder (FLU-1094)

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

* chore(repo): sort the stream_core override in the example pubspecs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vTgFwqhUc2wKCWrw635yC

* chore(repo): pin stream-core-flutter to the merged link finder

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

---------

Co-authored-by: Alexey Alter-Pesotskiy <alex@testableapple.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

2 participants