Repository: zulip/zulip-flutter (language: dart)

Issue:
compose: On recent servers, quote-and-reply doesn't fill empty topic input
To reproduce:

1. Connect to CZO or another realm with feature level 334+
2. Enter a channel narrow
3. Long-press a message; tap "Quote and reply"
4. See the content input update with the quoted message

Expected: the topic input is automatically filled with the message's topic

Actual: the topic input remains blank

-----

This looks like a regression from the recent "general chat" work. To check if the topic input was left empty, the quote-and-reply button checks the condition `composeBoxController.topic.textNormalized == kNoTopicTopic`. Before 769cc7df405562d5f99a869ed009662e2b5801b6, that condition was a reliable test of whether the topic input was left empty. But it isn't reliable anymore; on recent servers, if the topic input is left empty, `composeBoxController.topic.textNormalized` will be the empty string.

Assigning @PIG208 as the author of 769cc7df405562d5f99a869ed009662e2b5801b6.


Discussion hints:
nan

Pull request: compose: Fix bug where topic input not filled on quote-and-reply
Fixes #1469.

Test command: flutter test test/widgets/action_sheet_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart
index 30bbf433f5..c56e41d5c2 100644
--- a/lib/widgets/action_sheet.dart
+++ b/lib/widgets/action_sheet.dart
@@ -1409,7 +1409,7 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
     composeBoxController!;
     if (
       composeBoxController is StreamComposeBoxController
-      && composeBoxController.topic.textNormalized == kNoTopicTopic
+      && composeBoxController.topic.isTopicVacuous
       && message is StreamMessage
     ) {
       composeBoxController.topic.setTopic(message.topic);


Test patch:
diff --git a/test/model/autocomplete_checks.dart b/test/model/autocomplete_checks.dart
index cb08378e77..65e3aaf755 100644
--- a/test/model/autocomplete_checks.dart
+++ b/test/model/autocomplete_checks.dart
@@ -10,6 +10,7 @@ extension ComposeContentControllerChecks on Subject<ComposeContentController> {
 
 extension ComposeTopicControllerChecks on Subject<ComposeTopicController> {
   Subject<AutocompleteIntent<TopicAutocompleteQuery>?> get autocompleteIntent => has((c) => c.autocompleteIntent(), 'autocompleteIntent');
+  Subject<String> get textNormalized => has((c) => c.textNormalized, 'textNormalized');
 }
 
 extension AutocompleteIntentChecks on Subject<AutocompleteIntent<AutocompleteQuery>> {
diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart
index 621f759f28..c43c6daab7 100644
--- a/test/widgets/action_sheet_test.dart
+++ b/test/widgets/action_sheet_test.dart
@@ -41,6 +41,7 @@ import '../api/fake_api.dart';
 import '../api/model/model_checks.dart';
 import '../example_data.dart' as eg;
 import '../flutter_checks.dart';
+import '../model/autocomplete_checks.dart';
 import '../model/binding.dart';
 import '../model/content_test.dart';
 import '../model/test_store.dart';
@@ -1674,8 +1675,33 @@ void main() {
         check(contentController).not((it) => it.validationErrors.contains(ContentValidationError.quoteAndReplyInProgress));
       }
 
-      testWidgets('in channel narrow', (tester) async {
-        final message = eg.streamMessage();
+      testWidgets('in channel narrow with different, non-vacuous topic', (tester) async {
+        final message = eg.streamMessage(topic: 'some topic');
+        await setupToMessageActionSheet(tester, message: message, narrow: ChannelNarrow(message.streamId));
+
+        final composeBoxController = findComposeBoxController(tester) as StreamComposeBoxController;
+        final contentController = composeBoxController.content;
+
+        // Ensure channel-topics are loaded before testing quote & reply behavior
+        connection.prepare(body:
+          jsonEncode(GetStreamTopicsResult(topics: [eg.getStreamTopicsEntry()]).toJson()));
+        final topicController = composeBoxController.topic;
+        topicController.value = TextEditingValue(text: 'other topic');
+
+        final valueBefore = contentController.value;
+        prepareRawContentResponseSuccess(message: message, rawContent: 'Hello world');
+        await tapQuoteAndReplyButton(tester);
+        checkLoadingState(store, contentController, valueBefore: valueBefore, message: message);
+        await tester.pump(Duration.zero); // message is fetched; compose box updates
+        check(composeBoxController.contentFocusNode.hasFocus).isTrue();
+        checkSuccessState(store, contentController,
+          valueBefore: valueBefore, message: message, rawContent: 'Hello world');
+        check(topicController).textNormalized.equals('other topic');
+      });
+
+      testWidgets('in channel narrow with empty topic', (tester) async {
+        // Regression test for https://github.com/zulip/zulip-flutter/issues/1469
+        final message = eg.streamMessage(topic: 'some topic');
         await setupToMessageActionSheet(tester, message: message, narrow: ChannelNarrow(message.streamId));
 
         final composeBoxController = findComposeBoxController(tester) as StreamComposeBoxController;
@@ -1685,7 +1711,7 @@ void main() {
         connection.prepare(body:
           jsonEncode(GetStreamTopicsResult(topics: [eg.getStreamTopicsEntry()]).toJson()));
         final topicController = composeBoxController.topic;
-        topicController.value = const TextEditingValue(text: kNoTopicTopic);
+        topicController.value = const TextEditingValue(text: '');
 
         final valueBefore = contentController.value;
         prepareRawContentResponseSuccess(message: message, rawContent: 'Hello world');
@@ -1695,6 +1721,7 @@ void main() {
         check(composeBoxController.contentFocusNode.hasFocus).isTrue();
         checkSuccessState(store, contentController,
           valueBefore: valueBefore, message: message, rawContent: 'Hello world');
+        check(topicController).textNormalized.equals('some topic');
       });
 
       group('in topic narrow', () {