Skip to content

Commit 8c74297

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 fcc4da9 commit 8c74297

19 files changed

+457
-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';
@@ -1840,6 +1841,16 @@ class ComposeBox extends StatefulWidget {
18401841
abstract class ComposeBoxState extends State<ComposeBox> {
18411842
ComposeBoxController get controller;
18421843

1844+
/// Fills the compose box with the content of an [OutboxMessage]
1845+
/// for a failed [sendMessage] request.
1846+
///
1847+
/// If there is already text in the compose box, gives a confirmation dialog
1848+
/// to confirm that it is OK to discard that text.
1849+
///
1850+
/// [localMessageId], as in [OutboxMessage.localMessageId], must be present
1851+
/// in the message store.
1852+
void restoreMessageNotSent(int localMessageId);
1853+
18431854
/// Switch the compose box to editing mode.
18441855
///
18451856
/// If there is already text in the compose box, gives a confirmation dialog
@@ -1861,6 +1872,29 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
18611872
@override ComposeBoxController get controller => _controller!;
18621873
ComposeBoxController? _controller;
18631874

1875+
@override
1876+
void restoreMessageNotSent(int localMessageId) async {
1877+
final zulipLocalizations = ZulipLocalizations.of(context);
1878+
1879+
final abort = await _abortBecauseContentInputNotEmpty(
1880+
dialogMessage: zulipLocalizations.discardDraftForOutboxConfirmationDialogMessage);
1881+
if (abort || !mounted) return;
1882+
1883+
final store = PerAccountStoreWidget.of(context);
1884+
final outboxMessage = store.takeOutboxMessage(localMessageId);
1885+
setState(() {
1886+
_setNewController(store);
1887+
final controller = this.controller;
1888+
controller
1889+
..content.value = TextEditingValue(text: outboxMessage.contentMarkdown)
1890+
..contentFocusNode.requestFocus();
1891+
if (controller is StreamComposeBoxController) {
1892+
controller.topic.setTopic(
1893+
(outboxMessage.conversation as StreamConversation).topic);
1894+
}
1895+
});
1896+
}
1897+
18641898
@override
18651899
void startEditInteraction(int messageId) async {
18661900
final zulipLocalizations = ZulipLocalizations.of(context);
@@ -1942,7 +1976,7 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
19421976
if (!mounted) return;
19431977
if (!identical(controller, emptyEditController)) {
19441978
// During the fetch-raw-content request, the user tapped Cancel
1945-
// or tapped a failed message edit to restore.
1979+
// or tapped a failed message edit or failed outbox message to restore.
19461980
// TODO in this case we don't want the error dialog caused by
19471981
// ZulipAction.fetchRawContentWithFeedback; suppress that
19481982
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';
@@ -1748,19 +1749,113 @@ class OutboxMessageWithPossibleSender extends StatelessWidget {
17481749
@override
17491750
Widget build(BuildContext context) {
17501751
final message = item.message;
1752+
final localMessageId = message.localMessageId;
1753+
1754+
// This is adapted from [MessageContent].
1755+
// TODO(#576): Offer InheritedMessage ancestor once we are ready
1756+
// to support local echoing images and lightbox.
1757+
Widget content = DefaultTextStyle(
1758+
style: ContentTheme.of(context).textStylePlainParagraph,
1759+
child: BlockContentList(nodes: item.content.nodes));
1760+
1761+
switch (message.state) {
1762+
case OutboxMessageState.hidden:
1763+
throw StateError('Hidden OutboxMessage messages should not appear in message lists');
1764+
case OutboxMessageState.waiting:
1765+
break;
1766+
case OutboxMessageState.failed:
1767+
case OutboxMessageState.waitPeriodExpired:
1768+
// TODO(#576): When we support rendered-content local echo,
1769+
// use IgnorePointer along with this faded appearance,
1770+
// like we do for the failed-message-edit state
1771+
content = _RestoreOutboxMessageGestureDetector(
1772+
localMessageId: localMessageId,
1773+
child: Opacity(opacity: 0.6, child: content));
1774+
}
1775+
17511776
return Padding(
1752-
padding: const EdgeInsets.symmetric(vertical: 4),
1777+
padding: const EdgeInsets.only(top: 4),
17531778
child: Column(children: [
17541779
if (item.showSender)
17551780
_SenderRow(message: message, showTimestamp: false),
17561781
Padding(
17571782
padding: const EdgeInsets.symmetric(horizontal: 16),
1758-
// This is adapted from [MessageContent].
1759-
// TODO(#576): Offer InheritedMessage ancestor once we are ready
1760-
// to support local echoing images and lightbox.
1761-
child: DefaultTextStyle(
1762-
style: ContentTheme.of(context).textStylePlainParagraph,
1763-
child: BlockContentList(nodes: item.content.nodes))),
1783+
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch,
1784+
children: [
1785+
content,
1786+
_OutboxMessageStatusRow(
1787+
localMessageId: localMessageId, outboxMessageState: message.state),
1788+
])),
17641789
]));
17651790
}
17661791
}
1792+
1793+
class _OutboxMessageStatusRow extends StatelessWidget {
1794+
const _OutboxMessageStatusRow({
1795+
required this.localMessageId,
1796+
required this.outboxMessageState,
1797+
});
1798+
1799+
final int localMessageId;
1800+
final OutboxMessageState outboxMessageState;
1801+
1802+
@override
1803+
Widget build(BuildContext context) {
1804+
switch (outboxMessageState) {
1805+
case OutboxMessageState.hidden:
1806+
assert(false,
1807+
'Hidden OutboxMessage messages should not appear in message lists');
1808+
return SizedBox.shrink();
1809+
1810+
case OutboxMessageState.waiting:
1811+
final designVariables = DesignVariables.of(context);
1812+
return Padding(
1813+
padding: const EdgeInsetsGeometry.only(bottom: 2),
1814+
child: LinearProgressIndicator(
1815+
minHeight: 2,
1816+
color: designVariables.foreground.withFadedAlpha(0.5),
1817+
backgroundColor: designVariables.foreground.withFadedAlpha(0.2)));
1818+
1819+
case OutboxMessageState.failed:
1820+
case OutboxMessageState.waitPeriodExpired:
1821+
final designVariables = DesignVariables.of(context);
1822+
final zulipLocalizations = ZulipLocalizations.of(context);
1823+
return Padding(
1824+
padding: const EdgeInsets.only(bottom: 4),
1825+
child: _RestoreOutboxMessageGestureDetector(
1826+
localMessageId: localMessageId,
1827+
child: Text(
1828+
zulipLocalizations.messageNotSentLabel,
1829+
textAlign: TextAlign.end,
1830+
style: TextStyle(
1831+
color: designVariables.btnLabelAttLowIntDanger,
1832+
fontSize: 12,
1833+
height: 12 / 12,
1834+
letterSpacing: proportionalLetterSpacing(
1835+
context, 0.05, baseFontSize: 12)))));
1836+
}
1837+
}
1838+
}
1839+
1840+
class _RestoreOutboxMessageGestureDetector extends StatelessWidget {
1841+
const _RestoreOutboxMessageGestureDetector({
1842+
required this.localMessageId,
1843+
required this.child,
1844+
});
1845+
1846+
final int localMessageId;
1847+
final Widget child;
1848+
1849+
@override
1850+
Widget build(BuildContext context) {
1851+
return GestureDetector(
1852+
behavior: HitTestBehavior.opaque,
1853+
onTap: () {
1854+
final composeBoxState = MessageListPage.ancestorOf(context).composeBoxState;
1855+
// TODO(#1518) allow restore-outbox-message from any message-list page
1856+
if (composeBoxState == null) return;
1857+
composeBoxState.restoreMessageNotSent(localMessageId);
1858+
},
1859+
child: child);
1860+
}
1861+
}

0 commit comments

Comments
 (0)