Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a09817a975 | |||
| 37dbb7b374 |
@@ -60,7 +60,15 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
||||
|
||||
final status = data.statusCode;
|
||||
if (status < 200 || status >= 300) {
|
||||
final detail = 'Talk $endpoint -> HTTP $status';
|
||||
// Talk's OCS errors put the real reason in the response body (e.g.
|
||||
// expired session, removed participant, malformed reply target).
|
||||
// Include a trimmed preview so the in-app error dialog and logs
|
||||
// surface the cause instead of just the bare status code.
|
||||
final body = data.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty
|
||||
? 'Talk $endpoint -> HTTP $status'
|
||||
: 'Talk $endpoint -> HTTP $status body=$preview';
|
||||
log(detail);
|
||||
if (status == 401) {
|
||||
throw AuthException.unauthorized(technicalDetails: detail);
|
||||
|
||||
@@ -189,9 +189,7 @@ class _ChatViewState extends State<ChatView> 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<ChatView> 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<ChatView> 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 = <Widget>[];
|
||||
final chronologicalMatchIndex = <int, int>{};
|
||||
@@ -320,7 +317,19 @@ class _ChatViewState extends State<ChatView> with RouteAware {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
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(
|
||||
@@ -377,8 +386,9 @@ class _ChatViewState extends State<ChatView> with RouteAware {
|
||||
state.currentToken == widget.room.token,
|
||||
enablePullToRefresh: false,
|
||||
child: (state, _) {
|
||||
final items =
|
||||
_buildMessages(state.chatResponse!).reversed.toList();
|
||||
final items = _buildMessages(
|
||||
state.chatResponse!,
|
||||
).reversed.toList();
|
||||
return ScrollablePositionedList.builder(
|
||||
reverse: true,
|
||||
itemScrollController: _itemScrollController,
|
||||
@@ -402,5 +412,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,17 +110,27 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user