From 09d4ecda78c00424cb003130b5c82ef1e8ad6ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Fri, 14 Aug 2026 09:47:56 +0200 Subject: [PATCH] fixed bug that prevents sending messages if reply was triggered in a different chat --- lib/api/errors/talk_exception.dart | 8 +++++++- lib/api/marianumcloud/talk/talk_api.dart | 12 ++++++++++++ .../app/modules/chat/bloc/chat_bloc.dart | 4 ++++ .../pages/talk/widgets/chat_textfield.dart | 19 ++++++++++++++++++- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/api/errors/talk_exception.dart b/lib/api/errors/talk_exception.dart index 00bbe18..dc77588 100644 --- a/lib/api/errors/talk_exception.dart +++ b/lib/api/errors/talk_exception.dart @@ -4,16 +4,22 @@ import 'app_exception.dart'; class TalkException extends AppException { final TalkError source; - TalkException(this.source) + TalkException(this.source, {String? technicalDetails}) : super( userMessage: _mapMessage(source), technicalDetails: + technicalDetails ?? 'Talk ${source.status} (${source.code}): ${source.message}', allowRetry: source.code >= 500, ); static String _mapMessage(TalkError e) { switch (e.code) { + case 400: + return 'Talk hat diese Anfrage abgelehnt. Bitte lade den Chat neu und ' + 'versuche es erneut.'; + case 413: + return 'Diese Nachricht ist zu lang.'; case 401: return 'Bitte melde dich erneut an, um auf Talk zuzugreifen.'; case 403: diff --git a/lib/api/marianumcloud/talk/talk_api.dart b/lib/api/marianumcloud/talk/talk_api.dart index d8b4e6a..f8c3e60 100644 --- a/lib/api/marianumcloud/talk/talk_api.dart +++ b/lib/api/marianumcloud/talk/talk_api.dart @@ -6,8 +6,10 @@ import '../../api_params.dart'; import '../../api_response.dart'; import '../../errors/network_exception.dart'; import '../../errors/parse_exception.dart'; +import '../../errors/talk_exception.dart'; import '../../http_errors.dart'; import '../nextcloud_ocs.dart'; +import 'talk_error.dart'; abstract class TalkApi { String path; @@ -49,6 +51,16 @@ abstract class TalkApi { // logs surface the cause instead of just the bare status code. final detail = httpErrorDetail('Talk $endpoint', data.body, status); log(detail); + // Talk's own codes need their own wording: a 400/412/413 here is a + // rejected request, not a struggling server, and "try again later" + // would send the user in circles. 401/403/404 keep the shared mapping + // so the auth handling stays in one place. + if (status == 400 || status == 412 || status == 413 || status == 429) { + throw TalkException( + TalkError('failure', status, ''), + technicalDetails: detail, + ); + } throwForStatus(status, detail); } diff --git a/lib/state/app/modules/chat/bloc/chat_bloc.dart b/lib/state/app/modules/chat/bloc/chat_bloc.dart index 4bdd44a..f8384b2 100644 --- a/lib/state/app/modules/chat/bloc/chat_bloc.dart +++ b/lib/state/app/modules/chat/bloc/chat_bloc.dart @@ -93,9 +93,13 @@ class ChatBloc _stopLongPoll(); add( Emit( + // A reply reference belongs to the room it was picked in — carrying it + // into the next chat makes every send there fail on an unrelated + // parent id. ChatTextfield restores this room's draft reply after. (s) => s.copyWith( currentToken: token, chatResponse: null, + referenceMessageId: null, hasMoreOld: true, isLoadingOlder: false, ), diff --git a/lib/view/pages/talk/widgets/chat_textfield.dart b/lib/view/pages/talk/widgets/chat_textfield.dart index ee1cacc..0473bcf 100644 --- a/lib/view/pages/talk/widgets/chat_textfield.dart +++ b/lib/view/pages/talk/widgets/chat_textfield.dart @@ -11,6 +11,7 @@ import '../../../../api/marianumcloud/talk/send_message/send_message_params.dart import '../../../../api/marianumcloud/talk/share_files_to_chat.dart'; import '../../../../api/marianumcloud/webdav/webdav_api.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; +import '../../../../state/app/modules/chat/bloc/chat_state.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../widget/async_action_button.dart'; import '../../../../widget/demo_restricted.dart'; @@ -216,11 +217,27 @@ class _ChatTextfieldState extends State { _setDraft(newText); } + /// Reply target for the next send, or null when there is none. Requires the + /// referenced message to sit in this room's loaded history — the same + /// criterion the reply banner renders on, so what the user sees is what gets + /// sent. Talk rejects the whole message when the parent id is foreign to the + /// room, so a leftover reference would otherwise block the chat entirely. + String? _replyTargetFor(ChatState? chatState) { + final referenceId = chatState?.referenceMessageId; + if (referenceId == null) return null; + if (chatState!.currentToken != widget.sendToToken) return null; + final messages = chatState.chatResponse?.data; + if (messages == null || !messages.any((e) => e.id == referenceId)) { + return null; + } + return referenceId.toString(); + } + Future _sendMessage(ChatBloc chatBloc) async { if (_textBoxController.text.isEmpty) return; if (guardDemoAction(context)) return; final text = _textBoxController.text; - final replyTo = chatBloc.state.data?.referenceMessageId?.toString(); + final replyTo = _replyTargetFor(chatBloc.state.data); final ownToken = widget.sendToToken; setState(() => _sendError = null); await SendMessage(