@@ -347,9 +347,18 @@ class ChatPageBloc extends Bloc<ChatPageEvent, ChatPageState> {
347347
348348 final channelId = channel.channelId;
349349 if (channelId != null ) {
350- initLiveCollection (channelId);
350+ // Await init so the message-stream + loading-state listeners and the
351+ // live collection's reactor are fully wired before the first page is
352+ // loaded. Then load the first page directly WITHOUT reset(): reset()
353+ // stops the reactor and forces an empty first-page query whose
354+ // deletePagingIdByHash wipes paging-ids, which races the user's first
355+ // optimistic send (the create flow drops them straight into a ready
356+ // chat) and makes that message invisible until re-entry (PDT new-chat
357+ // message-not-shown). Keeping the reactor alive lets the optimistic
358+ // message keep its paging-id.
359+ await initLiveCollection (channelId);
351360 addEvent (ChatPageEventChannelIdChanged (channelId));
352- addEvent ( ChatPageEventRefresh () );
361+ liveCollection ? . loadNext ( );
353362 addEvent (const ChatPageEventFetchMuteState ());
354363 }
355364 });
@@ -508,12 +517,16 @@ class ChatPageBloc extends Bloc<ChatPageEvent, ChatPageState> {
508517 addEvent (ChatPageSetAroundMessage (
509518 aroundMessageId: event.aroundMessageId));
510519
511- // Reinitialize the live collection with the aroundMessageId
512- initLiveCollection (state.channelId,
520+ // Reinitialize the live collection with the aroundMessageId. Await so the
521+ // rebuilt collection + listeners are wired, then load the first page
522+ // WITHOUT reset(): reset() stops the reactor and forces an empty-first-page
523+ // paging-id wipe that can hide an in-flight optimistic send. Keeping the
524+ // reactor alive avoids that race.
525+ await initLiveCollection (state.channelId,
513526 aroundMessageId: event.aroundMessageId);
514527
515- // Refresh to load messages around the target message
516- addEvent ( ChatPageEventRefresh () );
528+ // Load messages around the target message.
529+ liveCollection ? . loadNext ( );
517530 });
518531
519532 on < ChatPageSetAroundMessage > ((event, emit) async {
@@ -555,9 +568,14 @@ class ChatPageBloc extends Bloc<ChatPageEvent, ChatPageState> {
555568 if (channelId != null && channelId.isNotEmpty) {
556569 addEvent (ChatPageSetAroundMessage (aroundMessageId: jumpToMessageId));
557570
571+ // initLiveCollection assigns liveCollection and attaches its listeners
572+ // synchronously (before its first await) on a fresh open, so loadNext()
573+ // below runs against the live collection. Load directly WITHOUT reset()
574+ // so the reactor is never stopped (avoids the empty-first-page paging-id
575+ // wipe racing an optimistic send).
558576 initLiveCollection (channelId, aroundMessageId: jumpToMessageId);
559577 addEvent (ChatPageEventChannelIdChanged (channelId));
560- addEvent ( ChatPageEventRefresh () );
578+ liveCollection ? . loadNext ( );
561579 addEvent (const ChatPageEventFetchMuteState ());
562580 } else if (userId != null && userId.isNotEmpty) {
563581 addEvent (ChatPageEventCreateNewChannel (userId: userId));
@@ -622,6 +640,9 @@ class ChatPageBloc extends Bloc<ChatPageEvent, ChatPageState> {
622640
623641 @override
624642 Future <void > close () {
643+ // PDT-3317: guarantee the global loading toast is dismissed when leaving
644+ // the page so a stuck "loading" toast can never leak across the app.
645+ toastBloc.add (AmityToastDismissIfLoading ());
625646 _scrollController.dispose ();
626647 subscription.cancel ();
627648 _jumpToMessageTimeoutTimer? .cancel ();
@@ -630,7 +651,34 @@ class ChatPageBloc extends Bloc<ChatPageEvent, ChatPageState> {
630651 return super .close ();
631652 }
632653
633- void initLiveCollection (String channelId, {String ? aroundMessageId}) async {
654+ /// Resolve the 1:1 header's other participant from the LOCAL cache only (no
655+ /// network `getMembers()` ). No-op once `state.channelMember` is set, so the
656+ /// message-listener retry stops as soon as the header fills.
657+ Future <void > _resolveHeaderFromCache (String channelId) async {
658+ if (state.channelMember != null ) return ;
659+ try {
660+ final list = await AmityChatClient .newChannelRepository ()
661+ .membership (channelId)
662+ .getMembersFromCache ();
663+ AmityChannelMember ? otherMember;
664+ for (final member in list) {
665+ if (member.userId != AmityCoreClient .getUserId ()) {
666+ otherMember = member;
667+ break ;
668+ }
669+ }
670+
671+ if (otherMember? .user != null ) {
672+ addEvent (ChatPageHeaderEventChanged (channelMember: otherMember! ));
673+ // User will be updated in the HeaderEventChanged handler
674+ }
675+ } catch (_) {
676+ // Cache not ready yet; a later collection emit retries.
677+ }
678+ }
679+
680+ Future <void > initLiveCollection (String channelId,
681+ {String ? aroundMessageId}) async {
634682 if (liveCollection != null ) {
635683 liveCollection? .getStreamController ().close ();
636684 await liveCollection? .dispose ();
@@ -651,26 +699,30 @@ class ChatPageBloc extends Bloc<ChatPageEvent, ChatPageState> {
651699
652700 liveCollection = query.getLiveCollection ();
653701
654- final list = await AmityChatClient .newChannelRepository ()
655- .membership (channelId)
656- .getMembersFromCache ();
657- final otherMember = list
658- .firstWhere ((element) => element.userId != AmityCoreClient .getUserId ());
659-
660- if (otherMember.user != null ) {
661- addEvent (ChatPageHeaderEventChanged (channelMember: otherMember));
662- // User will be updated in the HeaderEventChanged handler
663- }
664-
702+ // Attach the message + loading-state listeners FIRST so the page can
703+ // render and the loading state can reach `false` regardless of the
704+ // member-cache lookup below. The lookup used an unguarded `firstWhere`
705+ // that throws "Bad state: No element" when the member cache is empty or
706+ // holds only the current user (a never-messaged contact whose channel
707+ // exists with no messages). Because the throw happened before these
708+ // listeners attached, the chat hung on loading.
665709 liveCollection? .getStreamController ().stream.listen ((event) {
666710 addEvent (
667711 ChatPageEventChanged (messages: event, isFetching: state.isFetching));
712+ // Brand-new chat: member cache is empty at init, so the header starts
713+ // blank. Retry the cache read on each emit until it resolves — no network
714+ // getMembers(); no-op once state.channelMember is set.
715+ if (state.channelMember == null ) _resolveHeaderFromCache (channelId);
668716 });
669717
670718 liveCollection? .observeLoadingState ().listen ((event) {
671719 addEvent (ChatPageEventLoadingStateUpdated (isFetching: event));
672720 });
673721
722+ // First attempt at init; may be empty for a just-created channel, in which
723+ // case the message-listener retry above resolves it once the cache fills.
724+ await _resolveHeaderFromCache (channelId);
725+
674726 // Observe Network Connectivity status here
675727 subscription =
676728 Connectivity ().onConnectivityChanged.listen ((connectivityEvent) {
0 commit comments