Skip to content

Commit 3ffb88b

Browse files
committed
api: Add subscription events
Add events for subscription with `op` values of `add`, `remove`, and `update`. `peer_add` and `peer_remove` left for #374.
1 parent f301e0f commit 3ffb88b

File tree

7 files changed

+447
-0
lines changed

7 files changed

+447
-0
lines changed

lib/api/model/events.dart

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ sealed class Event {
3030
case 'update': return RealmUserUpdateEvent.fromJson(json);
3131
default: return UnexpectedEvent.fromJson(json);
3232
}
33+
case 'subscription':
34+
switch (json['op'] as String) {
35+
case 'add': return SubscriptionAddEvent.fromJson(json);
36+
case 'remove': return SubscriptionRemoveEvent.fromJson(json);
37+
case 'update': return SubscriptionUpdateEvent.fromJson(json);
38+
case 'peer_add': return SubscriptionPeerAddEvent.fromJson(json);
39+
case 'peer_remove': return SubscriptionPeerRemoveEvent.fromJson(json);
40+
default: return UnexpectedEvent.fromJson(json);
41+
}
3342
case 'stream':
3443
switch (json['op'] as String) {
3544
case 'create': return StreamCreateEvent.fromJson(json);
@@ -272,6 +281,185 @@ class RealmUserUpdateEvent extends RealmUserEvent {
272281
Map<String, dynamic> toJson() => _$RealmUserUpdateEventToJson(this);
273282
}
274283

284+
/// A Zulip event of type `subscription`.
285+
///
286+
/// The corresponding API docs are in several places for
287+
/// different values of `op`; see subclasses.
288+
sealed class SubscriptionEvent extends Event {
289+
@override
290+
@JsonKey(includeToJson: true)
291+
String get type => 'subscription';
292+
293+
String get op;
294+
295+
SubscriptionEvent({required super.id});
296+
}
297+
298+
/// A [SubscriptionEvent] with op `add`: https://zulip.com/api/get-events#subscription-add
299+
@JsonSerializable(fieldRename: FieldRename.snake)
300+
class SubscriptionAddEvent extends SubscriptionEvent {
301+
@override
302+
@JsonKey(includeToJson: true)
303+
String get op => 'add';
304+
305+
final List<Subscription> subscriptions;
306+
307+
SubscriptionAddEvent({required super.id, required this.subscriptions});
308+
309+
factory SubscriptionAddEvent.fromJson(Map<String, dynamic> json) =>
310+
_$SubscriptionAddEventFromJson(json);
311+
312+
@override
313+
Map<String, dynamic> toJson() => _$SubscriptionAddEventToJson(this);
314+
}
315+
316+
/// A [SubscriptionEvent] with op `remove`: https://zulip.com/api/get-events#subscription-remove
317+
@JsonSerializable(fieldRename: FieldRename.snake)
318+
class SubscriptionRemoveEvent extends SubscriptionEvent {
319+
@override
320+
@JsonKey(includeToJson: true)
321+
String get op => 'remove';
322+
323+
@JsonKey(readValue: _readStreamIds)
324+
final List<int> streamIds;
325+
326+
static List<int> _readStreamIds(Map json, String key) {
327+
return (json['subscriptions'] as List<dynamic>)
328+
.map((e) => (e as Map<String, dynamic>)['stream_id'] as int)
329+
.toList();
330+
}
331+
332+
SubscriptionRemoveEvent({required super.id, required this.streamIds});
333+
334+
factory SubscriptionRemoveEvent.fromJson(Map<String, dynamic> json) =>
335+
_$SubscriptionRemoveEventFromJson(json);
336+
337+
@override
338+
Map<String, dynamic> toJson() => _$SubscriptionRemoveEventToJson(this);
339+
}
340+
341+
/// A [SubscriptionEvent] with op `update`: https://zulip.com/api/get-events#subscription-update
342+
@JsonSerializable(fieldRename: FieldRename.snake)
343+
class SubscriptionUpdateEvent extends SubscriptionEvent {
344+
@override
345+
@JsonKey(includeToJson: true)
346+
String get op => 'update';
347+
348+
final int streamId;
349+
350+
final SubscriptionProperty property;
351+
352+
/// The new value, or null if we don't recognize the setting.
353+
///
354+
/// This will have the type appropriate for [property]; for example,
355+
/// if the setting is boolean, then `value is bool` will always be true.
356+
/// This invariant is enforced by [SubscriptionUpdateEvent.fromJson].
357+
@JsonKey(readValue: _readValue)
358+
final Object? value;
359+
360+
/// [value], with a check that its type corresponds to [property]
361+
/// (e.g., `value as bool`).
362+
static Object? _readValue(Map json, String key) {
363+
final value = json['value'];
364+
switch (SubscriptionProperty.fromRawString(json['property'] as String)) {
365+
case SubscriptionProperty.color:
366+
return value as String;
367+
case SubscriptionProperty.isMuted:
368+
case SubscriptionProperty.inHomeView:
369+
case SubscriptionProperty.pinToTop:
370+
case SubscriptionProperty.desktopNotifications:
371+
case SubscriptionProperty.audibleNotifications:
372+
case SubscriptionProperty.pushNotifications:
373+
case SubscriptionProperty.emailNotifications:
374+
case SubscriptionProperty.wildcardMentionsNotify:
375+
return value as bool;
376+
case SubscriptionProperty.unknown:
377+
return null;
378+
}
379+
}
380+
381+
SubscriptionUpdateEvent({
382+
required super.id,
383+
required this.streamId,
384+
required this.property,
385+
required this.value,
386+
});
387+
388+
factory SubscriptionUpdateEvent.fromJson(Map<String, dynamic> json) =>
389+
_$SubscriptionUpdateEventFromJson(json);
390+
391+
@override
392+
Map<String, dynamic> toJson() => _$SubscriptionUpdateEventToJson(this);
393+
}
394+
395+
/// The name of a property in [Subscription].
396+
///
397+
/// Used in handling of [SubscriptionUpdateEvent].
398+
@JsonEnum(fieldRename: FieldRename.snake, alwaysCreate: true)
399+
enum SubscriptionProperty {
400+
color,
401+
isMuted,
402+
inHomeView,
403+
pinToTop,
404+
desktopNotifications,
405+
audibleNotifications,
406+
pushNotifications,
407+
emailNotifications,
408+
wildcardMentionsNotify,
409+
unknown;
410+
411+
static SubscriptionProperty fromRawString(String raw) => _byRawString[raw] ?? unknown;
412+
413+
static final _byRawString = _$SubscriptionPropertyEnumMap
414+
.map((key, value) => MapEntry(value, key));
415+
}
416+
417+
/// A [SubscriptionEvent] with op `peer_add`: https://zulip.com/api/get-events#subscription-peer_add
418+
@JsonSerializable(fieldRename: FieldRename.snake)
419+
class SubscriptionPeerAddEvent extends SubscriptionEvent {
420+
@override
421+
@JsonKey(includeToJson: true)
422+
String get op => 'peer_add';
423+
424+
List<int> streamIds;
425+
List<int> userIds;
426+
427+
SubscriptionPeerAddEvent({
428+
required super.id,
429+
required this.streamIds,
430+
required this.userIds,
431+
});
432+
433+
factory SubscriptionPeerAddEvent.fromJson(Map<String, dynamic> json) =>
434+
_$SubscriptionPeerAddEventFromJson(json);
435+
436+
@override
437+
Map<String, dynamic> toJson() => _$SubscriptionPeerAddEventToJson(this);
438+
}
439+
440+
/// A [SubscriptionEvent] with op `peer_remove`: https://zulip.com/api/get-events#subscription-peer_remove
441+
@JsonSerializable(fieldRename: FieldRename.snake)
442+
class SubscriptionPeerRemoveEvent extends SubscriptionEvent {
443+
@override
444+
@JsonKey(includeToJson: true)
445+
String get op => 'peer_remove';
446+
447+
List<int> streamIds;
448+
List<int> userIds;
449+
450+
SubscriptionPeerRemoveEvent({
451+
required super.id,
452+
required this.streamIds,
453+
required this.userIds,
454+
});
455+
456+
factory SubscriptionPeerRemoveEvent.fromJson(Map<String, dynamic> json) =>
457+
_$SubscriptionPeerRemoveEventFromJson(json);
458+
459+
@override
460+
Map<String, dynamic> toJson() => _$SubscriptionPeerRemoveEventToJson(this);
461+
}
462+
275463
/// A Zulip event of type `stream`.
276464
///
277465
/// The corresponding API docs are in several places for

lib/api/model/events.g.dart

Lines changed: 110 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/store.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,52 @@ class PerAccountStore extends ChangeNotifier {
305305
subscriptions.remove(stream.streamId);
306306
}
307307
notifyListeners();
308+
} else if (event is SubscriptionAddEvent) {
309+
assert(debugLog("server event: subscription/add"));
310+
for (final subscription in event.subscriptions) {
311+
subscriptions[subscription.streamId] = subscription;
312+
}
313+
notifyListeners();
314+
} else if (event is SubscriptionRemoveEvent) {
315+
assert(debugLog("server event: subscription/remove"));
316+
for (final streamId in event.streamIds) {
317+
subscriptions.remove(streamId);
318+
}
319+
notifyListeners();
320+
} else if (event is SubscriptionUpdateEvent) {
321+
assert(debugLog("server event: subscription/update"));
322+
final subscription = subscriptions[event.streamId];
323+
if (subscription == null) return; // TODO(log)
324+
switch (event.property) {
325+
case SubscriptionProperty.color:
326+
subscription.color = event.value as String;
327+
case SubscriptionProperty.isMuted:
328+
subscription.isMuted = event.value as bool;
329+
case SubscriptionProperty.inHomeView:
330+
subscription.isMuted = !(event.value as bool);
331+
case SubscriptionProperty.pinToTop:
332+
subscription.pinToTop = event.value as bool;
333+
case SubscriptionProperty.desktopNotifications:
334+
subscription.desktopNotifications = event.value as bool;
335+
case SubscriptionProperty.audibleNotifications:
336+
subscription.audibleNotifications = event.value as bool;
337+
case SubscriptionProperty.pushNotifications:
338+
subscription.pushNotifications = event.value as bool;
339+
case SubscriptionProperty.emailNotifications:
340+
subscription.emailNotifications = event.value as bool;
341+
case SubscriptionProperty.wildcardMentionsNotify:
342+
subscription.wildcardMentionsNotify = event.value as bool;
343+
case SubscriptionProperty.unknown:
344+
// unrecognized property; do nothing
345+
return;
346+
}
347+
notifyListeners();
348+
} else if (event is SubscriptionPeerAddEvent) {
349+
assert(debugLog("server event: subscription/peer_add"));
350+
// TODO(#374): handle event
351+
} else if (event is SubscriptionPeerRemoveEvent) {
352+
assert(debugLog("server event: subscription/peer_remove"));
353+
// TODO(#374): handle event
308354
} else if (event is MessageEvent) {
309355
assert(debugLog("server event: message ${jsonEncode(event.message.toJson())}"));
310356
recentDmConversationsView.handleMessageEvent(event);

test/api/model/events_checks.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ extension AlertWordsEventChecks on Subject<AlertWordsEvent> {
1515
Subject<List<String>> get alertWords => has((e) => e.alertWords, 'alertWords');
1616
}
1717

18+
extension SubscriptionRemoveEventChecks on Subject<SubscriptionRemoveEvent> {
19+
Subject<List<int>> get streamIds => has((e) => e.streamIds, 'streamIds');
20+
}
21+
1822
extension MessageEventChecks on Subject<MessageEvent> {
1923
Subject<Message> get message => has((e) => e.message, 'message');
2024
}

0 commit comments

Comments
 (0)