Skip to content

Commit 419e17b

Browse files
PIG208chrisbobbe
andcommitted
msglist: Support retrieving failed outbox message content
Different from the Figma design, the bottom padding below the progress bar is changed from 0.5px to 2px, as discussed here: #1453 (comment) Fixes: #1441 Co-authored-by: Chris Bobbe <[email protected]>
1 parent d56e20f commit 419e17b

18 files changed

+447
-38
lines changed

assets/l10n/app_en.arb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,9 @@
385385
"@discardDraftForEditConfirmationDialogMessage": {
386386
"description": "Message for a confirmation dialog for discarding message text that was typed into the compose box, when editing a message."
387387
},
388-
"discardDraftForMessageNotSentConfirmationDialogMessage": "When you restore a message not sent, the content that was previously in the compose box is discarded.",
389-
"@discardDraftForMessageNotSentConfirmationDialogMessage": {
390-
"description": "Message for a confirmation dialog when restoring a message not sent, for discarding message text that was typed into the compose box."
388+
"discardDraftForOutboxConfirmationDialogMessage": "When you restore an unsent message, the content that was previously in the compose box is discarded.",
389+
"@discardDraftForOutboxConfirmationDialogMessage": {
390+
"description": "Message for a confirmation dialog when restoring an outbox message, for discarding message text that was typed into the compose box."
391391
},
392392
"discardDraftConfirmationDialogConfirmButton": "Discard",
393393
"@discardDraftConfirmationDialogConfirmButton": {

lib/generated/l10n/zulip_localizations.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -655,11 +655,11 @@ abstract class ZulipLocalizations {
655655
/// **'When you edit a message, the content that was previously in the compose box is discarded.'**
656656
String get discardDraftForEditConfirmationDialogMessage;
657657

658-
/// Message for a confirmation dialog when restoring a message not sent, for discarding message text that was typed into the compose box.
658+
/// Message for a confirmation dialog when restoring an outbox message, for discarding message text that was typed into the compose box.
659659
///
660660
/// In en, this message translates to:
661-
/// **'When you restore a message not sent, the content that was previously in the compose box is discarded.'**
662-
String get discardDraftForMessageNotSentConfirmationDialogMessage;
661+
/// **'When you restore an unsent message, the content that was previously in the compose box is discarded.'**
662+
String get discardDraftForOutboxConfirmationDialogMessage;
663663

664664
/// Label for the 'Discard' button on a confirmation dialog for discarding message text that was typed into the compose box.
665665
///

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
325325
'When you edit a message, the content that was previously in the compose box is discarded.';
326326

327327
@override
328-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
329-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
328+
String get discardDraftForOutboxConfirmationDialogMessage =>
329+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
330330

331331
@override
332332
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/generated/l10n/zulip_localizations_de.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
325325
'When you edit a message, the content that was previously in the compose box is discarded.';
326326

327327
@override
328-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
329-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
328+
String get discardDraftForOutboxConfirmationDialogMessage =>
329+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
330330

331331
@override
332332
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
325325
'When you edit a message, the content that was previously in the compose box is discarded.';
326326

327327
@override
328-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
329-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
328+
String get discardDraftForOutboxConfirmationDialogMessage =>
329+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
330330

331331
@override
332332
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
325325
'When you edit a message, the content that was previously in the compose box is discarded.';
326326

327327
@override
328-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
329-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
328+
String get discardDraftForOutboxConfirmationDialogMessage =>
329+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
330330

331331
@override
332332
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
325325
'When you edit a message, the content that was previously in the compose box is discarded.';
326326

327327
@override
328-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
329-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
328+
String get discardDraftForOutboxConfirmationDialogMessage =>
329+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
330330

331331
@override
332332
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
332332
'Miej na uwadze, że przechodząc do zmiany wiadomości wyczyścisz okno nowej wiadomości.';
333333

334334
@override
335-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
336-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
335+
String get discardDraftForOutboxConfirmationDialogMessage =>
336+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
337337

338338
@override
339339
String get discardDraftConfirmationDialogConfirmButton => 'Odrzuć';

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
333333
'При изменении сообщения текст из поля для редактирования удаляется.';
334334

335335
@override
336-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
337-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
336+
String get discardDraftForOutboxConfirmationDialogMessage =>
337+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
338338

339339
@override
340340
String get discardDraftConfirmationDialogConfirmButton => 'Сбросить';

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
325325
'When you edit a message, the content that was previously in the compose box is discarded.';
326326

327327
@override
328-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
329-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
328+
String get discardDraftForOutboxConfirmationDialogMessage =>
329+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
330330

331331
@override
332332
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/generated/l10n/zulip_localizations_uk.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
334334
'When you edit a message, the content that was previously in the compose box is discarded.';
335335

336336
@override
337-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
338-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
337+
String get discardDraftForOutboxConfirmationDialogMessage =>
338+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
339339

340340
@override
341341
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/generated/l10n/zulip_localizations_zh.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
325325
'When you edit a message, the content that was previously in the compose box is discarded.';
326326

327327
@override
328-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
329-
'When you restore a message not sent, the content that was previously in the compose box is discarded.';
328+
String get discardDraftForOutboxConfirmationDialogMessage =>
329+
'When you restore an unsent message, the content that was previously in the compose box is discarded.';
330330

331331
@override
332332
String get discardDraftConfirmationDialogConfirmButton => 'Discard';

lib/model/message.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -881,9 +881,8 @@ mixin _OutboxMessageStore on PerAccountStoreBase {
881881
void _handleMessageEventOutbox(MessageEvent event) {
882882
if (event.localMessageId != null) {
883883
final localMessageId = int.parse(event.localMessageId!, radix: 10);
884-
// The outbox message can be missing if the user removes it (to be
885-
// implemented in #1441) before the event arrives.
886-
// Nothing to do in that case.
884+
// The outbox message can be missing if the user removes it before the
885+
// event arrives. Nothing to do in that case.
887886
_outboxMessages.remove(localMessageId);
888887
_outboxMessageDebounceTimers.remove(localMessageId)?.cancel();
889888
_outboxMessageWaitPeriodTimers.remove(localMessageId)?.cancel();

lib/widgets/compose_box.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../api/route/messages.dart';
1313
import '../generated/l10n/zulip_localizations.dart';
1414
import '../model/binding.dart';
1515
import '../model/compose.dart';
16+
import '../model/message.dart';
1617
import '../model/narrow.dart';
1718
import '../model/store.dart';
1819
import 'actions.dart';
@@ -1818,6 +1819,16 @@ class ComposeBox extends StatefulWidget {
18181819
abstract class ComposeBoxState extends State<ComposeBox> {
18191820
ComposeBoxController get controller;
18201821

1822+
/// Fills the compose box with the content of an [OutboxMessage]
1823+
/// for a failed [sendMessage] request.
1824+
///
1825+
/// If there is already text in the compose box, gives a confirmation dialog
1826+
/// to confirm that it is OK to discard that text.
1827+
///
1828+
/// [localMessageId], as in [OutboxMessage.localMessageId], must be present
1829+
/// in the message store.
1830+
void restoreMessageNotSent(int localMessageId);
1831+
18211832
/// Switch the compose box to editing mode.
18221833
///
18231834
/// If there is already text in the compose box, gives a confirmation dialog
@@ -1839,6 +1850,29 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
18391850
@override ComposeBoxController get controller => _controller!;
18401851
ComposeBoxController? _controller;
18411852

1853+
@override
1854+
void restoreMessageNotSent(int localMessageId) async {
1855+
final zulipLocalizations = ZulipLocalizations.of(context);
1856+
1857+
final abort = await _abortBecauseContentInputNotEmpty(
1858+
dialogMessage: zulipLocalizations.discardDraftForOutboxConfirmationDialogMessage);
1859+
if (abort || !mounted) return;
1860+
1861+
final store = PerAccountStoreWidget.of(context);
1862+
final outboxMessage = store.takeOutboxMessage(localMessageId);
1863+
setState(() {
1864+
_setNewController(store);
1865+
final controller = this.controller;
1866+
controller
1867+
..content.value = TextEditingValue(text: outboxMessage.contentMarkdown)
1868+
..contentFocusNode.requestFocus();
1869+
if (controller is StreamComposeBoxController) {
1870+
controller.topic.setTopic(
1871+
(outboxMessage.conversation as StreamConversation).topic);
1872+
}
1873+
});
1874+
}
1875+
18421876
@override
18431877
void startEditInteraction(int messageId) async {
18441878
final zulipLocalizations = ZulipLocalizations.of(context);
@@ -1920,7 +1954,7 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
19201954
if (!mounted) return;
19211955
if (!identical(controller, emptyEditController)) {
19221956
// During the fetch-raw-content request, the user tapped Cancel
1923-
// or tapped a failed message edit to restore.
1957+
// or tapped a failed message edit or failed outbox message to restore.
19241958
// TODO in this case we don't want the error dialog caused by
19251959
// ZulipAction.fetchRawContentWithFeedback; suppress that
19261960
return;

lib/widgets/message_list.dart

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:intl/intl.dart' hide TextDirection;
55

66
import '../api/model/model.dart';
77
import '../generated/l10n/zulip_localizations.dart';
8+
import '../model/message.dart';
89
import '../model/message_list.dart';
910
import '../model/narrow.dart';
1011
import '../model/store.dart';
@@ -1667,19 +1668,114 @@ class OutboxMessageWithPossibleSender extends StatelessWidget {
16671668
@override
16681669
Widget build(BuildContext context) {
16691670
final message = item.message;
1671+
final localMessageId = message.localMessageId;
1672+
final state = item.message.state;
1673+
1674+
// This is adapted from [MessageContent].
1675+
// TODO(#576): Offer InheritedMessage ancestor once we are ready
1676+
// to support local echoing images and lightbox.
1677+
Widget content = DefaultTextStyle(
1678+
style: ContentTheme.of(context).textStylePlainParagraph,
1679+
child: BlockContentList(nodes: item.content.nodes));
1680+
1681+
switch (state) {
1682+
case OutboxMessageState.hidden:
1683+
throw StateError('Hidden OutboxMessage messages should not appear in message lists');
1684+
case OutboxMessageState.waiting:
1685+
break;
1686+
case OutboxMessageState.failed:
1687+
case OutboxMessageState.waitPeriodExpired:
1688+
// TODO(#576): When we support rendered-content local echo,
1689+
// use IgnorePointer along with this faded appearance,
1690+
// like we do for the failed-message-edit state
1691+
content = _RestoreOutboxMessageGestureDetector(
1692+
localMessageId: localMessageId,
1693+
child: Opacity(opacity: 0.6, child: content));
1694+
}
1695+
16701696
return Padding(
1671-
padding: const EdgeInsets.symmetric(vertical: 4),
1697+
padding: const EdgeInsets.only(top: 4),
16721698
child: Column(children: [
16731699
if (item.showSender)
16741700
_SenderRow(message: message, showTimestamp: false),
16751701
Padding(
16761702
padding: const EdgeInsets.symmetric(horizontal: 16),
1677-
// This is adapted from [MessageContent].
1678-
// TODO(#576): Offer InheritedMessage ancestor once we are ready
1679-
// to support local echoing images and lightbox.
1680-
child: DefaultTextStyle(
1681-
style: ContentTheme.of(context).textStylePlainParagraph,
1682-
child: BlockContentList(nodes: item.content.nodes))),
1703+
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch,
1704+
children: [
1705+
content,
1706+
_OutboxMessageStatusRow(
1707+
localMessageId: localMessageId, outboxMessageState: state),
1708+
])),
16831709
]));
16841710
}
16851711
}
1712+
1713+
class _OutboxMessageStatusRow extends StatelessWidget {
1714+
const _OutboxMessageStatusRow({
1715+
required this.localMessageId,
1716+
required this.outboxMessageState,
1717+
});
1718+
1719+
final int localMessageId;
1720+
final OutboxMessageState outboxMessageState;
1721+
1722+
@override
1723+
Widget build(BuildContext context) {
1724+
switch (outboxMessageState) {
1725+
case OutboxMessageState.hidden:
1726+
assert(false,
1727+
'Hidden OutboxMessage messages should not appear in message lists');
1728+
return SizedBox.shrink();
1729+
1730+
case OutboxMessageState.waiting:
1731+
final designVariables = DesignVariables.of(context);
1732+
return Padding(
1733+
padding: const EdgeInsetsGeometry.only(bottom: 2),
1734+
child: LinearProgressIndicator(
1735+
minHeight: 2,
1736+
color: designVariables.foreground.withFadedAlpha(0.5),
1737+
backgroundColor: designVariables.foreground.withFadedAlpha(0.2)));
1738+
1739+
case OutboxMessageState.failed:
1740+
case OutboxMessageState.waitPeriodExpired:
1741+
final designVariables = DesignVariables.of(context);
1742+
final zulipLocalizations = ZulipLocalizations.of(context);
1743+
return Padding(
1744+
padding: const EdgeInsets.only(bottom: 4),
1745+
child: _RestoreOutboxMessageGestureDetector(
1746+
localMessageId: localMessageId,
1747+
child: Text(
1748+
zulipLocalizations.messageNotSentLabel,
1749+
textAlign: TextAlign.end,
1750+
style: TextStyle(
1751+
color: designVariables.btnLabelAttLowIntDanger,
1752+
fontSize: 12,
1753+
height: 12 / 12,
1754+
letterSpacing: proportionalLetterSpacing(
1755+
context, 0.05, baseFontSize: 12)))));
1756+
}
1757+
}
1758+
}
1759+
1760+
class _RestoreOutboxMessageGestureDetector extends StatelessWidget {
1761+
const _RestoreOutboxMessageGestureDetector({
1762+
required this.localMessageId,
1763+
required this.child,
1764+
});
1765+
1766+
final int localMessageId;
1767+
final Widget child;
1768+
1769+
@override
1770+
Widget build(BuildContext context) {
1771+
return GestureDetector(
1772+
behavior: HitTestBehavior.opaque,
1773+
onTap: () {
1774+
final composeBoxState = MessageListPage.ancestorOf(context).composeBoxState;
1775+
// TODO(#1518) allow restore-outbox-message from any message-list page
1776+
if (composeBoxState == null) return;
1777+
composeBoxState.restoreMessageNotSent(localMessageId);
1778+
},
1779+
child: child);
1780+
}
1781+
}

test/widgets/compose_box_checks.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ extension ComposeBoxControllerChecks on Subject<ComposeBoxController> {
1111
Subject<FocusNode> get contentFocusNode => has((c) => c.contentFocusNode, 'contentFocusNode');
1212
}
1313

14+
extension StreamComposeBoxControllerChecks on Subject<StreamComposeBoxController> {
15+
Subject<ComposeTopicController> get topic => has((c) => c.topic, 'topic');
16+
}
17+
1418
extension EditMessageComposeBoxControllerChecks on Subject<EditMessageComposeBoxController> {
1519
Subject<int> get messageId => has((c) => c.messageId, 'messageId');
1620
Subject<String?> get originalRawContent => has((c) => c.originalRawContent, 'originalRawContent');

0 commit comments

Comments
 (0)