Skip to content

Commit

Permalink
api: Add stream/update event
Browse files Browse the repository at this point in the history
Fixes: #182
  • Loading branch information
sm-sayedi committed Aug 16, 2024
1 parent 16a5759 commit 7a4c2fb
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 3 deletions.
71 changes: 68 additions & 3 deletions lib/api/model/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ sealed class Event {
switch (json['op'] as String) {
case 'create': return ChannelCreateEvent.fromJson(json);
case 'delete': return ChannelDeleteEvent.fromJson(json);
// TODO(#182): case 'update':
case 'update': return ChannelUpdateEvent.fromJson(json);
default: return UnexpectedEvent.fromJson(json);
}
case 'subscription':
Expand Down Expand Up @@ -379,8 +379,73 @@ class ChannelDeleteEvent extends ChannelEvent {
Map<String, dynamic> toJson() => _$ChannelDeleteEventToJson(this);
}

// TODO(#182) ChannelUpdateEvent, for a [ChannelEvent] with op `update`:
// https://zulip.com/api/get-events#stream-update
/// A [ChannelEvent] with op `update`: https://zulip.com/api/get-events#stream-update
@JsonSerializable(fieldRename: FieldRename.snake)
class ChannelUpdateEvent extends ChannelEvent {
@override
String get op => 'update';

final int streamId;
final String name;

/// The name of the channel property, or null if we don't recognize it.
@JsonKey(unknownEnumValue: JsonKey.nullForUndefinedEnumValue)
final ChannelPropertyName? property;

/// The new value, or null if we don't recognize the property.
///
/// This will have the type appropriate for [property]; for example,
/// if the property is boolean, then `value is bool` will always be true.
/// This invariant is enforced by [ChannelUpdateEvent.fromJson].
@JsonKey(readValue: _readValue)
final Object? value;

final String? renderedDescription;
final bool? historyPublicToSubscribers;
final bool? isWebPublic;

ChannelUpdateEvent({
required super.id,
required this.streamId,
required this.name,
required this.property,
required this.value,
this.renderedDescription,
this.historyPublicToSubscribers,
this.isWebPublic,
});

/// [value], with a check that its type corresponds to [property]
/// (e.g., `value as bool`).
static Object? _readValue(Map<dynamic, dynamic> json, String key) {
final value = json['value'];
switch (ChannelPropertyName.fromRawString(json['property'] as String)) {
case ChannelPropertyName.name:
case ChannelPropertyName.description:
return value as String;
case ChannelPropertyName.firstMessageId:
return value as int?;
case ChannelPropertyName.inviteOnly:
return value as bool;
case ChannelPropertyName.messageRetentionDays:
return value as int?;
case ChannelPropertyName.channelPostPolicy:
return ChannelPostPolicy.fromInt(value as int?);
case ChannelPropertyName.canRemoveSubscribersGroup:
case ChannelPropertyName.canRemoveSubscribersGroupId:
case ChannelPropertyName.streamWeeklyTraffic:
return value as int?;
case null:
return null;
}
}

factory ChannelUpdateEvent.fromJson(Map<String, dynamic> json) =>
_$ChannelUpdateEventFromJson(json);

@override
Map<String, dynamic> toJson() => _$ChannelUpdateEventToJson(this);
}

/// A Zulip event of type `subscription`.
///
Expand Down
41 changes: 41 additions & 0 deletions lib/api/model/events.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,45 @@ enum ChannelPostPolicy {
.map((key, value) => MapEntry(value, key));
}

/// The name of the [ZulipStream] properties that gets updated through [ChannelUpdateEvent.property].
///
/// In Zulip event-handling code (for [ChannelUpdateEvent]),
/// we switch exhaustively on a value of this type
/// to ensure that every property change in [ZulipStream] responds to the event.
///
/// Fields on [ZulipStream] not present here:
/// streamId, dateCreated
/// Each of those is immutable on any given channel, and there is no
/// [ChannelUpdateEvent] that updates them.
///
/// Other fields on [ZulipStream] not present here:
/// renderedDescription, historyPublicToSubscribers, isWebPublic
/// Each of those are updated through separate fields of [ChannelUpdateEvent]
/// with the same names.
@JsonEnum(fieldRename: FieldRename.snake, alwaysCreate: true)
enum ChannelPropertyName {
name,
description,
firstMessageId,
inviteOnly,
messageRetentionDays,
@JsonValue('stream_post_policy')
channelPostPolicy,
canRemoveSubscribersGroup,
canRemoveSubscribersGroupId, // TODO(Zulip-6): remove, replaced by canRemoveSubscribersGroup
streamWeeklyTraffic;

/// Get a [ChannelPropertyName] from a raw, snake-case string we recognize, else null.
///
/// Example:
/// 'invite_only' -> ChannelPropertyName.inviteOnly
static ChannelPropertyName? fromRawString(String raw) => _byRawString[raw];

// _$…EnumMap is thanks to `alwaysCreate: true` and `fieldRename: FieldRename.snake`
static final _byRawString = _$ChannelPropertyNameEnumMap
.map((key, value) => MapEntry(value, key));
}

/// As in `subscriptions` in the initial snapshot.
///
/// For docs, search for "subscriptions:"
Expand Down
13 changes: 13 additions & 0 deletions lib/api/model/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions lib/model/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,51 @@ class ChannelStoreImpl with ChannelStore {
streamsByName.remove(stream.name);
subscriptions.remove(stream.streamId);
}

case ChannelUpdateEvent():
final ChannelUpdateEvent(:streamId, name: String streamName) = event;
assert(identical(streams[streamId], streamsByName[streamName]));
assert(subscriptions[streamId] == null
|| identical(subscriptions[streamId], streams[streamId]));

final channel = streams[streamId];
if (channel == null) return;

if (event.renderedDescription != null) {
channel.renderedDescription = event.renderedDescription!;
}
if (event.historyPublicToSubscribers != null) {
channel.historyPublicToSubscribers = event.historyPublicToSubscribers!;
}
if (event.isWebPublic != null) {
channel.isWebPublic = event.isWebPublic!;
}

if (event.property == null) {
// unrecognized property; do nothing
return;
}
switch (event.property!) {
case ChannelPropertyName.name:
channel.name = event.value as String;
streamsByName.remove(streamName);
streamsByName[channel.name] = channel;
case ChannelPropertyName.description:
channel.description = event.value as String;
case ChannelPropertyName.firstMessageId:
channel.firstMessageId = event.value as int?;
case ChannelPropertyName.inviteOnly:
channel.inviteOnly = event.value as bool;
case ChannelPropertyName.messageRetentionDays:
channel.messageRetentionDays = event.value as int?;
case ChannelPropertyName.channelPostPolicy:
channel.channelPostPolicy = event.value as ChannelPostPolicy;
case ChannelPropertyName.canRemoveSubscribersGroup:
case ChannelPropertyName.canRemoveSubscribersGroupId:
channel.canRemoveSubscribersGroup = event.value as int?;
case ChannelPropertyName.streamWeeklyTraffic:
channel.streamWeeklyTraffic = event.value as int?;
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions test/example_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,37 @@ ReactionEvent reactionEvent(Reaction reaction, ReactionOp op, int messageId) {
);
}

ChannelUpdateEvent channelUpdateEvent(
ZulipStream stream, {
required ChannelPropertyName property,
required Object? value,
}) {
switch (property) {
case ChannelPropertyName.name:
case ChannelPropertyName.description:
assert(value is String);
case ChannelPropertyName.firstMessageId:
assert(value is int?);
case ChannelPropertyName.inviteOnly:
assert(value is bool);
case ChannelPropertyName.messageRetentionDays:
assert(value is int?);
case ChannelPropertyName.channelPostPolicy:
assert(value is ChannelPostPolicy);
case ChannelPropertyName.canRemoveSubscribersGroup:
case ChannelPropertyName.canRemoveSubscribersGroupId:
case ChannelPropertyName.streamWeeklyTraffic:
assert(value is int?);
}
return ChannelUpdateEvent(
id: 1,
streamId: stream.streamId,
name: stream.name,
property: property,
value: value,
);
}

////////////////////////////////////////////////////////////////
// The entire per-account or global state.
//
Expand Down
11 changes: 11 additions & 0 deletions test/model/channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ void main() {

await store.addSubscription(eg.subscription(stream1));
checkUnified(store);

await store.handleEvent(eg.channelUpdateEvent(store.streams[stream1.streamId]!,
property: ChannelPropertyName.name, value: 'new stream',
));
checkUnified(store);

await store.handleEvent(eg.channelUpdateEvent(store.streams[stream1.streamId]!,
property: ChannelPropertyName.channelPostPolicy,
value: ChannelPostPolicy.administrators,
));
checkUnified(store);
});
});

Expand Down

0 comments on commit 7a4c2fb

Please sign in to comment.