Skip to content

Commit a6e4e28

Browse files
committed
compose: Show send message error with dismissable error banners
Different from the Figma, we use a sharp border instead of a rounded one, because most of the time (for error messages of a single line) the 40x40 button is attached to the top/bottom/right border, and a rounded border would leave some blank space at the corners. We also align the button to the top when we soft wrap the error message; centering it leaves some awkward vertical padding. Apart from that, when the action button is not available (e.g. _ErrorBanner that replaces the compose box in narrows without post permission), the padding around the label is adjusted to center it. Fixes: zulip#720 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 5b9bb6e commit a6e4e28

File tree

2 files changed

+43
-12
lines changed

2 files changed

+43
-12
lines changed

lib/widgets/compose_box.dart

+6-3
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,7 @@ class _SendButtonState extends State<_SendButton> {
10041004
.sendMessage(destination: widget.getDestination(), content: content)
10051005
.timeout(kSendMessageTimeout);
10061006
widget.controller.content.clear();
1007+
widget.controller._sendMessageError.value = null;
10071008
} catch (e) {
10081009
if (!mounted) return;
10091010
final zulipLocalizations = ZulipLocalizations.of(context);
@@ -1014,9 +1015,7 @@ class _SendButtonState extends State<_SendButton> {
10141015
case TimeoutException(): message = zulipLocalizations.errorSendMessageTimeout;
10151016
default: rethrow;
10161017
}
1017-
showErrorDialog(context: context,
1018-
title: zulipLocalizations.errorMessageNotSent,
1019-
message: message);
1018+
widget.controller._sendMessageError.value = message;
10201019
return;
10211020
} finally {
10221021
widget.controller._enabled.value = true;
@@ -1244,11 +1243,15 @@ sealed class ComposeBoxController {
12441243
bool get enabled => _enabled.value;
12451244
final ValueNotifier<bool> _enabled = ValueNotifier<bool>(true);
12461245

1246+
String? get sendMessageError => _sendMessageError.value;
1247+
final ValueNotifier<String?> _sendMessageError = ValueNotifier<String?>(null);
1248+
12471249
@mustCallSuper
12481250
void dispose() {
12491251
content.dispose();
12501252
contentFocusNode.dispose();
12511253
_enabled.dispose();
1254+
_sendMessageError.dispose();
12521255
}
12531256
}
12541257

test/widgets/compose_box_test.dart

+37-9
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import '../model/binding.dart';
3030
import '../model/test_store.dart';
3131
import '../model/typing_status_test.dart';
3232
import '../stdlib_checks.dart';
33-
import 'dialog_checks.dart';
3433
import 'test_app.dart';
3534

3635
void main() {
@@ -480,6 +479,39 @@ void main() {
480479
check(find.byType(LinearProgressIndicator)).findsNothing();
481480
});
482481

482+
testWidgets('dismiss validation error banner by tapping the remove icon', (tester) async {
483+
await setupAndTapSend(tester, prepareResponse: (_) {
484+
return connection.prepare(httpStatus: 400,
485+
json: {'result': 'error', 'code': 'BAD_REQUEST', 'msg': 'error'});
486+
});
487+
check(find.byIcon(ZulipIcons.remove)).findsOne();
488+
489+
await tester.tap(find.byIcon(ZulipIcons.remove));
490+
await tester.pump();
491+
check(find.byIcon(ZulipIcons.remove)).findsNothing();
492+
});
493+
494+
testWidgets('dismiss error banner after a successful request', (tester) async {
495+
await setupAndTapSend(tester, prepareResponse: (_) {
496+
return connection.prepare(httpStatus: 400,
497+
json: {'result': 'error', 'code': 'BAD_REQUEST', 'msg': 'error'});
498+
});
499+
check(find.byIcon(ZulipIcons.remove)).findsOne();
500+
501+
await tester.enterText(contentInputFinder, 'hello world');
502+
check(find.byIcon(ZulipIcons.remove)).findsOne();
503+
504+
connection.prepare(
505+
json: SendMessageResult(id: 123).toJson(),
506+
delay: const Duration(seconds: 2));
507+
await tester.tap(find.byIcon(ZulipIcons.send));
508+
await tester.pump();
509+
check(find.byIcon(ZulipIcons.remove)).findsOne();
510+
511+
await tester.pump(const Duration(seconds: 2));
512+
check(find.byIcon(ZulipIcons.remove)).findsNothing();
513+
});
514+
483515
testWidgets('fail after timeout', (tester) async {
484516
const longDelay = Duration(hours: 1);
485517
assert(longDelay > kSendMessageTimeout);
@@ -492,9 +524,7 @@ void main() {
492524

493525
await tester.pump(kSendMessageTimeout);
494526
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
495-
await tester.tap(find.byWidget(checkErrorDialog(tester,
496-
expectedTitle: zulipLocalizations.errorMessageNotSent,
497-
expectedMessage: zulipLocalizations.errorSendMessageTimeout)));
527+
check(find.text(zulipLocalizations.errorSendMessageTimeout)).findsOne();
498528

499529
await tester.pump(longDelay);
500530
});
@@ -510,11 +540,9 @@ void main() {
510540
});
511541
});
512542
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
513-
await tester.tap(find.byWidget(checkErrorDialog(tester,
514-
expectedTitle: zulipLocalizations.errorMessageNotSent,
515-
expectedMessage: zulipLocalizations.errorServerMessage(
516-
'You do not have permission to initiate direct message conversations.'),
517-
)));
543+
check(find.text(zulipLocalizations.errorServerMessage(
544+
'You do not have permission to initiate direct message conversations.'),
545+
)).findsOne();
518546
});
519547
});
520548

0 commit comments

Comments
 (0)