@@ -275,17 +275,65 @@ class ComposeContentController extends ComposeController<ContentValidationError>
275
275
}
276
276
}
277
277
278
- class _TopBar extends StatelessWidget {
279
- const _TopBar ({required this .showProgressIndicator});
278
+ class SendErrorController extends ChangeNotifier {
279
+ SendErrorController ();
280
+
281
+ Iterable <String > get errors => _errors;
282
+ Set <String > _errors = {};
283
+ set errors (Iterable <String > value) {
284
+ _errors = Set .from (value);
285
+ assert (_errors.length == value.length, 'There should not be duplicate errors' );
286
+ notifyListeners ();
287
+ }
288
+
289
+ void remove (String value) {
290
+ final removed = _errors.remove (value);
291
+ assert (removed);
292
+ notifyListeners ();
293
+ }
294
+ }
295
+
296
+ class _TopBar extends StatefulWidget {
297
+ const _TopBar ({required this .showProgressIndicator, required this .sendErrorController});
280
298
281
299
final bool showProgressIndicator;
300
+ final SendErrorController sendErrorController;
301
+
302
+ @override
303
+ State <_TopBar > createState () => _TopBarState ();
304
+ }
305
+
306
+ class _TopBarState extends State <_TopBar > {
307
+ @override
308
+ void initState () {
309
+ super .initState ();
310
+ widget.sendErrorController.addListener (_sendErrorChanged);
311
+ }
312
+
313
+ @override
314
+ void dispose () {
315
+ widget.sendErrorController.removeListener (_sendErrorChanged);
316
+ super .dispose ();
317
+ }
318
+
319
+ void _sendErrorChanged () {
320
+ setState (() {
321
+ // The actual state lives in `widget.sendErrorController`.
322
+ });
323
+ }
282
324
283
325
@override
284
326
Widget build (BuildContext context) {
327
+ final designVariables = DesignVariables .of (context);
285
328
// TODO: Figure out a way so that this does not shift the message list
286
329
// when it gains more height.
287
330
return Column (children: [
288
- if (showProgressIndicator) _progressIndicator (context),
331
+ if (widget.showProgressIndicator) _progressIndicator (context),
332
+ for (final value in widget.sendErrorController.errors)
333
+ _ErrorBanner (label: value,
334
+ action: IconButton (icon: Icon (
335
+ ZulipIcons .remove, color: designVariables.btnLabelAttLowIntDanger),
336
+ onPressed: () => widget.sendErrorController.remove (value))),
289
337
]);
290
338
}
291
339
}
@@ -968,12 +1016,14 @@ class _SendButton extends StatefulWidget {
968
1016
required this .enabled,
969
1017
required this .topicController,
970
1018
required this .contentController,
1019
+ required this .sendErrorController,
971
1020
required this .getDestination,
972
1021
});
973
1022
974
1023
final ValueNotifier <bool > enabled;
975
1024
final ComposeTopicController ? topicController;
976
1025
final ComposeContentController contentController;
1026
+ final SendErrorController sendErrorController;
977
1027
final MessageDestination Function () getDestination;
978
1028
979
1029
@override
@@ -1029,10 +1079,7 @@ class _SendButtonState extends State<_SendButton> {
1029
1079
for (final error in widget.contentController.validationErrors)
1030
1080
error.message (zulipLocalizations),
1031
1081
];
1032
- showErrorDialog (
1033
- context: context,
1034
- title: zulipLocalizations.errorMessageNotSent,
1035
- message: validationErrorMessages.join ('\n\n ' ));
1082
+ widget.sendErrorController.errors = validationErrorMessages;
1036
1083
return ;
1037
1084
}
1038
1085
if (! widget.enabled.value) {
@@ -1053,6 +1100,7 @@ class _SendButtonState extends State<_SendButton> {
1053
1100
.sendMessage (destination: widget.getDestination (), content: content)
1054
1101
.timeout (kSendMessageTimeout);
1055
1102
widget.contentController.clear ();
1103
+ widget.sendErrorController.errors = [];
1056
1104
} catch (e) {
1057
1105
if (! mounted) return ;
1058
1106
@@ -1064,9 +1112,7 @@ class _SendButtonState extends State<_SendButton> {
1064
1112
case ApiRequestException (): message = e.message;
1065
1113
default : rethrow ;
1066
1114
}
1067
- showErrorDialog (context: context,
1068
- title: zulipLocalizations.errorMessageNotSent,
1069
- message: message);
1115
+ widget.sendErrorController.errors = [message];
1070
1116
return ;
1071
1117
} finally {
1072
1118
widget.enabled.value = true ;
@@ -1196,6 +1242,7 @@ abstract class ComposeBoxController<T extends StatefulWidget> extends State<T> {
1196
1242
bool get enabled;
1197
1243
ComposeTopicController ? get topicController;
1198
1244
ComposeContentController get contentController;
1245
+ SendErrorController get sendErrorController;
1199
1246
FocusNode get contentFocusNode;
1200
1247
}
1201
1248
@@ -1229,6 +1276,9 @@ class _StreamComposeBoxState extends State<_StreamComposeBox> implements Compose
1229
1276
FocusNode get topicFocusNode => _topicFocusNode;
1230
1277
final _topicFocusNode = FocusNode ();
1231
1278
1279
+ @override SendErrorController get sendErrorController => _sendErrorController;
1280
+ final _sendErrorController = SendErrorController ();
1281
+
1232
1282
@override
1233
1283
void initState () {
1234
1284
super .initState ();
@@ -1241,6 +1291,7 @@ class _StreamComposeBoxState extends State<_StreamComposeBox> implements Compose
1241
1291
_topicController.dispose ();
1242
1292
_contentController.dispose ();
1243
1293
_contentFocusNode.dispose ();
1294
+ _sendErrorController.dispose ();
1244
1295
super .dispose ();
1245
1296
}
1246
1297
@@ -1253,7 +1304,10 @@ class _StreamComposeBoxState extends State<_StreamComposeBox> implements Compose
1253
1304
@override
1254
1305
Widget build (BuildContext context) {
1255
1306
return _ComposeBoxLayout (
1256
- topBar: _TopBar (showProgressIndicator: ! enabled),
1307
+ topBar: _TopBar (
1308
+ showProgressIndicator: ! enabled,
1309
+ sendErrorController: sendErrorController,
1310
+ ),
1257
1311
topicInput: _TopicInput (
1258
1312
enabled: enabled,
1259
1313
streamId: widget.narrow.streamId,
@@ -1277,16 +1331,18 @@ class _StreamComposeBoxState extends State<_StreamComposeBox> implements Compose
1277
1331
enabled: _enabled,
1278
1332
topicController: _topicController,
1279
1333
contentController: _contentController,
1334
+ sendErrorController: _sendErrorController,
1280
1335
getDestination: () => StreamDestination (
1281
1336
widget.narrow.streamId, _topicController.textNormalized),
1282
1337
));
1283
1338
}
1284
1339
}
1285
1340
1286
1341
class _ErrorBanner extends StatelessWidget {
1287
- const _ErrorBanner ({required this .label});
1342
+ const _ErrorBanner ({required this .label, this .action });
1288
1343
1289
1344
final String label;
1345
+ final Widget ? action;
1290
1346
1291
1347
@override
1292
1348
Widget build (BuildContext context) {
@@ -1298,6 +1354,11 @@ class _ErrorBanner extends StatelessWidget {
1298
1354
// which is a variable equivalent to this value.
1299
1355
wght: 600 ));
1300
1356
1357
+ final padding = (action == null )
1358
+ // Ensure that the text is centered when it is the only element.
1359
+ ? const EdgeInsets .symmetric (horizontal: 16 , vertical: 5 )
1360
+ : const EdgeInsetsDirectional .fromSTEB (16 , 5 , 8 , 5 );
1361
+
1301
1362
return Container (
1302
1363
constraints: const BoxConstraints (minHeight: 40 ),
1303
1364
decoration: BoxDecoration (
@@ -1307,8 +1368,9 @@ class _ErrorBanner extends StatelessWidget {
1307
1368
children: [
1308
1369
Expanded (
1309
1370
child: Padding (
1310
- padding: const EdgeInsets . symmetric (horizontal : 16 , vertical : 5 ) ,
1371
+ padding: padding ,
1311
1372
child: Text (label, style: labelTextStyle))),
1373
+ if (action != null ) action! ,
1312
1374
]));
1313
1375
}
1314
1376
}
@@ -1334,6 +1396,9 @@ class _FixedDestinationComposeBoxState extends State<_FixedDestinationComposeBox
1334
1396
@override FocusNode get contentFocusNode => _contentFocusNode;
1335
1397
final _contentFocusNode = FocusNode ();
1336
1398
1399
+ @override SendErrorController get sendErrorController => _sendErrorController;
1400
+ final _sendErrorController = SendErrorController ();
1401
+
1337
1402
@override
1338
1403
void initState () {
1339
1404
super .initState ();
@@ -1345,6 +1410,7 @@ class _FixedDestinationComposeBoxState extends State<_FixedDestinationComposeBox
1345
1410
_enabled.dispose ();
1346
1411
_contentController.dispose ();
1347
1412
_contentFocusNode.dispose ();
1413
+ _sendErrorController.dispose ();
1348
1414
super .dispose ();
1349
1415
}
1350
1416
@@ -1357,7 +1423,10 @@ class _FixedDestinationComposeBoxState extends State<_FixedDestinationComposeBox
1357
1423
@override
1358
1424
Widget build (BuildContext context) {
1359
1425
return _ComposeBoxLayout (
1360
- topBar: _TopBar (showProgressIndicator: ! enabled),
1426
+ topBar: _TopBar (
1427
+ showProgressIndicator: ! enabled,
1428
+ sendErrorController: sendErrorController,
1429
+ ),
1361
1430
topicInput: null ,
1362
1431
contentInput: _FixedDestinationContentInput (
1363
1432
enabled: enabled,
@@ -1374,6 +1443,7 @@ class _FixedDestinationComposeBoxState extends State<_FixedDestinationComposeBox
1374
1443
enabled: _enabled,
1375
1444
topicController: null ,
1376
1445
contentController: _contentController,
1446
+ sendErrorController: _sendErrorController,
1377
1447
getDestination: () => widget.narrow.destination,
1378
1448
));
1379
1449
}
0 commit comments