Skip to content

compose: Use topicsPolicy and realmTopicsPolicy for per-channel topics policy#2231

Open
Ruhdee wants to merge 5 commits into
zulip:mainfrom
Ruhdee:issue-1604
Open

compose: Use topicsPolicy and realmTopicsPolicy for per-channel topics policy#2231
Ruhdee wants to merge 5 commits into
zulip:mainfrom
Ruhdee:issue-1604

Conversation

@Ruhdee
Copy link
Copy Markdown
Contributor

@Ruhdee Ruhdee commented Mar 18, 2026

Fixes #1604.
Completes #1956.

First 3 commits are from #1956. This PR specifically addresses the second paragraph of this comment: #1956 (comment)

Changes Made:

  1. Added topics policy based handling of topic input in compose box.
  2. Updated existing tests to stop using deprecated realmMandatoryTopics and replace with realmTopicsPolicy.
  3. Updated error string for 'mandatory but empty' validation error.

Doubts:

  1. Are TODO comments added for realmMandatoryTopics considered unnecessary?
  2. I have derived boolean mandatory from effective topics policy for code clarity, and because using switch statement on topicsPolicy is awkward due to topicsPolicy.unknown and topicsPolicy.inherit. Is current approach fine?
  3. Has the previous author been credited correctly?

@Ruhdee Ruhdee marked this pull request as draft March 18, 2026 19:19
@Ruhdee Ruhdee marked this pull request as ready for review March 18, 2026 19:38
@Ruhdee Ruhdee force-pushed the issue-1604 branch 6 times, most recently from 398ae70 to 202c644 Compare March 22, 2026 07:48
Copy link
Copy Markdown
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

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

Thanks! Comments below.

Would you add a commit making InitialSnapshot.realmMandatoryTopics optional? Then the server will be free to drop it once most mobile users have upgraded their mobile-app installation to take that change. The UI should respond in the same way it does when new servers send a value for InitialSnapshot.realmTopicsPolicy that we don't recognize.

Comment thread test/example_data.dart Outdated
Comment thread lib/api/model/model.dart Outdated
Comment thread lib/api/model/model.dart Outdated
Comment thread lib/widgets/compose_box.dart Outdated
Comment thread lib/widgets/compose_box.dart Outdated
Comment on lines +179 to +193
final channelTopicsPolicy = store.streams[streamId]?.topicsPolicy;
if (
channelTopicsPolicy != null
&& channelTopicsPolicy != TopicsPolicy.inherit
) {
return channelTopicsPolicy;
}

// Default to realm-level policy when topicsPolicy is not found,
// or when topicsPolicy is set to inherit.
return switch (store.realmTopicsPolicy) {
RealmTopicsPolicy.allowEmptyTopic => TopicsPolicy.allowEmptyTopic,
RealmTopicsPolicy.disableEmptyTopic => TopicsPolicy.disableEmptyTopic,
RealmTopicsPolicy.unknown || null => TopicsPolicy.unknown,
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • Can tighten up the code in _effectiveTopicsPolicy; also remove a comment which repeats what's obvious in the code.
  • For whether to use the new feature or the old feature, just check whether the new feature is available, i.e. check if store.realmMandatoryTopics is non-null, rather than an explicit feature-level check.
  • Can pull out the "channel policy for realm policy" conversion into a method on TopicsPolicy.

E.g. like this:

  // TODO(#668): listen to [PerAccountStore] once we subscribe to topic policies.
  TopicsPolicy get _effectiveTopicsPolicy {
    final channelTopicsPolicy = store.streams[streamId]?.topicsPolicy;

    if (channelTopicsPolicy case .inherit || null) {
      final realmTopicsPolicy = store.realmTopicsPolicy
        ?? (store.realmMandatoryTopics
              ? .disableEmptyTopic
              : .allowEmptyTopic);
      return TopicsPolicy.forRealmPolicy(realmTopicsPolicy);
    }

    return channelTopicsPolicy;
  }

with this diff to add TopicsPolicy.forRealmPolicy:

diff --git lib/api/model/model.dart lib/api/model/model.dart
index e5c3f87c4..ddaeb24a3 100644
--- lib/api/model/model.dart
+++ lib/api/model/model.dart
@@ -811,6 +811,14 @@ enum TopicsPolicy {
   emptyTopicOnly,
   unknown;
 
+  /// The [TopicsPolicy] corresponding to the given [RealmTopicsPolicy].
+  static TopicsPolicy forRealmPolicy(RealmTopicsPolicy realmPolicy) =>
+    switch (realmPolicy) {
+      .allowEmptyTopic   => .allowEmptyTopic,
+      .disableEmptyTopic => .disableEmptyTopic,
+      .unknown           => .unknown,
+    };
+
   static TopicsPolicy fromApiValue(String value) => _byApiValue[value] ?? unknown;
 
   static final _byApiValue = _$TopicsPolicyEnumMap

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh, also: let's instead make _effectiveTopicsPolicy a public method on ChannelStore, maybe just above selfHasContentAccess and selfCanSendMessage. That way we can offer it to other consumers, if desired, in the future.

Copy link
Copy Markdown
Contributor Author

@Ruhdee Ruhdee Mar 29, 2026

Choose a reason for hiding this comment

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

        ?? (store.realmMandatoryTopics
              ? .disableEmptyTopic
              : .allowEmptyTopic);

Is this formatting (no. of spaces) intentional in the suggested code?

final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
final finder = find.byWidgetPredicate((widget) => widget is TextField
&& widget.decoration?.hintText == zulipLocalizations.composeBoxTopicHintText);
&& widget.controller is ComposeTopicController);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why this change?

Copy link
Copy Markdown
Contributor Author

@Ruhdee Ruhdee Mar 27, 2026

Choose a reason for hiding this comment

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

  1. The following test: "TopicAutocomplete options appear, disappear, and change correctly" fails without the change because the test calls finder twice, once before any interaction with the topic input, and once after interaction.
  2. Earlier, when realmMandatoryTopics was used, it defaulted to true. This meant that no matter the interaction status, hintText would be equal to composeBoxTopicHintText ("Topic").
  3. Now that realmTopicsPolicy is used and it defaults to unknown, or allowEmptyTopic according to your suggestion, hintText changes according to interaction status. So now line 619 returns no element because the hintText changed
    Line 619 of test/widgets/autocomplete_test.dart:
check(tester.widget<TextField>(topicInputFinder).controller!.text)
  .equals(topic3.name.displayName!);

Thus, I made that change and used controller to identify the widget as I felt it is less brittle than matching hintText string. If there is a better way to make that change, please suggest it.

Comment thread test/widgets/compose_box_test.dart
Comment thread test/widgets/compose_box_test.dart Outdated
@Ruhdee
Copy link
Copy Markdown
Contributor Author

Ruhdee commented Mar 29, 2026

Thank you for the review @chrisbobbe. I have addressed all the feedback, PTAL.

Doubt: Should the TODO comment about subscribing to store be changed now that effectiveTopicsPolicy is moved to ChannelStore?

// TODO(#668): listen to [PerAccountStore] once we subscribe to topic policies.

@Ruhdee
Copy link
Copy Markdown
Contributor Author

Ruhdee commented Apr 10, 2026

@chrisbobbe It's been more than a week, would appreciate a review!

chungwwei and others added 4 commits April 23, 2026 21:55
Co-authored-by: Ruhaan <ruhaansande@gmail.com>
Co-authored-by: Ruhaan <ruhaansande@gmail.com>
This commit starts treating realm_mandatory_topics as optional so
newer servers can stop sending it as it was deprecated in feature
level 392. See:
  https://zulip.com/api/changelog
@Ruhdee Ruhdee force-pushed the issue-1604 branch 2 times, most recently from e5bca43 to c8f78f1 Compare April 23, 2026 18:12
@Ruhdee
Copy link
Copy Markdown
Contributor Author

Ruhdee commented Apr 23, 2026

  1. Fixed merge conflict caused by Spanish translation update.
  2. Switched to using channelId instead of streamId for ComposeTopicController and StreamComposeBoxController because of Rename "stream" within codebase to "channel"  #631.
  3. Cleaned up the doTest helper in 'to ChannelNarrow, topic policy resolution' test group in compose_box_test.dart.

@Ruhdee Ruhdee force-pushed the issue-1604 branch 2 times, most recently from c825aeb to 1ca1269 Compare April 23, 2026 19:19
…opics policy

Fixes zulip#1604.

New 'topicsPolicy' and 'realmTopicsPolicy' were introduced in
feature level 392. For older servers, 'effectiveTopicsPolicy'
falls back to 'realmMandatoryTopics'. See:
  https://zulip.com/api/changelog
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per-channel topics_policy, to replace mandatory_topics

3 participants