Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions lib/api/model/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ sealed class Event {
case 'update': return UserSettingsUpdateEvent.fromJson(json);
default: return UnexpectedEvent.fromJson(json);
}
case 'device':
switch (json['op'] as String) {
case 'add': return DeviceAddEvent.fromJson(json);
case 'remove': return DeviceRemoveEvent.fromJson(json);
case 'update': return DeviceUpdateEvent.fromJson(json);
default: return UnexpectedEvent.fromJson(json);
}
case 'custom_profile_fields': return CustomProfileFieldsEvent.fromJson(json);
case 'user_group':
switch (json['op'] as String) {
Expand Down Expand Up @@ -210,6 +217,95 @@ class UserSettingsUpdateEvent extends Event {
Map<String, dynamic> toJson() => _$UserSettingsUpdateEventToJson(this);
}

/// A Zulip event of type `device`.
///
/// See API docs starting at:
/// https://zulip.com/api/get-events#device-add
sealed class DeviceEvent extends Event {
@override
@JsonKey(includeToJson: true)
String get type => 'device';

String get op;

final int deviceId;

DeviceEvent({required super.id, required this.deviceId});
}

/// A [DeviceEvent] with op `add`: https://zulip.com/api/get-events#device-add
@JsonSerializable(fieldRename: FieldRename.snake)
class DeviceAddEvent extends DeviceEvent {
@override
@JsonKey(includeToJson: true)
String get op => 'add';

DeviceAddEvent({required super.id, required super.deviceId});

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

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

/// A [DeviceEvent] with op `remove`: https://zulip.com/api/get-events#device-remove
@JsonSerializable(fieldRename: FieldRename.snake)
class DeviceRemoveEvent extends DeviceEvent {
@override
@JsonKey(includeToJson: true)
String get op => 'remove';

DeviceRemoveEvent({required super.id, required super.deviceId});

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

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

/// A [DeviceEvent] with op `update`: https://zulip.com/api/get-events#device-update
@JsonSerializable(fieldRename: FieldRename.snake)
@NullableIntJsonConverter()
@NullableStringJsonConverter()
class DeviceUpdateEvent extends DeviceEvent {
@override
@JsonKey(includeToJson: true)
String get op => 'update';

@JsonKey(readValue: JsonNullable.readIntFromJson)
final JsonNullable<int>? pushKeyId;

@JsonKey(readValue: JsonNullable.readStringFromJson)
final JsonNullable<String>? pushTokenId;

@JsonKey(readValue: JsonNullable.readStringFromJson)
final JsonNullable<String>? pendingPushTokenId;

@JsonKey(readValue: JsonNullable.readIntFromJson)
final JsonNullable<int>? pushTokenLastUpdatedTimestamp;

@JsonKey(readValue: JsonNullable.readStringFromJson)
final JsonNullable<String>? pushRegistrationErrorCode;

DeviceUpdateEvent({
required super.id,
required super.deviceId,
required this.pushKeyId,
required this.pushTokenId,
required this.pendingPushTokenId,
required this.pushTokenLastUpdatedTimestamp,
required this.pushRegistrationErrorCode,
});

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

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

/// A Zulip event of type `custom_profile_fields`: https://zulip.com/api/get-events#custom_profile_fields
@JsonSerializable(fieldRename: FieldRename.snake)
class CustomProfileFieldsEvent extends Event {
Expand Down
112 changes: 102 additions & 10 deletions lib/api/model/events.g.dart

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

3 changes: 3 additions & 0 deletions lib/api/model/initial_snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ class InitialSnapshot {

final List<UserTopicItem> userTopics;

final Map<int, ClientDevice>? devices; // TODO(server-12)

final GroupSettingValue? realmCanDeleteAnyMessageGroup; // TODO(server-10)

final GroupSettingValue? realmCanDeleteOwnMessageGroup; // TODO(server-10)
Expand Down Expand Up @@ -188,6 +190,7 @@ class InitialSnapshot {
required this.userStatuses,
required this.userSettings,
required this.userTopics,
required this.devices,
required this.realmCanDeleteAnyMessageGroup,
required this.realmCanDeleteOwnMessageGroup,
required this.realmDeleteOwnMessagePolicy,
Expand Down
7 changes: 7 additions & 0 deletions lib/api/model/initial_snapshot.g.dart

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

67 changes: 63 additions & 4 deletions lib/api/model/json.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,54 @@ import 'package:json_annotation/json_annotation.dart';
/// the `name` property being absent in JSON;
/// a value of `JsonNullable(null)` represents `'name': null` in JSON;
/// and a value of `JsonNullable("foo")` represents `'name': 'foo'` in JSON.
///
/// To use this class with [JsonSerializable]:
/// * On the field, apply [JsonKey.readValue] with a method from the
/// [readFromJson] family.
/// * On either the field or the class, apply an [IdentityJsonConverter]
/// subclass such as [NullableIntJsonConverter].
/// * Both the read method and the converter need to have a concrete type,
/// not generic in T, because of limitations in `package:json_serializable`.
/// Go ahead and add them for more concrete types whenever needed.
class JsonNullable<T extends Object> {
const JsonNullable(this.value);

final T? value;

/// Reads a [JsonNullable] from a JSON map, as in [JsonKey.readValue].
///
/// The method actually passed to [JsonKey.readValue] needs a concrete type;
/// see the wrapper methods [readIntFromJson] and [readStringFromJson],
/// and add more freely as needed.
///
/// This generic version is useful when writing a custom [JsonKey.readValue]
/// callback for other reasons, as well as for implementing those wrappers.
///
/// Because the [JsonKey.readValue] return value is expected to still be
/// a JSON-like value that needs conversion,
/// the field (or the class) will also need to be annotated with
/// [IdentityJsonConverter] or a subclass.
/// The converter tells `package:json_serializable` how to convert
/// the [JsonNullable] from JSON: namely, by doing nothing.
static JsonNullable<T>? readFromJson<T extends Object>(
Map<dynamic, dynamic> map, String key) {
return map.containsKey(key) ? JsonNullable(map[key] as T?) : null;
}

/// Reads a [JsonNullable<int>] from a JSON map, as in [JsonKey.readValue].
///
/// The field or class will need to be annotated with [NullableIntJsonConverter].
/// See [readFromJson].
static JsonNullable<int>? readIntFromJson(Map<dynamic, dynamic> map, String key) =>
readFromJson<int>(map, key);

/// Reads a [JsonNullable<String>] from a JSON map, as in [JsonKey.readValue].
///
/// The field or class will need to be annotated with [NullableStringJsonConverter].
/// See [readFromJson].
static JsonNullable<String>? readStringFromJson(Map<dynamic, dynamic> map, String key) =>
readFromJson<String>(map, key);

@override
bool operator ==(Object other) {
if (other is! JsonNullable) return false;
Expand All @@ -32,6 +70,19 @@ class JsonNullable<T extends Object> {
int get hashCode => Object.hash('JsonNullable', value);
}

/// "Converts" a value to and from JSON by using the value unmodified.
///
/// This is useful when e.g. a [JsonKey.readValue] callback has already
/// effectively converted the value from JSON,
/// as [JsonNullable.readFromJson] does.
///
/// The converter actually applied as an annotation needs a specific type.
/// Just writing `@IdentityJsonConverter<…>` directly as the annotation
/// doesn't work, as `package:json_serializable` gets confused;
/// instead, use a subclass like [NullableIntJsonConverter],
/// and add new such subclasses whenever needed.
// Possibly related to that issue with a generic converter:
// https://github.com/google/json_serializable.dart/issues/1398
class IdentityJsonConverter<T> extends JsonConverter<T, T> {
const IdentityJsonConverter();

Expand All @@ -42,10 +93,18 @@ class IdentityJsonConverter<T> extends JsonConverter<T, T> {
T toJson(T object) => object;
}

// Make similar IdentityJsonConverter<…> subclasses as needed.
// Just writing `@IdentityJsonConverter<…>` directly as the annotation
// doesn't work, as json_serializable gets confused. Possibly related:
// https://github.com/google/json_serializable.dart/issues/1398
/// "Converts" a [JsonNullable<int>] to and from JSON by using it unmodified.
///
/// This is useful with [JsonNullable.readIntFromJson].
/// See there, and the base class [IdentityJsonConverter].
class NullableIntJsonConverter extends IdentityJsonConverter<JsonNullable<int>> {
const NullableIntJsonConverter();
}

/// "Converts" a [JsonNullable<String>] to and from JSON by using it unmodified.
///
/// This is useful with [JsonNullable.readStringFromJson].
/// See there, and the base class [IdentityJsonConverter].
class NullableStringJsonConverter extends IdentityJsonConverter<JsonNullable<String>> {
const NullableStringJsonConverter();
}
Loading