diff --git a/lib/view/pages/talk/chat_view.dart b/lib/view/pages/talk/chat_view.dart index 0d62c39..39d47ba 100644 --- a/lib/view/pages/talk/chat_view.dart +++ b/lib/view/pages/talk/chat_view.dart @@ -189,9 +189,7 @@ class _ChatViewState extends State with RouteAware { setState(() { _activeMatchIndex = (_activeMatchIndex + 1) % _matches.length; }); - WidgetsBinding.instance.addPostFrameCallback( - (_) => _scrollToActiveMatch(), - ); + WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActiveMatch()); } void _goToNextMatch() { @@ -200,9 +198,7 @@ class _ChatViewState extends State with RouteAware { _activeMatchIndex = (_activeMatchIndex - 1 + _matches.length) % _matches.length; }); - WidgetsBinding.instance.addPostFrameCallback( - (_) => _scrollToActiveMatch(), - ); + WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActiveMatch()); } void _scrollToActiveMatch() { @@ -230,8 +226,9 @@ class _ChatViewState extends State with RouteAware { final activeId = _matches.isNotEmpty ? _matches[_activeMatchIndex].messageId : null; - final highlightQuery = - _searchActive && _searchQuery.trim().isNotEmpty ? _searchQuery : null; + final highlightQuery = _searchActive && _searchQuery.trim().isNotEmpty + ? _searchQuery + : null; final messages = []; final chronologicalMatchIndex = {}; @@ -264,8 +261,8 @@ class _ChatViewState extends State with RouteAware { final isMatch = matchIds.contains(element.id); final highlight = isMatch ? (element.id == activeId - ? SearchHighlight.active - : SearchHighlight.secondary) + ? SearchHighlight.active + : SearchHighlight.secondary) : SearchHighlight.none; if (isMatch) chronologicalMatchIndex[element.id] = messages.length; @@ -320,87 +317,102 @@ class _ChatViewState extends State with RouteAware { } @override - Widget build(BuildContext context) => Scaffold( - backgroundColor: const Color(0xffefeae2), - appBar: _searchActive - ? ChatSearchAppBar( - controller: _searchTextController, - matchCount: _matches.length, - activeIndex: _matches.isEmpty ? -1 : _activeMatchIndex, - onChanged: _onSearchChanged, - onClose: _exitSearchMode, - onPrevious: _matches.isEmpty ? null : _goToPreviousMatch, - onNext: _matches.isEmpty ? null : _goToNextMatch, - ) - : ClickableAppBar( - onTap: () => - TalkNavigator.pushSplitView(context, ChatInfo(widget.room)), - appBar: AppBar( - title: Row( - children: [ - widget.avatar, - const SizedBox(width: 10), - Expanded( - child: Text( - widget.room.displayName, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + Widget build(BuildContext context) { + // Swallow the first back gesture while the keyboard is visible so it + // dismisses the IME instead of popping the chat — matches platform UX + // expectations (e.g. WhatsApp/Telegram) and prevents accidental exits + // mid-typing. + final keyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0; + return PopScope( + canPop: !keyboardOpen, + onPopInvokedWithResult: (didPop, _) { + if (didPop) return; + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Scaffold( + backgroundColor: const Color(0xffefeae2), + appBar: _searchActive + ? ChatSearchAppBar( + controller: _searchTextController, + matchCount: _matches.length, + activeIndex: _matches.isEmpty ? -1 : _activeMatchIndex, + onChanged: _onSearchChanged, + onClose: _exitSearchMode, + onPrevious: _matches.isEmpty ? null : _goToPreviousMatch, + onNext: _matches.isEmpty ? null : _goToNextMatch, + ) + : ClickableAppBar( + onTap: () => + TalkNavigator.pushSplitView(context, ChatInfo(widget.room)), + appBar: AppBar( + title: Row( + children: [ + widget.avatar, + const SizedBox(width: 10), + Expanded( + child: Text( + widget.room.displayName, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], ), - ], - ), - actions: [ - IconButton( - icon: const Icon(Icons.search), - tooltip: 'In Chat suchen', - onPressed: _enterSearchMode, + actions: [ + IconButton( + icon: const Icon(Icons.search), + tooltip: 'In Chat suchen', + onPressed: _enterSearchMode, + ), + ], ), - ], + ), + body: DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: const AssetImage('assets/background/chat.png'), + scale: 1.5, + opacity: 1, + repeat: ImageRepeat.repeat, + invertColors: AppTheme.isDarkMode(context), ), ), - body: DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: const AssetImage('assets/background/chat.png'), - scale: 1.5, - opacity: 1, - repeat: ImageRepeat.repeat, - invertColors: AppTheme.isDarkMode(context), + child: Column( + children: [ + Expanded( + child: LoadableStateConsumer( + isReady: (state) => + state.chatResponse != null && + state.currentToken == widget.room.token, + enablePullToRefresh: false, + child: (state, _) { + final items = _buildMessages( + state.chatResponse!, + ).reversed.toList(); + return ScrollablePositionedList.builder( + reverse: true, + itemScrollController: _itemScrollController, + itemCount: items.length, + itemBuilder: (ctx, idx) => items[idx], + ); + }, + ), + ), + ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: TalkNavigator.isSecondaryVisible(context) + ? ChatTextfield(widget.room.token, selfId: widget.selfId) + : SafeArea( + child: ChatTextfield( + widget.room.token, + selfId: widget.selfId, + ), + ), + ), + ], + ), ), ), - child: Column( - children: [ - Expanded( - child: LoadableStateConsumer( - isReady: (state) => - state.chatResponse != null && - state.currentToken == widget.room.token, - enablePullToRefresh: false, - child: (state, _) { - final items = - _buildMessages(state.chatResponse!).reversed.toList(); - return ScrollablePositionedList.builder( - reverse: true, - itemScrollController: _itemScrollController, - itemCount: items.length, - itemBuilder: (ctx, idx) => items[idx], - ); - }, - ), - ), - ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: TalkNavigator.isSecondaryVisible(context) - ? ChatTextfield(widget.room.token, selfId: widget.selfId) - : SafeArea( - child: ChatTextfield( - widget.room.token, - selfId: widget.selfId, - ), - ), - ), - ], - ), - ), - ); + ); + } } diff --git a/lib/view/pages/talk/widgets/chat_textfield.dart b/lib/view/pages/talk/widgets/chat_textfield.dart index 26dd9db..135b9de 100644 --- a/lib/view/pages/talk/widgets/chat_textfield.dart +++ b/lib/view/pages/talk/widgets/chat_textfield.dart @@ -110,17 +110,27 @@ class _ChatTextfieldState extends State { if (_textBoxController.text.isEmpty) return; final text = _textBoxController.text; final replyTo = chatBloc.state.data?.referenceMessageId?.toString(); + final ownToken = widget.sendToToken; setState(() => _sendError = null); await SendMessage( - widget.sendToToken, + ownToken, SendMessageParams(text, replyTo: replyTo), ).run(); - if (!mounted) return; - chatBloc.refresh(); - _textBoxController.text = ''; + // Reached only on success — SendMessage.run() throws on failure and + // skips this block, leaving the persisted draft as a recovery aid. + // Drafts live on the global SettingsCubit keyed by token, so clear + // them even when the user navigated away mid-send — otherwise the + // sent message lingers as a phantom draft on the chat list. _setDraft(''); - chatBloc.setReferenceMessageId(null); _setDraftReply(null); + // The global ChatBloc may already point at a different chat (user + // switched mid-send); only touch it while it still references us. + if (chatBloc.state.data?.currentToken == ownToken) { + chatBloc.setReferenceMessageId(null); + chatBloc.refresh(); + } + if (!mounted) return; + _textBoxController.text = ''; } @override