Skip to content

Commit 51aee6a

Browse files
committed
api: Complete support for CustomProfileField
Change `CustomProfileField.type` into a proper enum, add support for server event `custom_profile_fields`.
1 parent 963eb8f commit 51aee6a

File tree

6 files changed

+181
-2
lines changed

6 files changed

+181
-2
lines changed

lib/api/model/events.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ sealed class Event {
2222
case 'update': return UserSettingsUpdateEvent.fromJson(json);
2323
default: return UnexpectedEvent.fromJson(json);
2424
}
25+
case 'custom_profile_fields': return CustomProfileFieldsEvent.fromJson(json);
2526
case 'realm_user':
2627
switch (json['op'] as String) {
2728
case 'add': return RealmUserAddEvent.fromJson(json);
@@ -130,6 +131,24 @@ class UserSettingsUpdateEvent extends Event {
130131
Map<String, dynamic> toJson() => _$UserSettingsUpdateEventToJson(this);
131132
}
132133

134+
/// A Zulip event of type `custom_profile_fields`: https://zulip.com/api/get-events#custom_profile_fields
135+
@JsonSerializable(fieldRename: FieldRename.snake)
136+
class CustomProfileFieldsEvent extends Event {
137+
@override
138+
@JsonKey(includeToJson: true)
139+
String get type => 'custom_profile_fields';
140+
141+
final List<CustomProfileField> fields;
142+
143+
CustomProfileFieldsEvent({required super.id, required this.fields});
144+
145+
factory CustomProfileFieldsEvent.fromJson(Map<String, dynamic> json) =>
146+
_$CustomProfileFieldsEventFromJson(json);
147+
148+
@override
149+
Map<String, dynamic> toJson() => _$CustomProfileFieldsEventToJson(this);
150+
}
151+
133152
/// A Zulip event of type `realm_user`.
134153
///
135154
/// The corresponding API docs are in several places for

lib/api/model/events.g.dart

Lines changed: 17 additions & 0 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: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ part 'model.g.dart';
99
@JsonSerializable(fieldRename: FieldRename.snake)
1010
class CustomProfileField {
1111
final int id;
12-
final int type; // TODO enum; also TODO(server-6) a value added
12+
@JsonKey(unknownEnumValue: CustomProfileFieldType.unknown)
13+
final CustomProfileFieldType type;
1314
final int order;
1415
final String name;
1516
final String hint;
@@ -32,6 +33,73 @@ class CustomProfileField {
3233
Map<String, dynamic> toJson() => _$CustomProfileFieldToJson(this);
3334
}
3435

36+
/// As in [CustomProfileField.type].
37+
@JsonEnum(fieldRename: FieldRename.snake, valueField: "apiValue")
38+
enum CustomProfileFieldType {
39+
shortText(apiValue: 1),
40+
longText(apiValue: 2),
41+
choice(apiValue: 3),
42+
date(apiValue: 4),
43+
link(apiValue: 5),
44+
user(apiValue: 6),
45+
externalAccount(apiValue: 7),
46+
pronouns(apiValue: 8), // TODO(server-6) newly added
47+
unknown(apiValue: null);
48+
49+
const CustomProfileFieldType({
50+
required this.apiValue
51+
});
52+
53+
final int? apiValue;
54+
55+
int? toJson() => apiValue;
56+
}
57+
58+
/// The realm-level field data item for a "choice" custom profile field.
59+
///
60+
/// This is the decoding of values of [CustomProfileField.fieldData] when
61+
/// the [CustomProfileField.type] is [CustomProfileFieldType.choice].
62+
///
63+
/// TODO(server): This isn't really documented. But see chat thread:
64+
/// https://chat.zulip.org/#narrow/stream/378-api-design/topic/custom.20profile.20fields/near/1383005
65+
@JsonSerializable(fieldRename: FieldRename.snake)
66+
class CustomProfileFieldChoiceDataItem {
67+
final String text;
68+
69+
const CustomProfileFieldChoiceDataItem({required this.text});
70+
71+
factory CustomProfileFieldChoiceDataItem.fromJson(Map<String, dynamic> json) =>
72+
_$CustomProfileFieldChoiceDataItemFromJson(json);
73+
74+
Map<String, dynamic> toJson() => _$CustomProfileFieldChoiceDataItemToJson(this);
75+
76+
static Map<String, CustomProfileFieldChoiceDataItem> parseFieldDataChoices(Map<String, dynamic> json) =>
77+
json.map((k, v) => MapEntry(k, CustomProfileFieldChoiceDataItem.fromJson(v)));
78+
}
79+
80+
/// The realm-level field data for an "external account" custom profile field.
81+
///
82+
/// This is the decoding of [CustomProfileField.fieldData] when
83+
/// the [CustomProfileField.type] is [CustomProfileFieldType.externalAccount].
84+
///
85+
/// TODO(server): This is undocumented. See chat thread:
86+
/// https://chat.zulip.org/#narrow/stream/378-api-design/topic/external.20account.20custom.20profile.20fields/near/1387213
87+
@JsonSerializable(fieldRename: FieldRename.snake)
88+
class CustomProfileFieldExternalAccountData {
89+
final String subtype;
90+
final String? urlPattern;
91+
92+
const CustomProfileFieldExternalAccountData({
93+
required this.subtype,
94+
required this.urlPattern,
95+
});
96+
97+
factory CustomProfileFieldExternalAccountData.fromJson(Map<String, dynamic> json) =>
98+
_$CustomProfileFieldExternalAccountDataFromJson(json);
99+
100+
Map<String, dynamic> toJson() => _$CustomProfileFieldExternalAccountDataToJson(this);
101+
}
102+
35103
/// As in [InitialSnapshot.realmUsers], [InitialSnapshot.realmNonActiveUsers], and [InitialSnapshot.crossRealmBots].
36104
///
37105
/// In the Zulip API, the items in realm_users, realm_non_active_users, and

lib/api/model/model.g.dart

Lines changed: 41 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/store.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ class PerAccountStore extends ChangeNotifier {
152152
}) : zulipVersion = initialSnapshot.zulipVersion,
153153
maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib,
154154
realmDefaultExternalAccounts = initialSnapshot.realmDefaultExternalAccounts,
155+
customProfileFields = _sortCustomProfileFields(initialSnapshot.customProfileFields),
155156
userSettings = initialSnapshot.userSettings,
156157
users = Map.fromEntries(
157158
initialSnapshot.realmUsers
@@ -174,6 +175,7 @@ class PerAccountStore extends ChangeNotifier {
174175
final String zulipVersion; // TODO get from account; update there on initial snapshot
175176
final int maxFileUploadSizeMib; // No event for this.
176177
final Map<String, RealmDefaultExternalAccount> realmDefaultExternalAccounts;
178+
List<CustomProfileField> customProfileFields;
177179

178180
// Data attached to the self-account on the realm.
179181
final UserSettings? userSettings; // TODO(server-5)
@@ -236,6 +238,10 @@ class PerAccountStore extends ChangeNotifier {
236238
userSettings?.emojiset = event.value as Emojiset;
237239
}
238240
notifyListeners();
241+
} else if (event is CustomProfileFieldsEvent) {
242+
assert(debugLog("server event: custom_profile_fields"));
243+
customProfileFields = _sortCustomProfileFields(event.fields);
244+
notifyListeners();
239245
} else if (event is RealmUserAddEvent) {
240246
assert(debugLog("server event: realm_user/add"));
241247
users[event.person.userId] = event.person;
@@ -317,6 +323,21 @@ class PerAccountStore extends ChangeNotifier {
317323
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M3881.20Sending.20outbox.20messages.20is.20fraught.20with.20issues/near/1405739
318324
return _apiSendMessage(connection, destination: destination, content: content);
319325
}
326+
327+
static List<CustomProfileField> _sortCustomProfileFields(List<CustomProfileField> initialCustomProfileFields) {
328+
// TODO(server): The realm-wide field objects have an `order` property,
329+
// but the actual API appears to be that the fields should be shown in
330+
// the order they appear in the array (`custom_profile_fields` in the
331+
// API; our `realmFields` array here.) See chat thread:
332+
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/custom.20profile.20fields/near/1382982
333+
//
334+
// We go on to put at the start of the list any fields that are marked for
335+
// displaying in the "profile summary". (Possibly they should be at the
336+
// start of the list in the first place, but make sure just in case.)
337+
final displayFields = initialCustomProfileFields.where((e) => e.displayInProfileSummary == true);
338+
final nonDisplayFields = initialCustomProfileFields.where((e) => e.displayInProfileSummary != true);
339+
return displayFields.followedBy(nonDisplayFields).toList();
340+
}
320341
}
321342

322343
const _apiSendMessage = sendMessage; // Bit ugly; for alternatives, see: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20PerAccountStore.20methods/near/1545809

test/api/model/model_test.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1+
import 'dart:convert';
2+
13
import 'package:checks/checks.dart';
24
import 'package:test/scaffolding.dart';
35
import 'package:zulip/api/model/model.dart';
46

57
import '../../example_data.dart' as eg;
68

79
void main() {
10+
group('CustomProfileFieldChoiceDataItem', () {
11+
const input = '''{
12+
"0": {"text": "Option 0", "order": 1},
13+
"1": {"text": "Option 1", "order": 2},
14+
"2": {"text": "Option 2", "order": 3}
15+
}''';
16+
final Map<String, CustomProfileFieldChoiceDataItem> choices = CustomProfileFieldChoiceDataItem.parseFieldDataChoices(jsonDecode(input));
17+
check(choices['0']?.text).equals('Option 0');
18+
check(choices['1']?.text).equals('Option 1');
19+
check(choices['2']?.text).equals('Option 2');
20+
});
21+
822
group('User', () {
923
final Map<String, dynamic> baseJson = Map.unmodifiable({
1024
'user_id': 123,

0 commit comments

Comments
 (0)