@@ -45,7 +45,30 @@ class StreamChannel extends StatefulWidget {
4545 this .initialMessageId,
4646 this .errorBuilder = _defaultErrorBuilder,
4747 this .loadingBuilder = _defaultLoadingBuilder,
48- });
48+ }) : _shouldPosition = true ;
49+
50+ /// Exposes a [channel] to descendants without repositioning the loaded
51+ /// window on mount.
52+ ///
53+ /// Use this when wrapping a channel in a sub-route or overlay for context
54+ /// access only — e.g. a thread page, channel info screen, long-press
55+ /// modal, or attachment viewer. The default constructor would otherwise
56+ /// re-run channel-page positioning and overwrite the parent route's
57+ /// loaded window.
58+ ///
59+ /// See also:
60+ ///
61+ /// * [StreamChannel.new] , which initializes the channel and positions it
62+ /// on mount.
63+ const StreamChannel .value ({
64+ super .key,
65+ required this .child,
66+ required this .channel,
67+ }) : showLoading = false ,
68+ initialMessageId = null ,
69+ errorBuilder = _defaultErrorBuilder,
70+ loadingBuilder = _defaultLoadingBuilder,
71+ _shouldPosition = false ;
4972
5073 /// The child of the widget
5174 final Widget child;
@@ -65,6 +88,10 @@ class StreamChannel extends StatefulWidget {
6588 /// Widget builder used in case an error occurs while building the channel.
6689 final ErrorWidgetBuilder errorBuilder;
6790
91+ // Whether to position the loaded window on mount (initialMessageId,
92+ // last-read, or latest). Only false for StreamChannel.value.
93+ final bool _shouldPosition;
94+
6895 static Widget _defaultLoadingBuilder (BuildContext context) {
6996 final backgroundColor = _getDefaultBackgroundColor (context);
7097 return Material (
@@ -675,16 +702,43 @@ class StreamChannelState extends State<StreamChannel> {
675702 );
676703 }
677704
678- ///
705+ /// Returns the message with the given [messageId] .
679706 Future <Message > getMessage (String messageId) async {
680- var message = channel.state? .messages.firstWhereOrNull (
681- (it) => it.id == messageId,
707+ if (_findCachedMessage (messageId) case final cached? ) return cached;
708+
709+ final response = await channel.getMessagesById ([messageId]);
710+ return response.messages.firstWhere (
711+ (m) => m.id == messageId,
712+ orElse: () => throw StateError ('Message "$messageId " not found' ),
682713 );
683- if (message == null ) {
684- final response = await channel.getMessagesById ([messageId]);
685- message = response.messages.first;
714+ }
715+
716+ // Scans cached locations in decreasing hit-likelihood, returning on the
717+ // first match. Plain for-loops over `firstWhereOrNull`/`expand` to avoid
718+ // per-call closure and iterable allocations.
719+ Message ? _findCachedMessage (String messageId) {
720+ final state = channel.state;
721+ if (state == null ) return null ;
722+
723+ // Hot path: regular channel messages in the loaded window.
724+ for (final message in state.messages) {
725+ if (message.id == messageId) return message;
686726 }
687- return message;
727+
728+ // Thread replies — only helps when `messageId` is itself a reply; thread
729+ // parents live in `state.messages`, not under `state.threads`.
730+ for (final replies in state.threads.values) {
731+ for (final message in replies) {
732+ if (message.id == messageId) return message;
733+ }
734+ }
735+
736+ // Pinned messages can sit outside the loaded window, so check them last.
737+ for (final message in state.pinnedMessages) {
738+ if (message.id == messageId) return message;
739+ }
740+
741+ return null ;
688742 }
689743
690744 /// Query channel members.
@@ -790,6 +844,10 @@ class StreamChannelState extends State<StreamChannel> {
790844 // Otherwise, we first initialize the channel if it's not yet initialized.
791845 if (channel.state == null ) await channel.watch ();
792846
847+ // If the widget was created using the StreamChannel.value constructor,
848+ // we skip positioning so the already-loaded channel state is kept intact.
849+ if (! widget._shouldPosition) return ;
850+
793851 // First we try to load the channel at the initial message if
794852 // 'initialMessageId' is provided in the widget.
795853 if (widget.initialMessageId case final initialMessageId? ) {
0 commit comments