Skip to content

Commit 902b63e

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

File tree

4 files changed

+145
-2
lines changed

4 files changed

+145
-2
lines changed

lib/api/model/events.dart

+67-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
@@ -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,69 @@ class UpdateMessageFlagsMessageDetail {
867869
Map<String, dynamic> toJson() => _$UpdateMessageFlagsMessageDetailToJson(this);
868870
}
869871

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

lib/api/model/events.g.dart

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

+42
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)