Skip to content

Commit d907d48

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

File tree

4 files changed

+147
-2
lines changed

4 files changed

+147
-2
lines changed

lib/api/model/events.dart

Lines changed: 63 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,65 @@ class UpdateMessageFlagsMessageDetail {
832834
Map<String, dynamic> toJson() => _$UpdateMessageFlagsMessageDetailToJson(this);
833835
}
834836

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