Skip to content

local echo (7/7): Support simplified version of local echo #1453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,9 @@
"@discardDraftForEditConfirmationDialogMessage": {
"description": "Message for a confirmation dialog for discarding message text that was typed into the compose box, when editing a message."
},
"discardDraftForMessageNotSentConfirmationDialogMessage": "When you restore a message not sent, the content that was previously in the compose box is discarded.",
"@discardDraftForMessageNotSentConfirmationDialogMessage": {
"description": "Message for a confirmation dialog when restoring a message not sent, for discarding message text that was typed into the compose box."
"discardDraftForOutboxConfirmationDialogMessage": "When you restore an unsent message, the content that was previously in the compose box is discarded.",
"@discardDraftForOutboxConfirmationDialogMessage": {
"description": "Message for a confirmation dialog when restoring an outbox message, for discarding message text that was typed into the compose box."
},
"discardDraftConfirmationDialogConfirmButton": "Discard",
"@discardDraftConfirmationDialogConfirmButton": {
Expand Down
4 changes: 0 additions & 4 deletions assets/l10n/app_pl.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1113,10 +1113,6 @@
"@messageNotSentLabel": {
"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.)"
},
"discardDraftForMessageNotSentConfirmationDialogMessage": "Odzyskanie wiadomości, która nie została wysłana, skutkuje wyczyszczeniem zawartości pola dodania wpisu.",
"@discardDraftForMessageNotSentConfirmationDialogMessage": {
"description": "Message for a confirmation dialog when restoring a message not sent, for discarding message text that was typed into the compose box."
},
"errorNotificationOpenAccountNotFound": "Nie odnaleziono konta powiązanego z tym powiadomieniem.",
"@errorNotificationOpenAccountNotFound": {
"description": "Error message when the account associated with the notification could not be found"
Expand Down
4 changes: 0 additions & 4 deletions assets/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1105,10 +1105,6 @@
"@newDmFabButtonLabel": {
"description": "Label for the floating action button (FAB) that opens the new DM sheet."
},
"discardDraftForMessageNotSentConfirmationDialogMessage": "При восстановлении неотправленного сообщения текст в поле ввода текста будет утрачен.",
"@discardDraftForMessageNotSentConfirmationDialogMessage": {
"description": "Message for a confirmation dialog when restoring a message not sent, for discarding message text that was typed into the compose box."
},
"newDmSheetScreenTitle": "Новое ЛС",
"@newDmSheetScreenTitle": {
"description": "Title displayed at the top of the new DM screen."
Expand Down
6 changes: 3 additions & 3 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -655,11 +655,11 @@ abstract class ZulipLocalizations {
/// **'When you edit a message, the content that was previously in the compose box is discarded.'**
String get discardDraftForEditConfirmationDialogMessage;

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

/// Label for the 'Discard' button on a confirmation dialog for discarding message text that was typed into the compose box.
///
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_de.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
'Miej na uwadze, że przechodząc do zmiany wiadomości wyczyścisz okno nowej wiadomości.';

@override
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
'Odzyskanie wiadomości, która nie została wysłana, skutkuje wyczyszczeniem zawartości pola dodania wpisu.';
String get discardDraftForOutboxConfirmationDialogMessage =>
'When you restore an unsent message, the content that was previously in the compose box is discarded.';

@override
String get discardDraftConfirmationDialogConfirmButton => 'Odrzuć';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
'При изменении сообщения текст из поля для редактирования удаляется.';

@override
String get discardDraftForMessageNotSentConfirmationDialogMessage =>
'При восстановлении неотправленного сообщения текст в поле ввода текста будет утрачен.';
String get discardDraftForOutboxConfirmationDialogMessage =>
'When you restore an unsent message, the content that was previously in the compose box is discarded.';

@override
String get discardDraftConfirmationDialogConfirmButton => 'Сбросить';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_uk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_zh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
'When you edit a message, the content that was previously in the compose box is discarded.';

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

@override
String get discardDraftConfirmationDialogConfirmButton => 'Discard';
Expand Down
5 changes: 2 additions & 3 deletions lib/model/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -881,9 +881,8 @@ mixin _OutboxMessageStore on PerAccountStoreBase {
void _handleMessageEventOutbox(MessageEvent event) {
if (event.localMessageId != null) {
final localMessageId = int.parse(event.localMessageId!, radix: 10);
// The outbox message can be missing if the user removes it (to be
// implemented in #1441) before the event arrives.
// Nothing to do in that case.
// The outbox message can be missing if the user removes it before the
// event arrives. Nothing to do in that case.
_outboxMessages.remove(localMessageId);
_outboxMessageDebounceTimers.remove(localMessageId)?.cancel();
_outboxMessageWaitPeriodTimers.remove(localMessageId)?.cancel();
Expand Down
58 changes: 40 additions & 18 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import '../api/route/messages.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../model/binding.dart';
import '../model/compose.dart';
import '../model/message.dart';
import '../model/narrow.dart';
import '../model/store.dart';
import 'actions.dart';
Expand Down Expand Up @@ -1288,15 +1289,8 @@ class _SendButtonState extends State<_SendButton> {
final content = controller.content.textNormalized;

controller.content.clear();
// The following `stoppedComposing` call is currently redundant,
// because clearing input sends a "typing stopped" notice.
// It will be necessary once we resolve #720.
store.typingNotifier.stoppedComposing();

try {
// TODO(#720) clear content input only on success response;
// while waiting, put input(s) and send button into a disabled
// "working on it" state (letting input text be selected for copying).
await store.sendMessage(destination: widget.getDestination(), content: content);
} on ApiRequestException catch (e) {
if (!mounted) return;
Expand Down Expand Up @@ -1388,7 +1382,6 @@ class _ComposeBoxContainer extends StatelessWidget {
border: Border(top: BorderSide(color: designVariables.borderBar)),
boxShadow: ComposeBoxTheme.of(context).boxShadow,
),
// TODO(#720) try a Stack for the overlaid linear progress indicator
child: Material(
color: designVariables.composeBoxBg,
child: Column(
Expand Down Expand Up @@ -1746,10 +1739,10 @@ class _ErrorBanner extends _Banner {

@override
Widget? buildTrailing(context) {
// TODO(#720) "x" button goes here.
// 24px square with 8px touchable padding in all directions?
// and `bool get padEnd => false`; see Figma:
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=4031-17029&m=dev
// An "x" button can go here.
// 24px square with 8px touchable padding in all directions?
// and `bool get padEnd => false`; see Figma:
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=4031-17029&m=dev
return null;
}
}
Expand Down Expand Up @@ -1848,6 +1841,16 @@ class ComposeBox extends StatefulWidget {
abstract class ComposeBoxState extends State<ComposeBox> {
ComposeBoxController get controller;

/// Fills the compose box with the content of an [OutboxMessage]
/// for a failed [sendMessage] request.
///
/// If there is already text in the compose box, gives a confirmation dialog
/// to confirm that it is OK to discard that text.
///
/// [localMessageId], as in [OutboxMessage.localMessageId], must be present
/// in the message store.
void restoreMessageNotSent(int localMessageId);

/// Switch the compose box to editing mode.
///
/// If there is already text in the compose box, gives a confirmation dialog
Expand All @@ -1869,6 +1872,29 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
@override ComposeBoxController get controller => _controller!;
ComposeBoxController? _controller;

@override
void restoreMessageNotSent(int localMessageId) async {
final zulipLocalizations = ZulipLocalizations.of(context);

final abort = await _abortBecauseContentInputNotEmpty(
dialogMessage: zulipLocalizations.discardDraftForOutboxConfirmationDialogMessage);
if (abort || !mounted) return;

final store = PerAccountStoreWidget.of(context);
final outboxMessage = store.takeOutboxMessage(localMessageId);
setState(() {
_setNewController(store);
final controller = this.controller;
controller
..content.value = TextEditingValue(text: outboxMessage.contentMarkdown)
..contentFocusNode.requestFocus();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit should ideally get a test.

In the interest of merging this for launch, let's just leave a TODO comment for it in the test code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is easy to test :) done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, looks good :)

if (controller is StreamComposeBoxController) {
controller.topic.setTopic(
(outboxMessage.conversation as StreamConversation).topic);
}
});
}

@override
void startEditInteraction(int messageId) async {
final zulipLocalizations = ZulipLocalizations.of(context);
Expand Down Expand Up @@ -1949,7 +1975,8 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
// TODO timeout this request?
if (!mounted) return;
if (!identical(controller, emptyEditController)) {
// user tapped Cancel during the fetch-raw-content request
// During the fetch-raw-content request, the user tapped Cancel
// or tapped a failed message edit or failed outbox message to restore.
// TODO in this case we don't want the error dialog caused by
// ZulipAction.fetchRawContentWithFeedback; suppress that
return;
Expand Down Expand Up @@ -2087,11 +2114,6 @@ class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateM
}
}

// TODO(#720) dismissable message-send error, maybe something like:
// if (controller.sendMessageError.value != null) {
// errorBanner = _ErrorBanner(label:
// ZulipLocalizations.of(context).errorSendMessageTimeout);
// }
return ComposeBoxInheritedWidget.fromComposeBoxState(this,
child: _ComposeBoxContainer(body: body, banner: banner));
}
Expand Down
Loading