diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index 4e2723eb59..dbaf96b8a4 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -30,6 +30,15 @@ sealed class Event { case 'update': return RealmUserUpdateEvent.fromJson(json); default: return UnexpectedEvent.fromJson(json); } + case 'subscription': + switch (json['op'] as String) { + case 'add': return SubscriptionAddEvent.fromJson(json); + case 'remove': return SubscriptionRemoveEvent.fromJson(json); + case 'update': return SubscriptionUpdateEvent.fromJson(json); + case 'peer_add': return SubscriptionPeerAddEvent.fromJson(json); + case 'peer_remove': return SubscriptionPeerRemoveEvent.fromJson(json); + default: return UnexpectedEvent.fromJson(json); + } case 'stream': switch (json['op'] as String) { case 'create': return StreamCreateEvent.fromJson(json); @@ -272,6 +281,185 @@ class RealmUserUpdateEvent extends RealmUserEvent { Map toJson() => _$RealmUserUpdateEventToJson(this); } +/// A Zulip event of type `subscription`. +/// +/// The corresponding API docs are in several places for +/// different values of `op`; see subclasses. +sealed class SubscriptionEvent extends Event { + @override + @JsonKey(includeToJson: true) + String get type => 'subscription'; + + String get op; + + SubscriptionEvent({required super.id}); +} + +/// A [SubscriptionEvent] with op `add`: https://zulip.com/api/get-events#subscription-add +@JsonSerializable(fieldRename: FieldRename.snake) +class SubscriptionAddEvent extends SubscriptionEvent { + @override + @JsonKey(includeToJson: true) + String get op => 'add'; + + final List subscriptions; + + SubscriptionAddEvent({required super.id, required this.subscriptions}); + + factory SubscriptionAddEvent.fromJson(Map json) => + _$SubscriptionAddEventFromJson(json); + + @override + Map toJson() => _$SubscriptionAddEventToJson(this); +} + +/// A [SubscriptionEvent] with op `remove`: https://zulip.com/api/get-events#subscription-remove +@JsonSerializable(fieldRename: FieldRename.snake) +class SubscriptionRemoveEvent extends SubscriptionEvent { + @override + @JsonKey(includeToJson: true) + String get op => 'remove'; + + @JsonKey(readValue: _readStreamIds) + final List streamIds; + + static List _readStreamIds(Map json, String key) { + return (json['subscriptions'] as List) + .map((e) => (e as Map)['stream_id'] as int) + .toList(); + } + + SubscriptionRemoveEvent({required super.id, required this.streamIds}); + + factory SubscriptionRemoveEvent.fromJson(Map json) => + _$SubscriptionRemoveEventFromJson(json); + + @override + Map toJson() => _$SubscriptionRemoveEventToJson(this); +} + +/// A [SubscriptionEvent] with op `update`: https://zulip.com/api/get-events#subscription-update +@JsonSerializable(fieldRename: FieldRename.snake) +class SubscriptionUpdateEvent extends SubscriptionEvent { + @override + @JsonKey(includeToJson: true) + String get op => 'update'; + + final int streamId; + + final SubscriptionProperty property; + + /// The new value, or null if we don't recognize the setting. + /// + /// This will have the type appropriate for [property]; for example, + /// if the setting is boolean, then `value is bool` will always be true. + /// This invariant is enforced by [SubscriptionUpdateEvent.fromJson]. + @JsonKey(readValue: _readValue) + final Object? value; + + /// [value], with a check that its type corresponds to [property] + /// (e.g., `value as bool`). + static Object? _readValue(Map json, String key) { + final value = json['value']; + switch (SubscriptionProperty.fromRawString(json['property'] as String)) { + case SubscriptionProperty.color: + return value as String; + case SubscriptionProperty.isMuted: + case SubscriptionProperty.inHomeView: + case SubscriptionProperty.pinToTop: + case SubscriptionProperty.desktopNotifications: + case SubscriptionProperty.audibleNotifications: + case SubscriptionProperty.pushNotifications: + case SubscriptionProperty.emailNotifications: + case SubscriptionProperty.wildcardMentionsNotify: + return value as bool; + case SubscriptionProperty.unknown: + return null; + } + } + + SubscriptionUpdateEvent({ + required super.id, + required this.streamId, + required this.property, + required this.value, + }); + + factory SubscriptionUpdateEvent.fromJson(Map json) => + _$SubscriptionUpdateEventFromJson(json); + + @override + Map toJson() => _$SubscriptionUpdateEventToJson(this); +} + +/// The name of a property in [Subscription]. +/// +/// Used in handling of [SubscriptionUpdateEvent]. +@JsonEnum(fieldRename: FieldRename.snake, alwaysCreate: true) +enum SubscriptionProperty { + color, + isMuted, + inHomeView, + pinToTop, + desktopNotifications, + audibleNotifications, + pushNotifications, + emailNotifications, + wildcardMentionsNotify, + unknown; + + static SubscriptionProperty fromRawString(String raw) => _byRawString[raw] ?? unknown; + + static final _byRawString = _$SubscriptionPropertyEnumMap + .map((key, value) => MapEntry(value, key)); +} + +/// A [SubscriptionEvent] with op `peer_add`: https://zulip.com/api/get-events#subscription-peer_add +@JsonSerializable(fieldRename: FieldRename.snake) +class SubscriptionPeerAddEvent extends SubscriptionEvent { + @override + @JsonKey(includeToJson: true) + String get op => 'peer_add'; + + List streamIds; + List userIds; + + SubscriptionPeerAddEvent({ + required super.id, + required this.streamIds, + required this.userIds, + }); + + factory SubscriptionPeerAddEvent.fromJson(Map json) => + _$SubscriptionPeerAddEventFromJson(json); + + @override + Map toJson() => _$SubscriptionPeerAddEventToJson(this); +} + +/// A [SubscriptionEvent] with op `peer_remove`: https://zulip.com/api/get-events#subscription-peer_remove +@JsonSerializable(fieldRename: FieldRename.snake) +class SubscriptionPeerRemoveEvent extends SubscriptionEvent { + @override + @JsonKey(includeToJson: true) + String get op => 'peer_remove'; + + List streamIds; + List userIds; + + SubscriptionPeerRemoveEvent({ + required super.id, + required this.streamIds, + required this.userIds, + }); + + factory SubscriptionPeerRemoveEvent.fromJson(Map json) => + _$SubscriptionPeerRemoveEventFromJson(json); + + @override + Map toJson() => _$SubscriptionPeerRemoveEventToJson(this); +} + /// A Zulip event of type `stream`. /// /// The corresponding API docs are in several places for diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart index b5c20aafd9..95b2670adc 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -156,6 +156,116 @@ const _$UserRoleEnumMap = { UserRole.unknown: null, }; +SubscriptionAddEvent _$SubscriptionAddEventFromJson( + Map json) => + SubscriptionAddEvent( + id: json['id'] as int, + subscriptions: (json['subscriptions'] as List) + .map((e) => Subscription.fromJson(e as Map)) + .toList(), + ); + +Map _$SubscriptionAddEventToJson( + SubscriptionAddEvent instance) => + { + 'id': instance.id, + 'type': instance.type, + 'op': instance.op, + 'subscriptions': instance.subscriptions, + }; + +SubscriptionRemoveEvent _$SubscriptionRemoveEventFromJson( + Map json) => + SubscriptionRemoveEvent( + id: json['id'] as int, + streamIds: (SubscriptionRemoveEvent._readStreamIds(json, 'stream_ids') + as List) + .map((e) => e as int) + .toList(), + ); + +Map _$SubscriptionRemoveEventToJson( + SubscriptionRemoveEvent instance) => + { + 'id': instance.id, + 'type': instance.type, + 'op': instance.op, + 'stream_ids': instance.streamIds, + }; + +SubscriptionUpdateEvent _$SubscriptionUpdateEventFromJson( + Map json) => + SubscriptionUpdateEvent( + id: json['id'] as int, + streamId: json['stream_id'] as int, + property: $enumDecode(_$SubscriptionPropertyEnumMap, json['property']), + value: SubscriptionUpdateEvent._readValue(json, 'value'), + ); + +Map _$SubscriptionUpdateEventToJson( + SubscriptionUpdateEvent instance) => + { + 'id': instance.id, + 'type': instance.type, + 'op': instance.op, + 'stream_id': instance.streamId, + 'property': _$SubscriptionPropertyEnumMap[instance.property]!, + 'value': instance.value, + }; + +const _$SubscriptionPropertyEnumMap = { + SubscriptionProperty.color: 'color', + SubscriptionProperty.isMuted: 'is_muted', + SubscriptionProperty.inHomeView: 'in_home_view', + SubscriptionProperty.pinToTop: 'pin_to_top', + SubscriptionProperty.desktopNotifications: 'desktop_notifications', + SubscriptionProperty.audibleNotifications: 'audible_notifications', + SubscriptionProperty.pushNotifications: 'push_notifications', + SubscriptionProperty.emailNotifications: 'email_notifications', + SubscriptionProperty.wildcardMentionsNotify: 'wildcard_mentions_notify', + SubscriptionProperty.unknown: 'unknown', +}; + +SubscriptionPeerAddEvent _$SubscriptionPeerAddEventFromJson( + Map json) => + SubscriptionPeerAddEvent( + id: json['id'] as int, + streamIds: + (json['stream_ids'] as List).map((e) => e as int).toList(), + userIds: + (json['user_ids'] as List).map((e) => e as int).toList(), + ); + +Map _$SubscriptionPeerAddEventToJson( + SubscriptionPeerAddEvent instance) => + { + 'id': instance.id, + 'type': instance.type, + 'op': instance.op, + 'stream_ids': instance.streamIds, + 'user_ids': instance.userIds, + }; + +SubscriptionPeerRemoveEvent _$SubscriptionPeerRemoveEventFromJson( + Map json) => + SubscriptionPeerRemoveEvent( + id: json['id'] as int, + streamIds: + (json['stream_ids'] as List).map((e) => e as int).toList(), + userIds: + (json['user_ids'] as List).map((e) => e as int).toList(), + ); + +Map _$SubscriptionPeerRemoveEventToJson( + SubscriptionPeerRemoveEvent instance) => + { + 'id': instance.id, + 'type': instance.type, + 'op': instance.op, + 'stream_ids': instance.streamIds, + 'user_ids': instance.userIds, + }; + StreamCreateEvent _$StreamCreateEventFromJson(Map json) => StreamCreateEvent( id: json['id'] as int, diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index 77a9581eea..23edbb100a 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -249,10 +249,20 @@ class ZulipStream { final bool historyPublicToSubscribers; final int? messageRetentionDays; - final int streamPostPolicy; // TODO enum - // final bool isAnnouncementOnly; // deprecated; ignore + final StreamPostPolicy streamPostPolicy; + // final bool isAnnouncementOnly; // deprecated for `streamPostPolicy`; ignore - final int? canRemoveSubscribersGroupId; // TODO(server-6) + // TODO(server-6): `canRemoveSubscribersGroupId` added in FL 142 + // TODO(server-8): in FL 197 renamed to `canRemoveSubscribersGroup` + @JsonKey(readValue: _readCanRemoveSubscribersGroup) + final int? canRemoveSubscribersGroup; + + // TODO(server-8): added in FL 199, was previously only on [Subscription] objects + final int? streamWeeklyTraffic; + + static int? _readCanRemoveSubscribersGroup(Map json, String key) { + return json[key] ?? json['can_remove_subscribers_group_id']; + } ZulipStream({ required this.streamId, @@ -266,7 +276,8 @@ class ZulipStream { required this.historyPublicToSubscribers, required this.messageRetentionDays, required this.streamPostPolicy, - required this.canRemoveSubscribersGroupId, + required this.canRemoveSubscribersGroup, + required this.streamWeeklyTraffic, }); factory ZulipStream.fromJson(Map json) => @@ -275,58 +286,61 @@ class ZulipStream { Map toJson() => _$ZulipStreamToJson(this); } +/// Policy for which users can post to the stream. +/// +/// For docs, search for "stream_post_policy" +/// in +@JsonEnum(valueField: 'apiValue') +enum StreamPostPolicy { + any(apiValue: 1), + administrators(apiValue: 2), + fullMembers(apiValue: 3), + moderators(apiValue: 4), + unknown(apiValue: null); + + const StreamPostPolicy({ + required this.apiValue, + }); + + final int? apiValue; + + int? toJson() => apiValue; +} + /// As in `subscriptions` in the initial snapshot. /// /// For docs, search for "subscriptions:" /// in . @JsonSerializable(fieldRename: FieldRename.snake) -class Subscription { - // First, fields that are about the stream and not the user's relation to it. - // These are largely the same as in [ZulipStream]. - - final int streamId; - final String name; - final String description; - final String renderedDescription; - - final int dateCreated; - final int? firstMessageId; - final int? streamWeeklyTraffic; - - final bool inviteOnly; - final bool? isWebPublic; // TODO(server-??): doc doesn't say when added - final bool historyPublicToSubscribers; - final int? messageRetentionDays; +class Subscription extends ZulipStream { // final List subscribers; // we register with includeSubscribers false - final int streamPostPolicy; // TODO enum - // final bool? isAnnouncementOnly; // deprecated; ignore + bool? desktopNotifications; + bool? emailNotifications; + bool? wildcardMentionsNotify; + bool? pushNotifications; + bool? audibleNotifications; - final int? canRemoveSubscribersGroupId; // TODO(server-6) - - // Then, fields that are specific to the subscription, - // i.e. the user's relationship to the stream. - - final bool? desktopNotifications; - final bool? emailNotifications; - final bool? wildcardMentionsNotify; - final bool? pushNotifications; - final bool? audibleNotifications; - - final bool pinToTop; - - final bool isMuted; + bool pinToTop; + bool isMuted; // final bool? inHomeView; // deprecated; ignore - final String color; + String color; Subscription({ - required this.streamId, - required this.name, - required this.description, - required this.renderedDescription, - required this.dateCreated, - required this.inviteOnly, + required super.streamId, + required super.name, + required super.description, + required super.renderedDescription, + required super.dateCreated, + required super.firstMessageId, + required super.inviteOnly, + required super.isWebPublic, + required super.historyPublicToSubscribers, + required super.messageRetentionDays, + required super.streamPostPolicy, + required super.canRemoveSubscribersGroup, + required super.streamWeeklyTraffic, required this.desktopNotifications, required this.emailNotifications, required this.wildcardMentionsNotify, @@ -334,19 +348,13 @@ class Subscription { required this.audibleNotifications, required this.pinToTop, required this.isMuted, - required this.isWebPublic, required this.color, - required this.streamPostPolicy, - required this.messageRetentionDays, - required this.historyPublicToSubscribers, - required this.firstMessageId, - required this.streamWeeklyTraffic, - required this.canRemoveSubscribersGroupId, }); factory Subscription.fromJson(Map json) => _$SubscriptionFromJson(json); + @override Map toJson() => _$SubscriptionToJson(this); } diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index 5f95103936..ecc8b11d48 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -155,9 +155,11 @@ 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 int?, - streamPostPolicy: json['stream_post_policy'] as int, - canRemoveSubscribersGroupId: - json['can_remove_subscribers_group_id'] as int?, + streamPostPolicy: + $enumDecode(_$StreamPostPolicyEnumMap, json['stream_post_policy']), + canRemoveSubscribersGroup: ZulipStream._readCanRemoveSubscribersGroup( + json, 'can_remove_subscribers_group') as int?, + streamWeeklyTraffic: json['stream_weekly_traffic'] as int?, ); Map _$ZulipStreamToJson(ZulipStream instance) => @@ -173,16 +175,34 @@ Map _$ZulipStreamToJson(ZulipStream instance) => 'history_public_to_subscribers': instance.historyPublicToSubscribers, 'message_retention_days': instance.messageRetentionDays, 'stream_post_policy': instance.streamPostPolicy, - 'can_remove_subscribers_group_id': instance.canRemoveSubscribersGroupId, + 'can_remove_subscribers_group': instance.canRemoveSubscribersGroup, + 'stream_weekly_traffic': instance.streamWeeklyTraffic, }; +const _$StreamPostPolicyEnumMap = { + StreamPostPolicy.any: 1, + StreamPostPolicy.administrators: 2, + StreamPostPolicy.fullMembers: 3, + StreamPostPolicy.moderators: 4, + StreamPostPolicy.unknown: null, +}; + Subscription _$SubscriptionFromJson(Map json) => Subscription( streamId: json['stream_id'] as int, name: json['name'] as String, description: json['description'] as String, renderedDescription: json['rendered_description'] as String, dateCreated: json['date_created'] as int, + firstMessageId: json['first_message_id'] as int?, inviteOnly: json['invite_only'] as bool, + isWebPublic: json['is_web_public'] as bool, + historyPublicToSubscribers: json['history_public_to_subscribers'] as bool, + messageRetentionDays: json['message_retention_days'] as int?, + streamPostPolicy: + $enumDecode(_$StreamPostPolicyEnumMap, json['stream_post_policy']), + canRemoveSubscribersGroup: ZulipStream._readCanRemoveSubscribersGroup( + json, 'can_remove_subscribers_group') as int?, + streamWeeklyTraffic: json['stream_weekly_traffic'] as int?, desktopNotifications: json['desktop_notifications'] as bool?, emailNotifications: json['email_notifications'] as bool?, wildcardMentionsNotify: json['wildcard_mentions_notify'] as bool?, @@ -190,15 +210,7 @@ Subscription _$SubscriptionFromJson(Map json) => Subscription( audibleNotifications: json['audible_notifications'] as bool?, pinToTop: json['pin_to_top'] as bool, isMuted: json['is_muted'] as bool, - isWebPublic: json['is_web_public'] as bool?, color: json['color'] as String, - streamPostPolicy: json['stream_post_policy'] as int, - messageRetentionDays: json['message_retention_days'] as int?, - historyPublicToSubscribers: json['history_public_to_subscribers'] as bool, - firstMessageId: json['first_message_id'] as int?, - streamWeeklyTraffic: json['stream_weekly_traffic'] as int?, - canRemoveSubscribersGroupId: - json['can_remove_subscribers_group_id'] as int?, ); Map _$SubscriptionToJson(Subscription instance) => @@ -209,13 +221,13 @@ Map _$SubscriptionToJson(Subscription instance) => 'rendered_description': instance.renderedDescription, 'date_created': instance.dateCreated, 'first_message_id': instance.firstMessageId, - 'stream_weekly_traffic': instance.streamWeeklyTraffic, 'invite_only': instance.inviteOnly, 'is_web_public': instance.isWebPublic, 'history_public_to_subscribers': instance.historyPublicToSubscribers, 'message_retention_days': instance.messageRetentionDays, 'stream_post_policy': instance.streamPostPolicy, - 'can_remove_subscribers_group_id': instance.canRemoveSubscribersGroupId, + 'can_remove_subscribers_group': instance.canRemoveSubscribersGroup, + 'stream_weekly_traffic': instance.streamWeeklyTraffic, 'desktop_notifications': instance.desktopNotifications, 'email_notifications': instance.emailNotifications, 'wildcard_mentions_notify': instance.wildcardMentionsNotify, diff --git a/lib/model/store.dart b/lib/model/store.dart index 655fa64310..fc8d164808 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -305,6 +305,52 @@ class PerAccountStore extends ChangeNotifier { subscriptions.remove(stream.streamId); } notifyListeners(); + } else if (event is SubscriptionAddEvent) { + assert(debugLog("server event: subscription/add")); + for (final subscription in event.subscriptions) { + subscriptions[subscription.streamId] = subscription; + } + notifyListeners(); + } else if (event is SubscriptionRemoveEvent) { + assert(debugLog("server event: subscription/remove")); + for (final streamId in event.streamIds) { + subscriptions.remove(streamId); + } + notifyListeners(); + } else if (event is SubscriptionUpdateEvent) { + assert(debugLog("server event: subscription/update")); + final subscription = subscriptions[event.streamId]; + if (subscription == null) return; // TODO(log) + switch (event.property) { + case SubscriptionProperty.color: + subscription.color = event.value as String; + case SubscriptionProperty.isMuted: + subscription.isMuted = event.value as bool; + case SubscriptionProperty.inHomeView: + subscription.isMuted = !(event.value as bool); + case SubscriptionProperty.pinToTop: + subscription.pinToTop = event.value as bool; + case SubscriptionProperty.desktopNotifications: + subscription.desktopNotifications = event.value as bool; + case SubscriptionProperty.audibleNotifications: + subscription.audibleNotifications = event.value as bool; + case SubscriptionProperty.pushNotifications: + subscription.pushNotifications = event.value as bool; + case SubscriptionProperty.emailNotifications: + subscription.emailNotifications = event.value as bool; + case SubscriptionProperty.wildcardMentionsNotify: + subscription.wildcardMentionsNotify = event.value as bool; + case SubscriptionProperty.unknown: + // unrecognized property; do nothing + return; + } + notifyListeners(); + } else if (event is SubscriptionPeerAddEvent) { + assert(debugLog("server event: subscription/peer_add")); + // TODO(#374): handle event + } else if (event is SubscriptionPeerRemoveEvent) { + assert(debugLog("server event: subscription/peer_remove")); + // TODO(#374): handle event } else if (event is MessageEvent) { assert(debugLog("server event: message ${jsonEncode(event.message.toJson())}")); recentDmConversationsView.handleMessageEvent(event); diff --git a/test/api/model/events_checks.dart b/test/api/model/events_checks.dart index a7e9440916..038abadb0a 100644 --- a/test/api/model/events_checks.dart +++ b/test/api/model/events_checks.dart @@ -15,6 +15,10 @@ extension AlertWordsEventChecks on Subject { Subject> get alertWords => has((e) => e.alertWords, 'alertWords'); } +extension SubscriptionRemoveEventChecks on Subject { + Subject> get streamIds => has((e) => e.streamIds, 'streamIds'); +} + extension MessageEventChecks on Subject { Subject get message => has((e) => e.message, 'message'); } diff --git a/test/api/model/events_test.dart b/test/api/model/events_test.dart index 2fe7153065..dd2412bd84 100644 --- a/test/api/model/events_test.dart +++ b/test/api/model/events_test.dart @@ -30,6 +30,18 @@ void main() { ).isEmpty(); }); + test('subscription/remove: deserialize stream_ids correctly', () { + check(Event.fromJson({ + 'id': 1, + 'type': 'subscription', + 'op': 'remove', + 'subscriptions': [ + {'stream_id': 123, 'name': 'name 1'}, + {'stream_id': 456, 'name': 'name 2'}, + ], + }) as SubscriptionRemoveEvent).streamIds.jsonEquals([123, 456]); + }); + test('message: move flags into message object', () { final message = eg.streamMessage(); MessageEvent mkEvent(List flags) => Event.fromJson({ diff --git a/test/api/model/model_checks.dart b/test/api/model/model_checks.dart index a6d4c4a2eb..603712a9f4 100644 --- a/test/api/model/model_checks.dart +++ b/test/api/model/model_checks.dart @@ -1,6 +1,10 @@ import 'package:checks/checks.dart'; import 'package:zulip/api/model/model.dart'; +extension ZulipStreamChecks on Subject { + Subject get canRemoveSubscribersGroup => has((e) => e.canRemoveSubscribersGroup, 'canRemoveSubscribersGroup'); +} + extension MessageChecks on Subject { Subject get content => has((e) => e.content, 'content'); Subject get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage'); diff --git a/test/api/model/model_test.dart b/test/api/model/model_test.dart index 90d6177e5a..1bacb090a2 100644 --- a/test/api/model/model_test.dart +++ b/test/api/model/model_test.dart @@ -65,6 +65,43 @@ void main() { }); }); + group('ZulipStream.canRemoveSubscribersGroup', () { + final Map baseJson = Map.unmodifiable({ + 'stream_id': 123, + 'name': 'A stream', + 'description': 'A description', + 'rendered_description': '

A description

', + 'date_created': 1686774898, + 'first_message_id': null, + 'invite_only': false, + 'is_web_public': false, + 'history_public_to_subscribers': true, + 'message_retention_days': null, + 'stream_post_policy': StreamPostPolicy.any.apiValue, + // 'can_remove_subscribers_group': null, + 'stream_weekly_traffic': null, + }); + + test('smoke', () { + check(ZulipStream.fromJson({ ...baseJson, + 'can_remove_subscribers_group': 123, + })).canRemoveSubscribersGroup.equals(123); + }); + + // TODO(server-8): field renamed in FL 197 + test('support old can_remove_subscribers_group_id', () { + check(ZulipStream.fromJson({ ...baseJson, + 'can_remove_subscribers_group_id': 456, + })).canRemoveSubscribersGroup.equals(456); + }); + + // TODO(server-6): field added in FL 142 + test('support field missing', () { + check(ZulipStream.fromJson({ ...baseJson, + })).canRemoveSubscribersGroup.isNull(); + }); + }); + group('Message', () { test('no crash on unrecognized flag', () { final m1 = Message.fromJson( diff --git a/test/example_data.dart b/test/example_data.dart index 43116a3fa3..9b31124609 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -103,8 +103,9 @@ ZulipStream stream({ bool? isWebPublic, bool? historyPublicToSubscribers, int? messageRetentionDays, - int? streamPostPolicy, - int? canRemoveSubscribersGroupId, + StreamPostPolicy? streamPostPolicy, + int? canRemoveSubscribersGroup, + int? streamWeeklyTraffic, }) { return ZulipStream( streamId: streamId ?? 123, // TODO generate example IDs @@ -117,12 +118,53 @@ ZulipStream stream({ isWebPublic: isWebPublic ?? false, historyPublicToSubscribers: historyPublicToSubscribers ?? true, messageRetentionDays: messageRetentionDays, - streamPostPolicy: streamPostPolicy ?? 1, - canRemoveSubscribersGroupId: canRemoveSubscribersGroupId ?? 123, + streamPostPolicy: streamPostPolicy ?? StreamPostPolicy.any, + canRemoveSubscribersGroup: canRemoveSubscribersGroup ?? 123, + streamWeeklyTraffic: streamWeeklyTraffic, ); } const _stream = stream; +/// Construct an example subscription from a stream. +/// +/// We only allow overrides of values specific to the [Subscription], all +/// other properties are copied from the [ZulipStream] provided. +Subscription subscription( + ZulipStream stream, { + bool? desktopNotifications, + bool? emailNotifications, + bool? wildcardMentionsNotify, + bool? pushNotifications, + bool? audibleNotifications, + bool? pinToTop, + bool? isMuted, + String? color, +}) { + return Subscription( + streamId: stream.streamId, + name: stream.name, + description: stream.description, + renderedDescription: stream.renderedDescription, + dateCreated: stream.dateCreated, + firstMessageId: stream.firstMessageId, + inviteOnly: stream.inviteOnly, + isWebPublic: stream.isWebPublic, + historyPublicToSubscribers: stream.historyPublicToSubscribers, + messageRetentionDays: stream.messageRetentionDays, + streamPostPolicy: stream.streamPostPolicy, + canRemoveSubscribersGroup: stream.canRemoveSubscribersGroup, + streamWeeklyTraffic: stream.streamWeeklyTraffic, + desktopNotifications: desktopNotifications ?? false, + emailNotifications: emailNotifications ?? false, + wildcardMentionsNotify: wildcardMentionsNotify ?? false, + pushNotifications: pushNotifications ?? false, + audibleNotifications: audibleNotifications ?? false, + pinToTop: pinToTop ?? false, + isMuted: isMuted ?? false, + color: color ?? "#FF0000", + ); +} + //////////////////////////////////////////////////////////////// // Messages, and pieces of messages. // diff --git a/test/model/store_test.dart b/test/model/store_test.dart index b4c4390fec..f1f53714a8 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:checks/checks.dart'; import 'package:http/http.dart' as http; import 'package:test/scaffolding.dart'; +import 'package:zulip/api/model/events.dart'; import 'package:zulip/model/store.dart'; import 'package:zulip/notifications.dart'; @@ -178,6 +179,52 @@ void main() { checkLastRequest(token: '456def'); }); }); + + group('handleEvent for SubscriptionEvent', () { + final stream = eg.stream(); + + test('SubscriptionProperty.color updates with a string value', () { + final store = eg.store(initialSnapshot: eg.initialSnapshot( + streams: [stream], + subscriptions: [eg.subscription(stream, color: "#FF0000")], + )); + check(store.subscriptions[stream.streamId]!.color).equals('#FF0000'); + + store.handleEvent(SubscriptionUpdateEvent(id: 1, + streamId: stream.streamId, + property: SubscriptionProperty.color, + value: "#FF00FF")); + check(store.subscriptions[stream.streamId]!.color).equals('#FF00FF'); + }); + + test('SubscriptionProperty.isMuted updates with a boolean value', () { + final store = eg.store(initialSnapshot: eg.initialSnapshot( + streams: [stream], + subscriptions: [eg.subscription(stream, isMuted: false)], + )); + check(store.subscriptions[stream.streamId]!.isMuted).isFalse(); + + store.handleEvent(SubscriptionUpdateEvent(id: 1, + streamId: stream.streamId, + property: SubscriptionProperty.isMuted, + value: true)); + check(store.subscriptions[stream.streamId]!.isMuted).isTrue(); + }); + + test('SubscriptionProperty.inHomeView updates isMuted instead', () { + final store = eg.store(initialSnapshot: eg.initialSnapshot( + streams: [stream], + subscriptions: [eg.subscription(stream, isMuted: false)], + )); + check(store.subscriptions[stream.streamId]!.isMuted).isFalse(); + + store.handleEvent(SubscriptionUpdateEvent(id: 1, + streamId: stream.streamId, + property: SubscriptionProperty.inHomeView, + value: false)); + check(store.subscriptions[stream.streamId]!.isMuted).isTrue(); + }); + }); } class LoadingTestGlobalStore extends TestGlobalStore {