Skip to content

Commit 64cdc7d

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: zulip#1453 (comment) Fixes: zulip#1441 Co-authored-by: Chris Bobbe <[email protected]>
1 parent 1aeb0b3 commit 64cdc7d

20 files changed

+446
-46
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": {

assets/l10n/app_pl.arb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,10 +1113,6 @@
11131113
"@messageNotSentLabel": {
11141114
"description": "Text on a message in the message list saying that a send message request failed. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)"
11151115
},
1116-
"discardDraftForMessageNotSentConfirmationDialogMessage": "Odzyskanie wiadomości, która nie została wysłana, skutkuje wyczyszczeniem zawartości pola dodania wpisu.",
1117-
"@discardDraftForMessageNotSentConfirmationDialogMessage": {
1118-
"description": "Message for a confirmation dialog when restoring a message not sent, for discarding message text that was typed into the compose box."
1119-
},
11201116
"errorNotificationOpenAccountNotFound": "Nie odnaleziono konta powiązanego z tym powiadomieniem.",
11211117
"@errorNotificationOpenAccountNotFound": {
11221118
"description": "Error message when the account associated with the notification could not be found"

assets/l10n/app_ru.arb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,10 +1105,6 @@
11051105
"@newDmFabButtonLabel": {
11061106
"description": "Label for the floating action button (FAB) that opens the new DM sheet."
11071107
},
1108-
"discardDraftForMessageNotSentConfirmationDialogMessage": "При восстановлении неотправленного сообщения текст в поле ввода текста будет утрачен.",
1109-
"@discardDraftForMessageNotSentConfirmationDialogMessage": {
1110-
"description": "Message for a confirmation dialog when restoring a message not sent, for discarding message text that was typed into the compose box."
1111-
},
11121108
"newDmSheetScreenTitle": "Новое ЛС",
11131109
"@newDmSheetScreenTitle": {
11141110
"description": "Title displayed at the top of the new DM screen."

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
@@ -333,8 +333,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
333333
'Miej na uwadze, że przechodząc do zmiany wiadomości wyczyścisz okno nowej wiadomości.';
334334

335335
@override
336-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
337-
'Odzyskanie wiadomości, która nie została wysłana, skutkuje wyczyszczeniem zawartości pola dodania wpisu.';
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 => 'Odrzuć';

lib/generated/l10n/zulip_localizations_ru.dart

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

336336
@override
337-
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
338-
'При восстановлении неотправленного сообщения текст в поле ввода текста будет утрачен.';
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 => 'Сбросить';

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: 102 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';
@@ -1675,19 +1676,113 @@ class OutboxMessageWithPossibleSender extends StatelessWidget {
16751676
@override
16761677
Widget build(BuildContext context) {
16771678
final message = item.message;
1679+
final localMessageId = message.localMessageId;
1680+
1681+
// This is adapted from [MessageContent].
1682+
// TODO(#576): Offer InheritedMessage ancestor once we are ready
1683+
// to support local echoing images and lightbox.
1684+
Widget content = DefaultTextStyle(
1685+
style: ContentTheme.of(context).textStylePlainParagraph,
1686+
child: BlockContentList(nodes: item.content.nodes));
1687+
1688+
switch (message.state) {
1689+
case OutboxMessageState.hidden:
1690+
throw StateError('Hidden OutboxMessage messages should not appear in message lists');
1691+
case OutboxMessageState.waiting:
1692+
break;
1693+
case OutboxMessageState.failed:
1694+
case OutboxMessageState.waitPeriodExpired:
1695+
// TODO(#576): When we support rendered-content local echo,
1696+
// use IgnorePointer along with this faded appearance,
1697+
// like we do for the failed-message-edit state
1698+
content = _RestoreOutboxMessageGestureDetector(
1699+
localMessageId: localMessageId,
1700+
child: Opacity(opacity: 0.6, child: content));
1701+
}
1702+
16781703
return Padding(
1679-
padding: const EdgeInsets.symmetric(vertical: 4),
1704+
padding: const EdgeInsets.only(top: 4),
16801705
child: Column(children: [
16811706
if (item.showSender)
16821707
_SenderRow(message: message, showTimestamp: false),
16831708
Padding(
16841709
padding: const EdgeInsets.symmetric(horizontal: 16),
1685-
// This is adapted from [MessageContent].
1686-
// TODO(#576): Offer InheritedMessage ancestor once we are ready
1687-
// to support local echoing images and lightbox.
1688-
child: DefaultTextStyle(
1689-
style: ContentTheme.of(context).textStylePlainParagraph,
1690-
child: BlockContentList(nodes: item.content.nodes))),
1710+
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch,
1711+
children: [
1712+
content,
1713+
_OutboxMessageStatusRow(
1714+
localMessageId: localMessageId, outboxMessageState: message.state),
1715+
])),
16911716
]));
16921717
}
16931718
}
1719+
1720+
class _OutboxMessageStatusRow extends StatelessWidget {
1721+
const _OutboxMessageStatusRow({
1722+
required this.localMessageId,
1723+
required this.outboxMessageState,
1724+
});
1725+
1726+
final int localMessageId;
1727+
final OutboxMessageState outboxMessageState;
1728+
1729+
@override
1730+
Widget build(BuildContext context) {
1731+
switch (outboxMessageState) {
1732+
case OutboxMessageState.hidden:
1733+
assert(false,
1734+
'Hidden OutboxMessage messages should not appear in message lists');
1735+
return SizedBox.shrink();
1736+
1737+
case OutboxMessageState.waiting:
1738+
final designVariables = DesignVariables.of(context);
1739+
return Padding(
1740+
padding: const EdgeInsetsGeometry.only(bottom: 2),
1741+
child: LinearProgressIndicator(
1742+
minHeight: 2,
1743+
color: designVariables.foreground.withFadedAlpha(0.5),
1744+
backgroundColor: designVariables.foreground.withFadedAlpha(0.2)));
1745+
1746+
case OutboxMessageState.failed:
1747+
case OutboxMessageState.waitPeriodExpired:
1748+
final designVariables = DesignVariables.of(context);
1749+
final zulipLocalizations = ZulipLocalizations.of(context);
1750+
return Padding(
1751+
padding: const EdgeInsets.only(bottom: 4),
1752+
child: _RestoreOutboxMessageGestureDetector(
1753+
localMessageId: localMessageId,
1754+
child: Text(
1755+
zulipLocalizations.messageNotSentLabel,
1756+
textAlign: TextAlign.end,
1757+
style: TextStyle(
1758+
color: designVariables.btnLabelAttLowIntDanger,
1759+
fontSize: 12,
1760+
height: 12 / 12,
1761+
letterSpacing: proportionalLetterSpacing(
1762+
context, 0.05, baseFontSize: 12)))));
1763+
}
1764+
}
1765+
}
1766+
1767+
class _RestoreOutboxMessageGestureDetector extends StatelessWidget {
1768+
const _RestoreOutboxMessageGestureDetector({
1769+
required this.localMessageId,
1770+
required this.child,
1771+
});
1772+
1773+
final int localMessageId;
1774+
final Widget child;
1775+
1776+
@override
1777+
Widget build(BuildContext context) {
1778+
return GestureDetector(
1779+
behavior: HitTestBehavior.opaque,
1780+
onTap: () {
1781+
final composeBoxState = MessageListPage.ancestorOf(context).composeBoxState;
1782+
// TODO(#1518) allow restore-outbox-message from any message-list page
1783+
if (composeBoxState == null) return;
1784+
composeBoxState.restoreMessageNotSent(localMessageId);
1785+
},
1786+
child: child);
1787+
}
1788+
}

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)