Skip to content

Commit 69a1542

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 6c3a8d2 commit 69a1542

File tree

5 files changed

+192
-46
lines changed

5 files changed

+192
-46
lines changed

lib/api/model/initial_snapshot.dart

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

6767
final List<ZulipStream> streams;
6868

69+
// In register-queue, the name of this field is the singular "user_status",
70+
// even though it actually contains user status information for all the users
71+
// that the self-user has access to. Therefore, we prefer to use the plural form.
72+
//
73+
// The API expresses each status as a change from the "zero status" (see
74+
// [UserStatus.zero]), with entries omitted for users whose status is the
75+
// zero status.
76+
@JsonKey(name: 'user_status')
77+
final Map<int, UserStatusChange> userStatuses;
78+
6979
// Servers pre-5.0 don't have `user_settings`, and instead provide whatever
7080
// user settings they support at toplevel in the initial snapshot. Since we're
7181
// likely to desupport pre-5.0 servers before wide release, we prefer to
@@ -154,6 +164,7 @@ class InitialSnapshot {
154164
required this.subscriptions,
155165
required this.unreadMsgs,
156166
required this.streams,
167+
required this.userStatuses,
157168
required this.userSettings,
158169
required this.userTopics,
159170
required this.realmWildcardMentionPolicy,

lib/api/model/initial_snapshot.g.dart

Lines changed: 54 additions & 46 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+
180+
/// A user's status emoji, as in [UserStatus.emoji].
181+
class StatusEmoji {
182+
final String emojiName;
183+
final String emojiCode;
184+
final ReactionType reactionType;
185+
186+
const StatusEmoji({
187+
required this.emojiName,
188+
required this.emojiCode,
189+
required this.reactionType,
190+
}) : assert(emojiName != ''), assert(emojiCode != '');
191+
}
192+
193+
/// A change to part or all of a user's status.
194+
///
195+
/// The absence of one of these means there is no change.
196+
class UserStatusChange {
197+
// final Option<bool> away; // deprecated in server-6 (FL-148); ignore
198+
final Option<String?> text;
199+
final Option<StatusEmoji?> emoji;
200+
201+
const UserStatusChange({required this.text, required this.emoji});
202+
203+
factory UserStatusChange.fromJson(Map<String, dynamic> json) {
204+
return UserStatusChange(
205+
text: _textFromJson(json), emoji: _emojiFromJson(json));
206+
}
207+
208+
static Option<String?> _textFromJson(Map<String, dynamic> json) {
209+
return switch (json['status_text'] as String?) {
210+
null => OptionNone(),
211+
'' => OptionSome(null),
212+
final apiValue => OptionSome(apiValue),
213+
};
214+
}
215+
216+
static Option<StatusEmoji?> _emojiFromJson(Map<String, dynamic> json) {
217+
final reactionType = json['reaction_type'] as String?;
218+
final emojiCode = json['emoji_code'] as String?;
219+
final emojiName = json['emoji_name'] as String?;
220+
221+
if (reactionType == null || emojiCode == null || emojiName == null) {
222+
return OptionNone();
223+
} else if (reactionType == '' || emojiCode == '' || emojiName == '') {
224+
// Sometimes `reaction_type` is 'unicode_emoji' when the emoji is cleared.
225+
// This is an accident, to be handled by looking at `emoji_code` instead:
226+
// https://chat.zulip.org/#narrow/channel/378-api-design/topic/user.20status/near/2203132
227+
return OptionSome(null);
228+
} else {
229+
return OptionSome(StatusEmoji(
230+
reactionType: ReactionType.fromApiValue(reactionType),
231+
emojiCode: emojiCode,
232+
emojiName: emojiName));
233+
}
234+
}
235+
236+
Map<String, dynamic> toJson() {
237+
return {
238+
..._textToJson(text),
239+
..._emojiToJson(emoji),
240+
};
241+
}
242+
243+
Map<String, dynamic> _textToJson(Option<String?> text) {
244+
return {
245+
'status_text': switch (text) {
246+
OptionNone<String?>() => null,
247+
OptionSome<String?>(:var value) => value ?? '',
248+
}
249+
};
250+
}
251+
252+
Map<String, dynamic> _emojiToJson(Option<StatusEmoji?> emoji) {
253+
return switch (emoji) {
254+
OptionNone<StatusEmoji?>() => {
255+
'reaction_type': null,
256+
'emoji_code': null,
257+
'emoji_name': null,
258+
},
259+
OptionSome<StatusEmoji?>(:var value) =>
260+
value == null
261+
? {'reaction_type': '', 'emoji_code': '', 'emoji_name': ''}
262+
: {
263+
'reaction_type': value.reactionType,
264+
'emoji_code': value.emojiCode,
265+
'emoji_name': value.emojiName,
266+
},
267+
};
268+
}
269+
270+
UserStatus apply(UserStatus old) {
271+
return UserStatus(text: text.or(old.text), emoji: emoji.or(old.emoji));
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ import 'package:checks/checks.dart';
22
import 'package:zulip/api/model/model.dart';
33
import 'package:zulip/api/model/submessage.dart';
44

5+
extension UserStatusChecks on Subject<UserStatus> {
6+
Subject<String?> get text => has((x) => x.text, 'text');
7+
Subject<StatusEmoji?> get emoji => has((x) => x.emoji, 'emoji');
8+
}
9+
10+
extension StatusEmojiChecks on Subject<StatusEmoji> {
11+
Subject<String> get emojiName => has((x) => x.emojiName, 'emojiName');
12+
Subject<String> get emojiCode => has((x) => x.emojiCode, 'emojiCode');
13+
Subject<ReactionType> get reactionType => has((x) => x.reactionType, 'reactionType');
14+
}
15+
516
extension UserChecks on Subject<User> {
617
Subject<int> get userId => has((x) => x.userId, 'userId');
718
Subject<String?> get deliveryEmail => has((x) => x.deliveryEmail, 'deliveryEmail');

test/example_data.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,7 @@ InitialSnapshot initialSnapshot({
11131113
List<Subscription>? subscriptions,
11141114
UnreadMessagesSnapshot? unreadMsgs,
11151115
List<ZulipStream>? streams,
1116+
Map<int, UserStatusChange>? userStatuses,
11161117
UserSettings? userSettings,
11171118
List<UserTopicItem>? userTopics,
11181119
RealmWildcardMentionPolicy? realmWildcardMentionPolicy,
@@ -1154,6 +1155,7 @@ InitialSnapshot initialSnapshot({
11541155
subscriptions: subscriptions ?? [], // TODO add subscriptions to default
11551156
unreadMsgs: unreadMsgs ?? _unreadMsgs(),
11561157
streams: streams ?? [], // TODO add streams to default
1158+
userStatuses: userStatuses ?? {},
11571159
userSettings: userSettings ?? UserSettings(
11581160
twentyFourHourTime: false,
11591161
displayEmojiReactionUsers: true,

0 commit comments

Comments
 (0)