Skip to content

Commit 2f0aac5

Browse files
sm-sayedignprice
authored andcommitted
api: Add stream/update event
Fixes: #182
1 parent 74748a0 commit 2f0aac5

File tree

7 files changed

+254
-4
lines changed

7 files changed

+254
-4
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.fromApiValue(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

+45
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,51 @@ enum ChannelPostPolicy {
382382
final int? apiValue;
383383

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

387432
/// As in `subscriptions` in the initial snapshot.

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

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

test/example_data.dart

+31
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,37 @@ ReactionEvent reactionEvent(Reaction reaction, ReactionOp op, int messageId) {
655655
);
656656
}
657657

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

test/model/channel_test.dart

+12-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ void main() {
4242
)));
4343
});
4444

45-
test('added by events', () async {
45+
test('added/updated by events', () async {
4646
final stream1 = eg.stream();
4747
final stream2 = eg.stream();
4848
final store = eg.store();
@@ -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)