Skip to content

Commit 7149a64

Browse files
sirpengignprice
authored andcommitted
api: Complete support for CustomProfileField
Change `CustomProfileField.type` into a proper enum, add support for server event `custom_profile_fields`.
1 parent a20f120 commit 7149a64

File tree

6 files changed

+184
-2
lines changed

6 files changed

+184
-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: 70 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,74 @@ 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+
/// An item in the realm-level field data for a "choice" custom profile field.
59+
///
60+
/// The value of [CustomProfileField.fieldData] decodes to a
61+
/// `List<CustomProfileFieldChoiceDataItem>` when
62+
/// the [CustomProfileField.type] is [CustomProfileFieldType.choice].
63+
///
64+
/// TODO(server): This isn't really documented. But see chat thread:
65+
/// https://chat.zulip.org/#narrow/stream/378-api-design/topic/custom.20profile.20fields/near/1383005
66+
@JsonSerializable(fieldRename: FieldRename.snake)
67+
class CustomProfileFieldChoiceDataItem {
68+
final String text;
69+
70+
const CustomProfileFieldChoiceDataItem({required this.text});
71+
72+
factory CustomProfileFieldChoiceDataItem.fromJson(Map<String, dynamic> json) =>
73+
_$CustomProfileFieldChoiceDataItemFromJson(json);
74+
75+
Map<String, dynamic> toJson() => _$CustomProfileFieldChoiceDataItemToJson(this);
76+
77+
static Map<String, CustomProfileFieldChoiceDataItem> parseFieldDataChoices(Map<String, dynamic> json) =>
78+
json.map((k, v) => MapEntry(k, CustomProfileFieldChoiceDataItem.fromJson(v)));
79+
}
80+
81+
/// The realm-level field data for an "external account" custom profile field.
82+
///
83+
/// This is the decoding of [CustomProfileField.fieldData] when
84+
/// the [CustomProfileField.type] is [CustomProfileFieldType.externalAccount].
85+
///
86+
/// TODO(server): This is undocumented. See chat thread:
87+
/// https://chat.zulip.org/#narrow/stream/378-api-design/topic/external.20account.20custom.20profile.20fields/near/1387213
88+
@JsonSerializable(fieldRename: FieldRename.snake)
89+
class CustomProfileFieldExternalAccountData {
90+
final String subtype;
91+
final String? urlPattern;
92+
93+
const CustomProfileFieldExternalAccountData({
94+
required this.subtype,
95+
required this.urlPattern,
96+
});
97+
98+
factory CustomProfileFieldExternalAccountData.fromJson(Map<String, dynamic> json) =>
99+
_$CustomProfileFieldExternalAccountDataFromJson(json);
100+
101+
Map<String, dynamic> toJson() => _$CustomProfileFieldExternalAccountDataToJson(this);
102+
}
103+
35104
/// As in [InitialSnapshot.realmUsers], [InitialSnapshot.realmNonActiveUsers], and [InitialSnapshot.crossRealmBots].
36105
///
37106
/// 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;
@@ -321,6 +327,21 @@ class PerAccountStore extends ChangeNotifier {
321327
// https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M3881.20Sending.20outbox.20messages.20is.20fraught.20with.20issues/near/1405739
322328
return _apiSendMessage(connection, destination: destination, content: content);
323329
}
330+
331+
static List<CustomProfileField> _sortCustomProfileFields(List<CustomProfileField> initialCustomProfileFields) {
332+
// TODO(server): The realm-wide field objects have an `order` property,
333+
// but the actual API appears to be that the fields should be shown in
334+
// the order they appear in the array (`custom_profile_fields` in the
335+
// API; our `realmFields` array here.) See chat thread:
336+
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/custom.20profile.20fields/near/1382982
337+
//
338+
// We go on to put at the start of the list any fields that are marked for
339+
// displaying in the "profile summary". (Possibly they should be at the
340+
// start of the list in the first place, but make sure just in case.)
341+
final displayFields = initialCustomProfileFields.where((e) => e.displayInProfileSummary == true);
342+
final nonDisplayFields = initialCustomProfileFields.where((e) => e.displayInProfileSummary != true);
343+
return displayFields.followedBy(nonDisplayFields).toList();
344+
}
324345
}
325346

326347
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
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';
@@ -7,6 +9,20 @@ import '../../stdlib_checks.dart';
79
import 'model_checks.dart';
810

911
void main() {
12+
test('CustomProfileFieldChoiceDataItem', () {
13+
const input = '''{
14+
"0": {"text": "Option 0", "order": 1},
15+
"1": {"text": "Option 1", "order": 2},
16+
"2": {"text": "Option 2", "order": 3}
17+
}''';
18+
final choices = CustomProfileFieldChoiceDataItem.parseFieldDataChoices(jsonDecode(input));
19+
check(choices).jsonEquals({
20+
'0': const CustomProfileFieldChoiceDataItem(text: 'Option 0'),
21+
'1': const CustomProfileFieldChoiceDataItem(text: 'Option 1'),
22+
'2': const CustomProfileFieldChoiceDataItem(text: 'Option 2'),
23+
});
24+
});
25+
1026
group('User', () {
1127
final Map<String, dynamic> baseJson = Map.unmodifiable({
1228
'user_id': 123,

0 commit comments

Comments
 (0)