Skip to content

Commit eddb1ba

Browse files
committed
api: Handle typing events.
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 3fc1af6 commit eddb1ba

File tree

4 files changed

+150
-2
lines changed

4 files changed

+150
-2
lines changed

lib/api/model/events.dart

+66-2
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
@@ -733,8 +734,9 @@ class DeleteMessageEvent extends Event {
733734
Map<String, dynamic> toJson() => _$DeleteMessageEventToJson(this);
734735
}
735736

736-
/// As in [DeleteMessageEvent.messageType]
737-
/// or [UpdateMessageFlagsMessageDetail.type].
737+
/// As in [DeleteMessageEvent.messageType],
738+
/// [UpdateMessageFlagsMessageDetail.type]
739+
/// or [TypingEvent.messageType]
738740
@JsonEnum(fieldRename: FieldRename.snake)
739741
enum MessageType {
740742
stream,
@@ -850,6 +852,68 @@ class UpdateMessageFlagsMessageDetail {
850852
Map<String, dynamic> toJson() => _$UpdateMessageFlagsMessageDetailToJson(this);
851853
}
852854

855+
856+
/// A Zulip event of type `typing`:
857+
/// https://zulip.com/api/get-events#typing-start
858+
/// https://zulip.com/api/get-events#typing-stop
859+
@JsonSerializable(fieldRename: FieldRename.snake)
860+
class TypingEvent extends Event {
861+
@override
862+
@JsonKey(includeToJson: true)
863+
String get type => 'typing';
864+
865+
final TypingOp op;
866+
final MessageType messageType;
867+
@JsonKey(name: 'sender', readValue: _readSenderId)
868+
final int senderId;
869+
@JsonKey(name: 'recipients', fromJson: _recipientIdsFromJson)
870+
final List<int>? recipientIds;
871+
final int? streamId;
872+
final String? topic;
873+
874+
TypingEvent({
875+
required super.id,
876+
required this.op,
877+
required this.messageType,
878+
required this.senderId,
879+
required this.recipientIds,
880+
required this.streamId,
881+
required this.topic,
882+
});
883+
884+
static dynamic _readSenderId(Map<dynamic, dynamic> json, String key) {
885+
return json[key]['user_id'];
886+
}
887+
888+
static List<int>? _recipientIdsFromJson(dynamic json) {
889+
if (json == null) return null;
890+
return (json as List<dynamic>).map((item) => item['user_id'] as int).toList();
891+
}
892+
893+
factory TypingEvent.fromJson(Map<String, dynamic> json) {
894+
final result = _$TypingEventFromJson(json);
895+
// Crunchy-shell validation
896+
switch (result.messageType) {
897+
case MessageType.stream:
898+
result.streamId as int;
899+
result.topic as String;
900+
case MessageType.private:
901+
result.recipientIds as List<int>;
902+
}
903+
return result;
904+
}
905+
906+
@override
907+
Map<String, dynamic> toJson() => _$TypingEventToJson(this);
908+
}
909+
910+
/// As in [TypingEvent.op].
911+
@JsonEnum(fieldRename: FieldRename.snake)
912+
enum TypingOp {
913+
start,
914+
stop
915+
}
916+
853917
/// A Zulip event of type `reaction`, with op `add` or `remove`.
854918
///
855919
/// See:

lib/api/model/events.g.dart

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/api/model/events_checks.dart

+8
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

+49
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,53 @@ void main() {
172172
'message_details': {'123': {'type': 'private', 'mentioned': false, 'user_ids': [2]}},
173173
})).returnsNormally();
174174
});
175+
176+
group('typing status event', () {
177+
final baseJson = {
178+
'id': 1,
179+
'type': 'typing',
180+
'op': 'start',
181+
'sender': {'user_id': 123, 'email': '[email protected]'},
182+
};
183+
184+
test('direct message typing events', () {
185+
final directMessageJson = {
186+
...baseJson,
187+
'message_type': 'private',
188+
'recipients': [1, 2, 3].map((e) => {'user_id': e, 'email': '$e@example.com'}).toList(),
189+
};
190+
check(TypingEvent.fromJson(directMessageJson))
191+
..recipientIds.isNotNull().deepEquals([1,2,3])
192+
..senderId.equals(123);
193+
check(() => TypingEvent.fromJson(directMessageJson)).returnsNormally();
194+
check(() => TypingEvent.fromJson({
195+
...directMessageJson, 'op': 'stop'})).returnsNormally();
196+
});
197+
198+
test('private type missing recipient', () {
199+
check(() => TypingEvent.fromJson({
200+
...baseJson, 'message_type': 'private'})).throws<void>();
201+
});
202+
203+
test('stream message typing events', () {
204+
final streamMessageJson = {
205+
...baseJson,
206+
'message_type': 'stream',
207+
'stream_id': 123,
208+
'topic': 'foo',
209+
};
210+
check(() => TypingEvent.fromJson(streamMessageJson)).returnsNormally();
211+
check(() => TypingEvent.fromJson({
212+
...streamMessageJson, 'op': 'stop'})).returnsNormally();
213+
});
214+
215+
test('stream type missing streamId/topic', () {
216+
check(() => TypingEvent.fromJson({
217+
...baseJson, 'message_type': 'stream'})).throws<void>();
218+
check(() => TypingEvent.fromJson({
219+
...baseJson, 'message_type': 'stream', 'topic': 'foo'})).throws<void>();
220+
check(() => TypingEvent.fromJson({
221+
...baseJson, 'message_type': 'stream', 'stream_id': 0})).throws<void>();
222+
});
223+
});
175224
}

0 commit comments

Comments
 (0)