Skip to content

Commit 93ce9c5

Browse files
committed
api: Add stream/update event
Fixes: #182
1 parent 4ddbeab commit 93ce9c5

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
@@ -149,6 +149,51 @@ class ChannelStoreImpl with ChannelStore {
149149
streamsByName.remove(stream.name);
150150
subscriptions.remove(stream.streamId);
151151
}
152+
153+
case ChannelUpdateEvent():
154+
final ChannelUpdateEvent(:streamId, name: String streamName) = event;
155+
assert(identical(streams[streamId], streamsByName[streamName]));
156+
assert(subscriptions[streamId] == null
157+
|| identical(subscriptions[streamId], streams[streamId]));
158+
159+
final channel = streams[streamId];
160+
if (channel == null) return;
161+
162+
if (event.renderedDescription != null) {
163+
channel.renderedDescription = event.renderedDescription!;
164+
}
165+
if (event.historyPublicToSubscribers != null) {
166+
channel.historyPublicToSubscribers = event.historyPublicToSubscribers!;
167+
}
168+
if (event.isWebPublic != null) {
169+
channel.isWebPublic = event.isWebPublic!;
170+
}
171+
172+
if (event.property == null) {
173+
// unrecognized property; do nothing
174+
return;
175+
}
176+
switch (event.property!) {
177+
case ChannelPropertyName.name:
178+
channel.name = event.value as String;
179+
streamsByName.remove(streamName);
180+
streamsByName[channel.name] = channel;
181+
case ChannelPropertyName.description:
182+
channel.description = event.value as String;
183+
case ChannelPropertyName.firstMessageId:
184+
channel.firstMessageId = event.value as int?;
185+
case ChannelPropertyName.inviteOnly:
186+
channel.inviteOnly = event.value as bool;
187+
case ChannelPropertyName.messageRetentionDays:
188+
channel.messageRetentionDays = event.value as int?;
189+
case ChannelPropertyName.channelPostPolicy:
190+
channel.channelPostPolicy = event.value as ChannelPostPolicy;
191+
case ChannelPropertyName.canRemoveSubscribersGroup:
192+
case ChannelPropertyName.canRemoveSubscribersGroupId:
193+
channel.canRemoveSubscribersGroup = event.value as int?;
194+
case ChannelPropertyName.streamWeeklyTraffic:
195+
channel.streamWeeklyTraffic = event.value as int?;
196+
}
152197
}
153198
}
154199

test/example_data.dart

+31
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,37 @@ ReactionEvent reactionEvent(Reaction reaction, ReactionOp op, int messageId) {
614614
);
615615
}
616616

617+
ChannelUpdateEvent channelUpdateEvent(
618+
ZulipStream stream, {
619+
required ChannelPropertyName property,
620+
required Object? value,
621+
}) {
622+
switch (property) {
623+
case ChannelPropertyName.name:
624+
case ChannelPropertyName.description:
625+
assert(value is String);
626+
case ChannelPropertyName.firstMessageId:
627+
assert(value is int?);
628+
case ChannelPropertyName.inviteOnly:
629+
assert(value is bool);
630+
case ChannelPropertyName.messageRetentionDays:
631+
assert(value is int?);
632+
case ChannelPropertyName.channelPostPolicy:
633+
assert(value is ChannelPostPolicy);
634+
case ChannelPropertyName.canRemoveSubscribersGroup:
635+
case ChannelPropertyName.canRemoveSubscribersGroupId:
636+
case ChannelPropertyName.streamWeeklyTraffic:
637+
assert(value is int?);
638+
}
639+
return ChannelUpdateEvent(
640+
id: 1,
641+
streamId: stream.streamId,
642+
name: stream.name,
643+
property: property,
644+
value: value,
645+
);
646+
}
647+
617648
////////////////////////////////////////////////////////////////
618649
// The entire per-account or global state.
619650
//

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)