Skip to content

Commit f65660a

Browse files
committed
api: Add stream/update event
Fixes: zulip#182
1 parent 4eed88c commit f65660a

File tree

7 files changed

+248
-3
lines changed

7 files changed

+248
-3
lines changed

lib/api/model/events.dart

+68-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ sealed class Event {
4040
switch (json['op'] as String) {
4141
case 'create': return ChannelCreateEvent.fromJson(json);
4242
case 'delete': return ChannelDeleteEvent.fromJson(json);
43-
// TODO(#182): case 'update':
43+
case 'update': return ChannelUpdateEvent.fromJson(json);
4444
default: return UnexpectedEvent.fromJson(json);
4545
}
4646
case 'subscription':
@@ -379,8 +379,73 @@ class ChannelDeleteEvent extends ChannelEvent {
379379
Map<String, dynamic> toJson() => _$ChannelDeleteEventToJson(this);
380380
}
381381

382-
// TODO(#182) ChannelUpdateEvent, for a [ChannelEvent] with op `update`:
383-
// https://zulip.com/api/get-events#stream-update
382+
/// A [ChannelEvent] with op `update`: https://zulip.com/api/get-events#stream-update
383+
@JsonSerializable(fieldRename: FieldRename.snake)
384+
class ChannelUpdateEvent extends ChannelEvent {
385+
@override
386+
String get op => 'update';
387+
388+
final int streamId;
389+
final String name;
390+
391+
/// The name of the channel property, or null if we don't recognize it.
392+
@JsonKey(unknownEnumValue: JsonKey.nullForUndefinedEnumValue)
393+
final ChannelPropertyName? property;
394+
395+
/// The new value, or null if we don't recognize the property.
396+
///
397+
/// This will have the type appropriate for [property]; for example,
398+
/// if the property is boolean, then `value is bool` will always be true.
399+
/// This invariant is enforced by [ChannelUpdateEvent.fromJson].
400+
@JsonKey(readValue: _readValue)
401+
final Object? value;
402+
403+
final String? renderedDescription;
404+
final bool? historyPublicToSubscribers;
405+
final bool? isWebPublic;
406+
407+
ChannelUpdateEvent({
408+
required super.id,
409+
required this.streamId,
410+
required this.name,
411+
required this.property,
412+
required this.value,
413+
this.renderedDescription,
414+
this.historyPublicToSubscribers,
415+
this.isWebPublic,
416+
});
417+
418+
/// [value], with a check that its type corresponds to [property]
419+
/// (e.g., `value as bool`).
420+
static Object? _readValue(Map<dynamic, dynamic> json, String key) {
421+
final value = json['value'];
422+
switch (ChannelPropertyName.fromRawString(json['property'] as String)) {
423+
case ChannelPropertyName.name:
424+
case ChannelPropertyName.description:
425+
return value as String;
426+
case ChannelPropertyName.firstMessageId:
427+
return value as int?;
428+
case ChannelPropertyName.inviteOnly:
429+
return value as bool;
430+
case ChannelPropertyName.messageRetentionDays:
431+
return value as int?;
432+
case ChannelPropertyName.channelPostPolicy:
433+
return ChannelPostPolicy.fromInt(value as int?);
434+
case ChannelPropertyName.canRemoveSubscribersGroup:
435+
case ChannelPropertyName.canRemoveSubscribersGroupId:
436+
case ChannelPropertyName.streamWeeklyTraffic:
437+
return value as int?;
438+
case null:
439+
return null;
440+
}
441+
}
442+
443+
factory ChannelUpdateEvent.fromJson(Map<String, dynamic> json) =>
444+
_$ChannelUpdateEventFromJson(json);
445+
446+
@override
447+
Map<String, dynamic> toJson() => _$ChannelUpdateEventToJson(this);
448+
}
384449

385450
/// A Zulip event of type `subscription`.
386451
///

lib/api/model/events.g.dart

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/api/model/model.dart

+39
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,45 @@ enum ChannelPostPolicy {
391391
.map((key, value) => MapEntry(value, key));
392392
}
393393

394+
/// The name of the [ZulipStream] properties that gets updated through [ChannelUpdateEvent.property].
395+
///
396+
/// In Zulip event-handling code (for [ChannelUpdateEvent]),
397+
/// we switch exhaustively on a value of this type
398+
/// to ensure that every property change in [ZulipStream] responds to the event.
399+
///
400+
/// Fields on [ZulipStream] not present here:
401+
/// streamId, dateCreated
402+
/// Each of those is immutable on any given channel, and there is no
403+
/// [ChannelUpdateEvent] that updates them.
404+
///
405+
/// Other fields on [ZulipStream] not present here:
406+
/// renderedDescription, historyPublicToSubscribers, isWebPublic
407+
/// Each of those are updated through separate fields of [ChannelUpdateEvent]
408+
/// with the same names.
409+
@JsonEnum(fieldRename: FieldRename.snake, alwaysCreate: true)
410+
enum ChannelPropertyName {
411+
name,
412+
description,
413+
firstMessageId,
414+
inviteOnly,
415+
messageRetentionDays,
416+
@JsonValue('stream_post_policy')
417+
channelPostPolicy,
418+
canRemoveSubscribersGroup,
419+
canRemoveSubscribersGroupId, // TODO(Zulip-6): remove, replaced by canRemoveSubscribersGroup
420+
streamWeeklyTraffic;
421+
422+
/// Get a [ChannelPropertyName] from a raw, snake-case string we recognize, else null.
423+
///
424+
/// Example:
425+
/// 'invite_only' -> ChannelPropertyName.inviteOnly
426+
static ChannelPropertyName? fromRawString(String raw) => _byRawString[raw];
427+
428+
// _$…EnumMap is thanks to `alwaysCreate: true` and `fieldRename: FieldRename.snake`
429+
static final _byRawString = _$ChannelPropertyNameEnumMap
430+
.map((key, value) => MapEntry(value, key));
431+
}
432+
394433
/// As in `subscriptions` in the initial snapshot.
395434
///
396435
/// For docs, search for "subscriptions:"

lib/api/model/model.g.dart

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/channel.dart

+45
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,51 @@ class ChannelStoreImpl with ChannelStore {
199199
streamsByName.remove(stream.name);
200200
subscriptions.remove(stream.streamId);
201201
}
202+
203+
case ChannelUpdateEvent():
204+
final ChannelUpdateEvent(:streamId, name: String streamName) = event;
205+
assert(identical(streams[streamId], streamsByName[streamName]));
206+
assert(subscriptions[streamId] == null
207+
|| identical(subscriptions[streamId], streams[streamId]));
208+
209+
final channel = streams[streamId];
210+
if (channel == null) return;
211+
212+
if (event.renderedDescription != null) {
213+
channel.renderedDescription = event.renderedDescription!;
214+
}
215+
if (event.historyPublicToSubscribers != null) {
216+
channel.historyPublicToSubscribers = event.historyPublicToSubscribers!;
217+
}
218+
if (event.isWebPublic != null) {
219+
channel.isWebPublic = event.isWebPublic!;
220+
}
221+
222+
if (event.property == null) {
223+
// unrecognized property; do nothing
224+
return;
225+
}
226+
switch (event.property!) {
227+
case ChannelPropertyName.name:
228+
channel.name = event.value as String;
229+
streamsByName.remove(streamName);
230+
streamsByName[channel.name] = channel;
231+
case ChannelPropertyName.description:
232+
channel.description = event.value as String;
233+
case ChannelPropertyName.firstMessageId:
234+
channel.firstMessageId = event.value as int?;
235+
case ChannelPropertyName.inviteOnly:
236+
channel.inviteOnly = event.value as bool;
237+
case ChannelPropertyName.messageRetentionDays:
238+
channel.messageRetentionDays = event.value as int?;
239+
case ChannelPropertyName.channelPostPolicy:
240+
channel.channelPostPolicy = event.value as ChannelPostPolicy;
241+
case ChannelPropertyName.canRemoveSubscribersGroup:
242+
case ChannelPropertyName.canRemoveSubscribersGroupId:
243+
channel.canRemoveSubscribersGroup = event.value as int?;
244+
case ChannelPropertyName.streamWeeklyTraffic:
245+
channel.streamWeeklyTraffic = event.value as int?;
246+
}
202247
}
203248
}
204249

test/example_data.dart

+31
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,37 @@ ReactionEvent reactionEvent(Reaction reaction, ReactionOp op, int messageId) {
650650
);
651651
}
652652

653+
ChannelUpdateEvent channelUpdateEvent(
654+
ZulipStream stream, {
655+
required ChannelPropertyName property,
656+
required Object? value,
657+
}) {
658+
switch (property) {
659+
case ChannelPropertyName.name:
660+
case ChannelPropertyName.description:
661+
assert(value is String);
662+
case ChannelPropertyName.firstMessageId:
663+
assert(value is int?);
664+
case ChannelPropertyName.inviteOnly:
665+
assert(value is bool);
666+
case ChannelPropertyName.messageRetentionDays:
667+
assert(value is int?);
668+
case ChannelPropertyName.channelPostPolicy:
669+
assert(value is ChannelPostPolicy);
670+
case ChannelPropertyName.canRemoveSubscribersGroup:
671+
case ChannelPropertyName.canRemoveSubscribersGroupId:
672+
case ChannelPropertyName.streamWeeklyTraffic:
673+
assert(value is int?);
674+
}
675+
return ChannelUpdateEvent(
676+
id: 1,
677+
streamId: stream.streamId,
678+
name: stream.name,
679+
property: property,
680+
value: value,
681+
);
682+
}
683+
653684
////////////////////////////////////////////////////////////////
654685
// The entire per-account or global state.
655686
//

test/model/channel_test.dart

+11
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ void main() {
5656

5757
await store.addSubscription(eg.subscription(stream1));
5858
checkUnified(store);
59+
60+
await store.handleEvent(eg.channelUpdateEvent(store.streams[stream1.streamId]!,
61+
property: ChannelPropertyName.name, value: 'new stream',
62+
));
63+
checkUnified(store);
64+
65+
await store.handleEvent(eg.channelUpdateEvent(store.streams[stream1.streamId]!,
66+
property: ChannelPropertyName.channelPostPolicy,
67+
value: ChannelPostPolicy.administrators,
68+
));
69+
checkUnified(store);
5970
});
6071
});
6172

0 commit comments

Comments
 (0)