Skip to content

Commit 368df50

Browse files
sm-sayedignpricechrisbobbe
committed
api: Add InitialSnapshot.userStatuses
Co-authored-by: Greg Price <[email protected]> Co-authored-by: Chris Bobbe <[email protected]>
1 parent 0ee6336 commit 368df50

File tree

6 files changed

+265
-47
lines changed

6 files changed

+265
-47
lines changed

lib/api/model/initial_snapshot.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ class InitialSnapshot {
6868

6969
final List<ZulipStream> streams;
7070

71+
// In register-queue, the name of this field is the singular "user_status",
72+
// even though it actually contains user status information for all the users
73+
// that the self-user has access to. Therefore, we prefer to use the plural form.
74+
//
75+
// The API expresses each status as a change from the "zero status" (see
76+
// [UserStatus.zero]), with entries omitted for users whose status is the
77+
// zero status.
78+
@JsonKey(name: 'user_status')
79+
final Map<int, UserStatusChange> userStatuses;
80+
7181
final UserSettings userSettings;
7282

7383
final List<UserTopicItem>? userTopics; // TODO(server-6)
@@ -151,6 +161,7 @@ class InitialSnapshot {
151161
required this.subscriptions,
152162
required this.unreadMsgs,
153163
required this.streams,
164+
required this.userStatuses,
154165
required this.userSettings,
155166
required this.userTopics,
156167
required this.realmWildcardMentionPolicy,

lib/api/model/initial_snapshot.g.dart

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

lib/api/model/model.dart

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:json_annotation/json_annotation.dart';
22

3+
import '../../basic.dart';
34
import '../../model/algorithms.dart';
45
import 'events.dart';
56
import 'initial_snapshot.dart';
@@ -158,6 +159,119 @@ class RealmEmojiItem {
158159
Map<String, dynamic> toJson() => _$RealmEmojiItemToJson(this);
159160
}
160161

162+
/// A user's status, with [text] and [emoji] parts.
163+
///
164+
/// If a part is null, that part is empty/unset.
165+
/// For a [UserStatus] with all parts empty, see [zero].
166+
class UserStatus {
167+
/// The text part (e.g. 'Working remotely'), or null if unset.
168+
///
169+
/// This won't be the empty string.
170+
final String? text;
171+
172+
/// The emoji part, or null if unset.
173+
final StatusEmoji? emoji;
174+
175+
const UserStatus({required this.text, required this.emoji}) : assert(text != '');
176+
177+
static const UserStatus zero = UserStatus(text: null, emoji: null);
178+
179+
@override
180+
bool operator ==(Object other) {
181+
if (other is! UserStatus) return false;
182+
return (text, emoji) == (other.text, other.emoji);
183+
}
184+
185+
@override
186+
int get hashCode => Object.hash(text, emoji);
187+
}
188+
189+
/// A user's status emoji, as in [UserStatus.emoji].
190+
class StatusEmoji {
191+
final String emojiName;
192+
final String emojiCode;
193+
final ReactionType reactionType;
194+
195+
const StatusEmoji({
196+
required this.emojiName,
197+
required this.emojiCode,
198+
required this.reactionType,
199+
}) : assert(emojiName != ''), assert(emojiCode != '');
200+
201+
@override
202+
bool operator ==(Object other) {
203+
if (other is! StatusEmoji) return false;
204+
return (emojiName, emojiCode, reactionType) ==
205+
(other.emojiName, other.emojiCode, other.reactionType);
206+
}
207+
208+
@override
209+
int get hashCode => Object.hash(emojiName, emojiCode, reactionType);
210+
}
211+
212+
/// A change to part or all of a user's status.
213+
///
214+
/// The absence of one of these means there is no change.
215+
class UserStatusChange {
216+
// final Option<bool> away; // deprecated in server-6 (FL-148); ignore
217+
final Option<String?> text;
218+
final Option<StatusEmoji?> emoji;
219+
220+
const UserStatusChange({required this.text, required this.emoji});
221+
222+
UserStatus apply(UserStatus old) {
223+
return UserStatus(text: text.or(old.text), emoji: emoji.or(old.emoji));
224+
}
225+
226+
factory UserStatusChange.fromJson(Map<String, dynamic> json) {
227+
return UserStatusChange(
228+
text: _textFromJson(json), emoji: _emojiFromJson(json));
229+
}
230+
231+
static Option<String?> _textFromJson(Map<String, dynamic> json) {
232+
return switch (json['status_text'] as String?) {
233+
null => OptionNone(),
234+
'' => OptionSome(null),
235+
final apiValue => OptionSome(apiValue),
236+
};
237+
}
238+
239+
static Option<StatusEmoji?> _emojiFromJson(Map<String, dynamic> json) {
240+
final emojiName = json['emoji_name'] as String?;
241+
final emojiCode = json['emoji_code'] as String?;
242+
final reactionType = json['reaction_type'] as String?;
243+
244+
if (emojiName == null || emojiCode == null || reactionType == null) {
245+
return OptionNone();
246+
} else if (emojiName == '' || emojiCode == '' || reactionType == '') {
247+
// Sometimes `reaction_type` is 'unicode_emoji' when the emoji is cleared.
248+
// This is an accident, to be handled by looking at `emoji_code` instead:
249+
// https://chat.zulip.org/#narrow/channel/378-api-design/topic/user.20status/near/2203132
250+
return OptionSome(null);
251+
} else {
252+
return OptionSome(StatusEmoji(
253+
emojiName: emojiName,
254+
emojiCode: emojiCode,
255+
reactionType: ReactionType.fromApiValue(reactionType)));
256+
}
257+
}
258+
259+
Map<String, dynamic> toJson() {
260+
return {
261+
if (text case OptionSome<String?>(:var value))
262+
'status_text': value ?? '',
263+
if (emoji case OptionSome<StatusEmoji?>(:var value))
264+
...value == null
265+
? {'emoji_name': '', 'emoji_code': '', 'reaction_type': ''}
266+
: {
267+
'emoji_name': value.emojiName,
268+
'emoji_code': value.emojiCode,
269+
'reaction_type': value.reactionType,
270+
},
271+
};
272+
}
273+
}
274+
161275
/// The name of a user setting that has a property in [UserSettings].
162276
///
163277
/// In Zulip event-handling code (for [UserSettingsUpdateEvent]),

test/api/model/model_checks.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import 'package:checks/checks.dart';
22
import 'package:zulip/api/model/model.dart';
33
import 'package:zulip/api/model/submessage.dart';
4+
import 'package:zulip/basic.dart';
5+
6+
extension UserStatusChecks on Subject<UserStatus> {
7+
Subject<String?> get text => has((x) => x.text, 'text');
8+
Subject<StatusEmoji?> get emoji => has((x) => x.emoji, 'emoji');
9+
}
10+
11+
extension StatusEmojiChecks on Subject<StatusEmoji> {
12+
Subject<String> get emojiName => has((x) => x.emojiName, 'emojiName');
13+
Subject<String> get emojiCode => has((x) => x.emojiCode, 'emojiCode');
14+
Subject<ReactionType> get reactionType => has((x) => x.reactionType, 'reactionType');
15+
}
16+
17+
extension UserStatusChangeChecks on Subject<UserStatusChange> {
18+
Subject<Option<String?>> get text => has((x) => x.text, 'text');
19+
Subject<Option<StatusEmoji?>> get emoji => has((x) => x.emoji, 'emoji');
20+
}
421

522
extension UserGroupChecks on Subject<UserGroup> {
623
Subject<int> get id => has((x) => x.id, 'id');

test/api/model/model_test.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:checks/checks.dart';
44
import 'package:json_annotation/json_annotation.dart';
55
import 'package:test/scaffolding.dart';
66
import 'package:zulip/api/model/model.dart';
7+
import 'package:zulip/basic.dart';
78

89
import '../../example_data.dart' as eg;
910
import '../../stdlib_checks.dart';
@@ -25,6 +26,71 @@ void main() {
2526
});
2627
});
2728

29+
test('UserStatusChange', () {
30+
void doCheck({
31+
required (String? statusText, String? emojiName,
32+
String? emojiCode, String? reactionType) incoming,
33+
required (Option<String?> text, Option<StatusEmoji?> emoji) expected,
34+
}) {
35+
check(UserStatusChange.fromJson({
36+
'status_text': incoming.$1,
37+
'emoji_name': incoming.$2,
38+
'emoji_code': incoming.$3,
39+
'reaction_type': incoming.$4,
40+
}))
41+
..text.equals(expected.$1)
42+
..emoji.equals(expected.$2);
43+
}
44+
45+
doCheck(
46+
incoming: ('Busy', 'working_on_it', '1f6e0', 'unicode_emoji'),
47+
expected: (OptionSome('Busy'), OptionSome(StatusEmoji(
48+
emojiName: 'working_on_it',
49+
emojiCode: '1f6e0',
50+
reactionType: ReactionType.unicodeEmoji))));
51+
52+
doCheck(
53+
incoming: ('', 'working_on_it', '1f6e0', 'unicode_emoji'),
54+
expected: (OptionSome(null), OptionSome(StatusEmoji(
55+
emojiName: 'working_on_it',
56+
emojiCode: '1f6e0',
57+
reactionType: ReactionType.unicodeEmoji))));
58+
59+
doCheck(
60+
incoming: (null, 'working_on_it', '1f6e0', 'unicode_emoji'),
61+
expected: (OptionNone(), OptionSome(StatusEmoji(
62+
emojiName: 'working_on_it',
63+
emojiCode: '1f6e0',
64+
reactionType: ReactionType.unicodeEmoji))));
65+
66+
doCheck(
67+
incoming: ('Busy', '', '', ''),
68+
expected: (OptionSome('Busy'), OptionSome(null)));
69+
70+
doCheck(
71+
incoming: ('Busy', null, null, null),
72+
expected: (OptionSome('Busy'), OptionNone()));
73+
74+
doCheck(
75+
incoming: ('', '', '', ''),
76+
expected: (OptionSome(null), OptionSome(null)));
77+
78+
doCheck(
79+
incoming: (null, null, null, null),
80+
expected: (OptionNone(), OptionNone()));
81+
82+
// For the API quirk when `reaction_type` is 'unicode_emoji' when the
83+
// emoji is cleared.
84+
doCheck(
85+
incoming: ('', '', '', 'unicode_emoji'),
86+
expected: (OptionSome(null), OptionSome(null)));
87+
88+
// Hardly likely to happen from the API standpoint, but we handle it anyway.
89+
doCheck(
90+
incoming: (null, null, null, 'unicode_emoji'),
91+
expected: (OptionNone(), OptionNone()));
92+
});
93+
2894
group('User', () {
2995
final Map<String, dynamic> baseJson = Map.unmodifiable({
3096
'user_id': 123,

test/example_data.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,7 @@ InitialSnapshot initialSnapshot({
11381138
List<Subscription>? subscriptions,
11391139
UnreadMessagesSnapshot? unreadMsgs,
11401140
List<ZulipStream>? streams,
1141+
Map<int, UserStatusChange>? userStatuses,
11411142
UserSettings? userSettings,
11421143
List<UserTopicItem>? userTopics,
11431144
RealmWildcardMentionPolicy? realmWildcardMentionPolicy,
@@ -1180,6 +1181,7 @@ InitialSnapshot initialSnapshot({
11801181
subscriptions: subscriptions ?? [], // TODO add subscriptions to default
11811182
unreadMsgs: unreadMsgs ?? _unreadMsgs(),
11821183
streams: streams ?? [], // TODO add streams to default
1184+
userStatuses: userStatuses ?? {},
11831185
userSettings: userSettings ?? UserSettings(
11841186
twentyFourHourTime: false,
11851187
displayEmojiReactionUsers: true,

0 commit comments

Comments
 (0)