Skip to content

Commit 112d66a

Browse files
committed
api: Add typing events.
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 2258e63 commit 112d66a

File tree

4 files changed

+146
-2
lines changed

4 files changed

+146
-2
lines changed

lib/api/model/events.dart

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ sealed class Event {
6262
case 'remove': return UpdateMessageFlagsRemoveEvent.fromJson(json);
6363
default: return UnexpectedEvent.fromJson(json);
6464
}
65+
case 'typing': return TypingEvent.fromJson(json);
6566
case 'reaction': return ReactionEvent.fromJson(json);
6667
case 'heartbeat': return HeartbeatEvent.fromJson(json);
6768
// TODO add many more event types
@@ -734,8 +735,9 @@ class DeleteMessageEvent extends Event {
734735
Map<String, dynamic> toJson() => _$DeleteMessageEventToJson(this);
735736
}
736737

737-
/// As in [DeleteMessageEvent.messageType]
738-
/// or [UpdateMessageFlagsMessageDetail.type].
738+
/// As in [DeleteMessageEvent.messageType],
739+
/// [UpdateMessageFlagsMessageDetail.type]
740+
/// or [TypingEvent.messageType]
739741
@JsonEnum(alwaysCreate: true)
740742
enum MessageType {
741743
stream,
@@ -867,6 +869,70 @@ class UpdateMessageFlagsMessageDetail {
867869
Map<String, dynamic> toJson() => _$UpdateMessageFlagsMessageDetailToJson(this);
868870
}
869871

872+
873+
/// A Zulip event of type `typing`:
874+
/// https://zulip.com/api/get-events#typing-start
875+
/// https://zulip.com/api/get-events#typing-stop
876+
@JsonSerializable(fieldRename: FieldRename.snake)
877+
class TypingEvent extends Event {
878+
@override
879+
@JsonKey(includeToJson: true)
880+
String get type => 'typing';
881+
882+
final TypingOp op;
883+
@MessageTypeConverter()
884+
final MessageType messageType;
885+
@JsonKey(readValue: _readSenderId)
886+
final int senderId;
887+
@JsonKey(name: 'recipients', fromJson: _recipientIdsFromJson)
888+
final List<int>? recipientIds;
889+
final int? streamId;
890+
final String? topic;
891+
892+
TypingEvent({
893+
required super.id,
894+
required this.op,
895+
required this.messageType,
896+
required this.senderId,
897+
required this.recipientIds,
898+
required this.streamId,
899+
required this.topic,
900+
});
901+
902+
static Object? _readSenderId(Map<Object?, Object?> json, String key) {
903+
return (json['sender'] as Map<String, dynamic>)['user_id'];
904+
}
905+
906+
static List<int>? _recipientIdsFromJson(Object? json) {
907+
if (json == null) return null;
908+
return (json as List<Object?>).map(
909+
(item) => (item as Map<String, Object?>)['user_id'] as int).toList();
910+
}
911+
912+
factory TypingEvent.fromJson(Map<String, dynamic> json) {
913+
final result = _$TypingEventFromJson(json);
914+
// Crunchy-shell validation
915+
switch (result.messageType) {
916+
case MessageType.stream:
917+
result.streamId as int;
918+
result.topic as String;
919+
case MessageType.direct:
920+
result.recipientIds as List<int>;
921+
}
922+
return result;
923+
}
924+
925+
@override
926+
Map<String, dynamic> toJson() => _$TypingEventToJson(this);
927+
}
928+
929+
/// As in [TypingEvent.op].
930+
@JsonEnum(fieldRename: FieldRename.snake)
931+
enum TypingOp {
932+
start,
933+
stop
934+
}
935+
870936
/// A Zulip event of type `reaction`, with op `add` or `remove`.
871937
///
872938
/// See:

lib/api/model/events.g.dart

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

test/api/model/events_checks.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ extension UpdateMessageEventChecks on Subject<UpdateMessageEvent> {
6060
Subject<bool?> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
6161
}
6262

63+
extension TypingEventChecks on Subject<TypingEvent> {
64+
Subject<MessageType> get messageType => has((e) => e.messageType, 'messageType');
65+
Subject<int> get senderId => has((e) => e.senderId, 'senderId');
66+
Subject<List<int>?> get recipientIds => has((e) => e.recipientIds, 'recipientIds');
67+
Subject<int?> get streamId => has((e) => e.streamId, 'streamId');
68+
Subject<String?> get topic => has((e) => e.topic, 'topic');
69+
}
70+
6371
extension HeartbeatEventChecks on Subject<HeartbeatEvent> {
6472
// No properties not covered by Event.
6573
}

test/api/model/events_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,46 @@ void main() {
194194
.equals(MessageType.direct);
195195
});
196196
});
197+
198+
group('typing status event', () {
199+
final baseJson = {
200+
'id': 1,
201+
'type': 'typing',
202+
'op': 'start',
203+
'sender': {'user_id': 123, 'email': '[email protected]'},
204+
};
205+
206+
final directMessageJson = {
207+
...baseJson,
208+
'message_type': 'direct',
209+
'recipients': [1, 2, 3].map((e) => {'user_id': e, 'email': '$e@example.com'}).toList(),
210+
};
211+
212+
test('direct message typing events', () {
213+
check(TypingEvent.fromJson(directMessageJson))
214+
..recipientIds.isNotNull().deepEquals([1, 2, 3])
215+
..senderId.equals(123);
216+
});
217+
218+
test('private type missing recipient', () {
219+
check(() => TypingEvent.fromJson({
220+
...baseJson, 'message_type': 'private'})).throws<void>();
221+
});
222+
223+
test('private -> direct', () {
224+
check(TypingEvent.fromJson({
225+
...directMessageJson,
226+
'message_type': 'private',
227+
})).messageType.equals(MessageType.direct);
228+
});
229+
230+
test('stream type missing streamId/topic', () {
231+
check(() => TypingEvent.fromJson({
232+
...baseJson, 'message_type': 'stream'})).throws<void>();
233+
check(() => TypingEvent.fromJson({
234+
...baseJson, 'message_type': 'stream', 'topic': 'foo'})).throws<void>();
235+
check(() => TypingEvent.fromJson({
236+
...baseJson, 'message_type': 'stream', 'stream_id': 123})).throws<void>();
237+
});
238+
});
197239
}

0 commit comments

Comments
 (0)