Skip to content

Commit b725b5e

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

File tree

4 files changed

+150
-2
lines changed

4 files changed

+150
-2
lines changed

lib/api/model/events.dart

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ sealed class Event {
6161
case 'remove': return UpdateMessageFlagsRemoveEvent.fromJson(json);
6262
default: return UnexpectedEvent.fromJson(json);
6363
}
64+
case 'typing': return TypingEvent.fromJson(json);
6465
case 'reaction': return ReactionEvent.fromJson(json);
6566
case 'heartbeat': return HeartbeatEvent.fromJson(json);
6667
// TODO add many more event types
@@ -715,8 +716,9 @@ class DeleteMessageEvent extends Event {
715716
Map<String, dynamic> toJson() => _$DeleteMessageEventToJson(this);
716717
}
717718

718-
/// As in [DeleteMessageEvent.messageType]
719-
/// or [UpdateMessageFlagsMessageDetail.type].
719+
/// As in [DeleteMessageEvent.messageType],
720+
/// [UpdateMessageFlagsMessageDetail.type]
721+
/// or [TypingEvent.messageType]
720722
@JsonEnum(fieldRename: FieldRename.snake)
721723
enum MessageType {
722724
stream,
@@ -832,6 +834,68 @@ class UpdateMessageFlagsMessageDetail {
832834
Map<String, dynamic> toJson() => _$UpdateMessageFlagsMessageDetailToJson(this);
833835
}
834836

837+
838+
/// A Zulip event of type `typing`:
839+
/// https://zulip.com/api/get-events#typing-start
840+
/// https://zulip.com/api/get-events#typing-stop
841+
@JsonSerializable(fieldRename: FieldRename.snake)
842+
class TypingEvent extends Event {
843+
@override
844+
@JsonKey(includeToJson: true)
845+
String get type => 'typing';
846+
847+
final TypingOp op;
848+
final MessageType messageType;
849+
@JsonKey(name: 'sender', readValue: _readSenderId)
850+
final int senderId;
851+
@JsonKey(name: 'recipients', fromJson: _recipientIdsFromJson)
852+
final List<int>? recipientIds;
853+
final int? streamId;
854+
final String? topic;
855+
856+
TypingEvent({
857+
required super.id,
858+
required this.op,
859+
required this.messageType,
860+
required this.senderId,
861+
required this.recipientIds,
862+
required this.streamId,
863+
required this.topic,
864+
});
865+
866+
static dynamic _readSenderId(Map<dynamic, dynamic> json, String key) {
867+
return json[key]['user_id'];
868+
}
869+
870+
static List<int>? _recipientIdsFromJson(dynamic json) {
871+
if (json == null) return null;
872+
return (json as List<dynamic>).map((item) => item['user_id'] as int).toList();
873+
}
874+
875+
factory TypingEvent.fromJson(Map<String, dynamic> json) {
876+
final result = _$TypingEventFromJson(json);
877+
// Crunchy-shell validation
878+
switch (result.messageType) {
879+
case MessageType.stream:
880+
result.streamId as int;
881+
result.topic as String;
882+
case MessageType.private:
883+
result.recipientIds as List<int>;
884+
}
885+
return result;
886+
}
887+
888+
@override
889+
Map<String, dynamic> toJson() => _$TypingEventToJson(this);
890+
}
891+
892+
/// As in [TypingEvent.op].
893+
@JsonEnum(fieldRename: FieldRename.snake)
894+
enum TypingOp {
895+
start,
896+
stop
897+
}
898+
835899
/// A Zulip event of type `reaction`, with op `add` or `remove`.
836900
///
837901
/// See:

lib/api/model/events.g.dart

Lines changed: 27 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
@@ -46,6 +46,14 @@ extension UpdateMessageEventChecks on Subject<UpdateMessageEvent> {
4646
Subject<bool?> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
4747
}
4848

49+
extension TypingEventChecks on Subject<TypingEvent> {
50+
Subject<MessageType> get messageType => has((e) => e.messageType, 'messageType');
51+
Subject<int> get senderId => has((e) => e.senderId, 'senderId');
52+
Subject<List<int>?> get recipientIds => has((e) => e.recipientIds, 'recipientIds');
53+
Subject<int?> get streamId => has((e) => e.streamId, 'streamId');
54+
Subject<String?> get topic => has((e) => e.topic, 'topic');
55+
}
56+
4957
extension HeartbeatEventChecks on Subject<HeartbeatEvent> {
5058
// No properties not covered by Event.
5159
}

test/api/model/events_test.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,53 @@ void main() {
150150
'message_details': {'123': {'type': 'private', 'mentioned': false, 'user_ids': [2]}},
151151
})).returnsNormally();
152152
});
153+
154+
group('typing status event', () {
155+
final baseJson = {
156+
'id': 1,
157+
'type': 'typing',
158+
'op': 'start',
159+
'sender': {'user_id': 123, 'email': '[email protected]'},
160+
};
161+
162+
test('direct message typing events', () {
163+
final directMessageJson = {
164+
...baseJson,
165+
'message_type': 'private',
166+
'recipients': [1, 2, 3].map((e) => {'user_id': e, 'email': '$e@example.com'}).toList(),
167+
};
168+
check(TypingEvent.fromJson(directMessageJson))
169+
..recipientIds.isNotNull().deepEquals([1,2,3])
170+
..senderId.equals(123);
171+
check(() => TypingEvent.fromJson(directMessageJson)).returnsNormally();
172+
check(() => TypingEvent.fromJson({
173+
...directMessageJson, 'op': 'stop'})).returnsNormally();
174+
});
175+
176+
test('private type missing recipient', () {
177+
check(() => TypingEvent.fromJson({
178+
...baseJson, 'message_type': 'private'})).throws<void>();
179+
});
180+
181+
test('stream message typing events', () {
182+
final streamMessageJson = {
183+
...baseJson,
184+
'message_type': 'stream',
185+
'stream_id': 123,
186+
'topic': 'foo',
187+
};
188+
check(() => TypingEvent.fromJson(streamMessageJson)).returnsNormally();
189+
check(() => TypingEvent.fromJson({
190+
...streamMessageJson, 'op': 'stop'})).returnsNormally();
191+
});
192+
193+
test('stream type missing streamId/topic', () {
194+
check(() => TypingEvent.fromJson({
195+
...baseJson, 'message_type': 'stream'})).throws<void>();
196+
check(() => TypingEvent.fromJson({
197+
...baseJson, 'message_type': 'stream', 'topic': 'foo'})).throws<void>();
198+
check(() => TypingEvent.fromJson({
199+
...baseJson, 'message_type': 'stream', 'stream_id': 0})).throws<void>();
200+
});
201+
});
153202
}

0 commit comments

Comments
 (0)