From 395d0daa221aaff8ef6e4c4db5ad41f3c3404c8f Mon Sep 17 00:00:00 2001 From: chungwwei Date: Sun, 2 Nov 2025 21:08:37 -0500 Subject: [PATCH 1/3] api: Add InitialSnapshot.realmTopicsPolicy --- lib/api/model/initial_snapshot.dart | 19 +++++++++++++++++++ lib/api/model/initial_snapshot.g.dart | 10 ++++++++++ test/example_data.dart | 2 ++ 3 files changed, 31 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index eeedcde14d..ad22954dfe 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -90,6 +90,8 @@ class InitialSnapshot { /// Search for "realm_wildcard_mention_policy" in https://zulip.com/api/register-queue. final RealmWildcardMentionPolicy realmWildcardMentionPolicy; + final RealmTopicsPolicy realmTopicsPolicy; + final bool realmMandatoryTopics; final String realmName; @@ -183,6 +185,7 @@ class InitialSnapshot { required this.realmCanDeleteOwnMessageGroup, required this.realmDeleteOwnMessagePolicy, required this.realmWildcardMentionPolicy, + required this.realmTopicsPolicy, required this.realmMandatoryTopics, required this.realmName, required this.realmWaitingPeriodThreshold, @@ -207,6 +210,22 @@ class InitialSnapshot { Map toJson() => _$InitialSnapshotToJson(this); } +/// A value of [InitialSnapshot.realmTopicsPolicy]. +/// +/// For docs, search for "realm_topics_policy" +/// in . +@JsonEnum(fieldRename: FieldRename.snake) +enum RealmTopicsPolicy { + allowEmptyTopic, + disableEmptyTopic; + + static RealmTopicsPolicy fromApiValue(String apiValue)=> _byApiValue[apiValue] ?? allowEmptyTopic; + + static final _byApiValue = _$RealmTopicsPolicyEnumMap.map((key, value) => MapEntry(value, key)); + + String toJson() => _$RealmTopicsPolicyEnumMap[this]!; +} + @JsonEnum(valueField: 'apiValue') enum RealmWildcardMentionPolicy { everyone(apiValue: 1), diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index 1c5505a653..a663582982 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -101,6 +101,10 @@ InitialSnapshot _$InitialSnapshotFromJson( _$RealmWildcardMentionPolicyEnumMap, json['realm_wildcard_mention_policy'], ), + realmTopicsPolicy: $enumDecode( + _$RealmTopicsPolicyEnumMap, + json['realm_topics_policy'], + ), realmMandatoryTopics: json['realm_mandatory_topics'] as bool, realmName: json['realm_name'] as String, realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num) @@ -181,6 +185,7 @@ Map _$InitialSnapshotToJson( 'realm_can_delete_own_message_group': instance.realmCanDeleteOwnMessageGroup, 'realm_delete_own_message_policy': instance.realmDeleteOwnMessagePolicy, 'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy, + 'realm_topics_policy': instance.realmTopicsPolicy, 'realm_mandatory_topics': instance.realmMandatoryTopics, 'realm_name': instance.realmName, 'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold, @@ -218,6 +223,11 @@ const _$RealmWildcardMentionPolicyEnumMap = { RealmWildcardMentionPolicy.moderators: 7, }; +const _$RealmTopicsPolicyEnumMap = { + RealmTopicsPolicy.allowEmptyTopic: 'allow_empty_topic', + RealmTopicsPolicy.disableEmptyTopic: 'disable_empty_topic', +}; + RealmDefaultExternalAccount _$RealmDefaultExternalAccountFromJson( Map json, ) => RealmDefaultExternalAccount( diff --git a/test/example_data.dart b/test/example_data.dart index a6e3e9655d..b0acea28a4 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1335,6 +1335,7 @@ InitialSnapshot initialSnapshot({ GroupSettingValue? realmCanDeleteOwnMessageGroup, RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy, RealmWildcardMentionPolicy? realmWildcardMentionPolicy, + RealmTopicsPolicy? realmTopicsPolicy, bool? realmMandatoryTopics, String? realmName, int? realmWaitingPeriodThreshold, @@ -1399,6 +1400,7 @@ InitialSnapshot initialSnapshot({ realmCanDeleteOwnMessageGroup: realmCanDeleteOwnMessageGroup, realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, realmWildcardMentionPolicy: realmWildcardMentionPolicy ?? RealmWildcardMentionPolicy.everyone, + realmTopicsPolicy: realmTopicsPolicy ?? RealmTopicsPolicy.allowEmptyTopic, realmMandatoryTopics: realmMandatoryTopics ?? true, realmName: realmName ?? 'Example Zulip organization', realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0, From ea2e450c1bc078f947dd8f353895e1972b5c6fa6 Mon Sep 17 00:00:00 2001 From: chungwwei Date: Sun, 2 Nov 2025 21:20:51 -0500 Subject: [PATCH 2/3] model: Add realmTopicsPolicy to RealmStore --- lib/model/realm.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/model/realm.dart b/lib/model/realm.dart index 5255e50623..c9d843bf30 100644 --- a/lib/model/realm.dart +++ b/lib/model/realm.dart @@ -46,6 +46,7 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { GroupSettingValue? get realmCanDeleteAnyMessageGroup; // TODO(server-10) GroupSettingValue? get realmCanDeleteOwnMessageGroup; // TODO(server-10) bool get realmEnableReadReceipts; + RealmTopicsPolicy get realmTopicsPolicy; bool get realmMandatoryTopics; int get maxFileUploadSizeMib; int? get realmMessageContentDeleteLimitSeconds; @@ -174,6 +175,8 @@ mixin ProxyRealmStore on RealmStore { @override bool get realmEnableReadReceipts => realmStore.realmEnableReadReceipts; @override + RealmTopicsPolicy get realmTopicsPolicy => realmStore.realmTopicsPolicy; + @override bool get realmMandatoryTopics => realmStore.realmMandatoryTopics; @override int get maxFileUploadSizeMib => realmStore.maxFileUploadSizeMib; @@ -233,6 +236,7 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { realmAllowMessageEditing = initialSnapshot.realmAllowMessageEditing, realmCanDeleteAnyMessageGroup = initialSnapshot.realmCanDeleteAnyMessageGroup, realmCanDeleteOwnMessageGroup = initialSnapshot.realmCanDeleteOwnMessageGroup, + realmTopicsPolicy = initialSnapshot.realmTopicsPolicy, realmMandatoryTopics = initialSnapshot.realmMandatoryTopics, maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib, realmMessageContentDeleteLimitSeconds = initialSnapshot.realmMessageContentDeleteLimitSeconds, @@ -383,6 +387,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { @override final bool realmEnableReadReceipts; @override + final RealmTopicsPolicy realmTopicsPolicy; + @override final bool realmMandatoryTopics; @override final int maxFileUploadSizeMib; From afc7562052265a781647014f2d39692e2ef5922a Mon Sep 17 00:00:00 2001 From: chungwwei Date: Wed, 22 Oct 2025 10:59:55 -0400 Subject: [PATCH 3/3] api: Add ZulipStream.topicsPolicy Enum TopicsPolicy is created for these four values: inherit, allow_empty_topic, disable_empty_topic, and empty_topic_only. --- lib/api/model/events.dart | 2 ++ lib/api/model/events.g.dart | 1 + lib/api/model/model.dart | 24 ++++++++++++++++++++++++ lib/api/model/model.g.dart | 16 ++++++++++++++++ lib/model/channel.dart | 2 ++ test/example_data.dart | 5 +++++ 6 files changed, 50 insertions(+) diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index e095c5360b..a369680b3d 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -681,6 +681,8 @@ class ChannelUpdateEvent extends ChannelEvent { return value as bool; case ChannelPropertyName.messageRetentionDays: return value as int?; + case ChannelPropertyName.topicsPolicy: + return TopicsPolicy.fromApiValue(value as String); case ChannelPropertyName.channelPostPolicy: return ChannelPostPolicy.fromApiValue(value as int); case ChannelPropertyName.folderId: diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart index dff0d21fa2..a2647198ed 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -461,6 +461,7 @@ const _$ChannelPropertyNameEnumMap = { ChannelPropertyName.firstMessageId: 'first_message_id', ChannelPropertyName.inviteOnly: 'invite_only', ChannelPropertyName.messageRetentionDays: 'message_retention_days', + ChannelPropertyName.topicsPolicy: 'topics_policy', ChannelPropertyName.channelPostPolicy: 'stream_post_policy', ChannelPropertyName.folderId: 'folder_id', ChannelPropertyName.canAddSubscribersGroup: 'can_add_subscribers_group', diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index da12823520..0d2779071c 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -659,6 +659,8 @@ class ZulipStream { bool isWebPublic; // present since 2.1, according to /api/changelog bool historyPublicToSubscribers; int? messageRetentionDays; + @JsonKey(defaultValue: TopicsPolicy.inherit) // TODO(server-11) remove default value + TopicsPolicy topicsPolicy; @JsonKey(name: 'stream_post_policy') ChannelPostPolicy? channelPostPolicy; // TODO(server-10) remove // final bool isAnnouncementOnly; // deprecated for `channelPostPolicy`; ignore @@ -684,6 +686,7 @@ class ZulipStream { required this.isWebPublic, required this.historyPublicToSubscribers, required this.messageRetentionDays, + required this.topicsPolicy, required this.channelPostPolicy, required this.folderId, required this.canAddSubscribersGroup, @@ -708,6 +711,7 @@ class ZulipStream { isWebPublic: subscription.isWebPublic, historyPublicToSubscribers: subscription.historyPublicToSubscribers, messageRetentionDays: subscription.messageRetentionDays, + topicsPolicy: subscription.topicsPolicy, channelPostPolicy: subscription.channelPostPolicy, folderId: subscription.folderId, canAddSubscribersGroup: subscription.canAddSubscribersGroup, @@ -744,6 +748,7 @@ enum ChannelPropertyName { // isWebPublic is updated via its own [ChannelUpdateEvent] field // historyPublicToSubscribers is updated via its own [ChannelUpdateEvent] field messageRetentionDays, + topicsPolicy, @JsonValue('stream_post_policy') channelPostPolicy, folderId, @@ -765,6 +770,24 @@ enum ChannelPropertyName { .map((key, value) => MapEntry(value, key)); } +/// A value of [ZulipStream.topicsPolicy]. +/// +/// For docs, search for "topics_policy" +/// in . +@JsonEnum(fieldRename: FieldRename.snake) +enum TopicsPolicy { + inherit, + allowEmptyTopic, + disableEmptyTopic, + emptyTopicOnly; + + static TopicsPolicy fromApiValue(String apiValue) => _byApiValue[apiValue] ?? inherit; + + static final _byApiValue = _$TopicsPolicyEnumMap.map((key, value) => MapEntry(value, key)); + + String toJson() => _$TopicsPolicyEnumMap[this]!; +} + /// Policy for which users can post to the stream. /// /// For docs, search for "stream_post_policy" @@ -830,6 +853,7 @@ class Subscription extends ZulipStream { required super.isWebPublic, required super.historyPublicToSubscribers, required super.messageRetentionDays, + required super.topicsPolicy, required super.channelPostPolicy, required super.folderId, required super.canAddSubscribersGroup, diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index b835c493c5..d48714979c 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -247,6 +247,9 @@ ZulipStream _$ZulipStreamFromJson(Map json) => ZulipStream( isWebPublic: json['is_web_public'] as bool, historyPublicToSubscribers: json['history_public_to_subscribers'] as bool, messageRetentionDays: (json['message_retention_days'] as num?)?.toInt(), + topicsPolicy: + $enumDecodeNullable(_$TopicsPolicyEnumMap, json['topics_policy']) ?? + TopicsPolicy.inherit, channelPostPolicy: $enumDecodeNullable( _$ChannelPostPolicyEnumMap, json['stream_post_policy'], @@ -284,6 +287,7 @@ Map _$ZulipStreamToJson(ZulipStream instance) => 'is_web_public': instance.isWebPublic, 'history_public_to_subscribers': instance.historyPublicToSubscribers, 'message_retention_days': instance.messageRetentionDays, + 'topics_policy': instance.topicsPolicy, 'stream_post_policy': instance.channelPostPolicy, 'can_add_subscribers_group': instance.canAddSubscribersGroup, 'can_delete_any_message_group': instance.canDeleteAnyMessageGroup, @@ -293,6 +297,13 @@ Map _$ZulipStreamToJson(ZulipStream instance) => 'stream_weekly_traffic': instance.streamWeeklyTraffic, }; +const _$TopicsPolicyEnumMap = { + TopicsPolicy.inherit: 'inherit', + TopicsPolicy.allowEmptyTopic: 'allow_empty_topic', + TopicsPolicy.disableEmptyTopic: 'disable_empty_topic', + TopicsPolicy.emptyTopicOnly: 'empty_topic_only', +}; + const _$ChannelPostPolicyEnumMap = { ChannelPostPolicy.any: 1, ChannelPostPolicy.administrators: 2, @@ -313,6 +324,9 @@ Subscription _$SubscriptionFromJson(Map json) => Subscription( isWebPublic: json['is_web_public'] as bool, historyPublicToSubscribers: json['history_public_to_subscribers'] as bool, messageRetentionDays: (json['message_retention_days'] as num?)?.toInt(), + topicsPolicy: + $enumDecodeNullable(_$TopicsPolicyEnumMap, json['topics_policy']) ?? + TopicsPolicy.inherit, channelPostPolicy: $enumDecodeNullable( _$ChannelPostPolicyEnumMap, json['stream_post_policy'], @@ -358,6 +372,7 @@ Map _$SubscriptionToJson(Subscription instance) => 'is_web_public': instance.isWebPublic, 'history_public_to_subscribers': instance.historyPublicToSubscribers, 'message_retention_days': instance.messageRetentionDays, + 'topics_policy': instance.topicsPolicy, 'stream_post_policy': instance.channelPostPolicy, 'can_add_subscribers_group': instance.canAddSubscribersGroup, 'can_delete_any_message_group': instance.canDeleteAnyMessageGroup, @@ -547,6 +562,7 @@ const _$ChannelPropertyNameEnumMap = { ChannelPropertyName.firstMessageId: 'first_message_id', ChannelPropertyName.inviteOnly: 'invite_only', ChannelPropertyName.messageRetentionDays: 'message_retention_days', + ChannelPropertyName.topicsPolicy: 'topics_policy', ChannelPropertyName.channelPostPolicy: 'stream_post_policy', ChannelPropertyName.folderId: 'folder_id', ChannelPropertyName.canAddSubscribersGroup: 'can_add_subscribers_group', diff --git a/lib/model/channel.dart b/lib/model/channel.dart index 10e9596eed..90d08ed2b4 100644 --- a/lib/model/channel.dart +++ b/lib/model/channel.dart @@ -444,6 +444,8 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore { stream.inviteOnly = event.value as bool; case ChannelPropertyName.messageRetentionDays: stream.messageRetentionDays = event.value as int?; + case ChannelPropertyName.topicsPolicy: + stream.topicsPolicy = event.value as TopicsPolicy; case ChannelPropertyName.channelPostPolicy: stream.channelPostPolicy = event.value as ChannelPostPolicy; case ChannelPropertyName.folderId: diff --git a/test/example_data.dart b/test/example_data.dart index b0acea28a4..68384bcc3b 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -464,6 +464,7 @@ ZulipStream stream({ bool? isWebPublic, bool? historyPublicToSubscribers, int? messageRetentionDays, + TopicsPolicy? topicsPolicy, ChannelPostPolicy? channelPostPolicy, int? folderId, GroupSettingValue? canAddSubscribersGroup, @@ -496,6 +497,7 @@ ZulipStream stream({ isWebPublic: isWebPublic ?? false, historyPublicToSubscribers: historyPublicToSubscribers ?? true, messageRetentionDays: messageRetentionDays, + topicsPolicy: topicsPolicy ?? TopicsPolicy.inherit, channelPostPolicy: channelPostPolicy ?? ChannelPostPolicy.any, folderId: folderId, canAddSubscribersGroup: canAddSubscribersGroup ?? GroupSettingValueNamed(nobodyGroup.id), @@ -541,6 +543,7 @@ Subscription subscription( isWebPublic: stream.isWebPublic, historyPublicToSubscribers: stream.historyPublicToSubscribers, messageRetentionDays: stream.messageRetentionDays, + topicsPolicy: stream.topicsPolicy, channelPostPolicy: stream.channelPostPolicy, folderId: stream.folderId, canAddSubscribersGroup: stream.canAddSubscribersGroup, @@ -1261,6 +1264,8 @@ ChannelUpdateEvent channelUpdateEvent( assert(value is bool); case ChannelPropertyName.messageRetentionDays: assert(value is int?); + case ChannelPropertyName.topicsPolicy: + assert(value is TopicsPolicy); case ChannelPropertyName.channelPostPolicy: assert(value is ChannelPostPolicy); case ChannelPropertyName.folderId: