Skip to content

Commit b7d9bbc

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 zulip#374.
1 parent 130117a commit b7d9bbc

File tree

8 files changed

+452
-1
lines changed

8 files changed

+452
-1
lines changed

lib/api/model/events.dart

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,6 @@ class Subscription extends ZulipStream {
322322
bool? audibleNotifications;
323323

324324
bool pinToTop;
325-
326325
bool isMuted;
327326
// final bool? inHomeView; // deprecated; ignore
328327

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;
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 setting; 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)