Skip to content

profile: Implement profile screen for users #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion lib/api/model/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sealed class Event {
case 'update': return UserSettingsUpdateEvent.fromJson(json);
default: return UnexpectedEvent.fromJson(json);
}
case 'custom_profile_fields': return CustomProfileFieldsEvent.fromJson(json);
case 'realm_user':
switch (json['op'] as String) {
case 'add': return RealmUserAddEvent.fromJson(json);
Expand Down Expand Up @@ -130,6 +131,24 @@ class UserSettingsUpdateEvent extends Event {
Map<String, dynamic> toJson() => _$UserSettingsUpdateEventToJson(this);
}

/// A Zulip event of type `custom_profile_fields`: https://zulip.com/api/get-events#custom_profile_fields
@JsonSerializable(fieldRename: FieldRename.snake)
class CustomProfileFieldsEvent extends Event {
@override
@JsonKey(includeToJson: true)
String get type => 'custom_profile_fields';

final List<CustomProfileField> fields;

CustomProfileFieldsEvent({required super.id, required this.fields});

factory CustomProfileFieldsEvent.fromJson(Map<String, dynamic> json) =>
_$CustomProfileFieldsEventFromJson(json);

@override
Map<String, dynamic> toJson() => _$CustomProfileFieldsEventToJson(this);
}

/// A Zulip event of type `realm_user`.
///
/// The corresponding API docs are in several places for
Expand Down Expand Up @@ -211,7 +230,7 @@ class RealmUserUpdateEvent extends RealmUserEvent {
@JsonKey(readValue: _readFromPerson) final int? avatarVersion;
@JsonKey(readValue: _readFromPerson) final String? timezone;
@JsonKey(readValue: _readFromPerson) final int? botOwnerId;
@JsonKey(readValue: _readFromPerson) final int? role; // TODO enum
@JsonKey(readValue: _readFromPerson, unknownEnumValue: UserRole.unknown) final UserRole? role;
@JsonKey(readValue: _readFromPerson) final bool? isBillingAdmin;
@JsonKey(readValue: _readFromPerson) final String? deliveryEmail; // TODO handle JSON `null`
@JsonKey(readValue: _readFromPerson) final RealmUserUpdateCustomProfileField? customProfileField;
Expand Down
30 changes: 29 additions & 1 deletion lib/api/model/events.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 27 additions & 2 deletions lib/api/model/initial_snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class InitialSnapshot {

final List<CustomProfileField> customProfileFields;

// TODO etc., etc.

final List<RecentDmConversation> recentPrivateConversations;

final List<Subscription> subscriptions;
Expand All @@ -41,6 +39,8 @@ class InitialSnapshot {
// TODO(server-5) remove pre-5.0 comment
final UserSettings? userSettings; // TODO(server-5)

final Map<String, RealmDefaultExternalAccount> realmDefaultExternalAccounts;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spelling in commit message:

api: Add realmDefaultExtrenalAccounts


final int maxFileUploadSizeMib;

@JsonKey(readValue: _readUsersIsActiveFallbackTrue)
Expand Down Expand Up @@ -84,6 +84,7 @@ class InitialSnapshot {
required this.unreadMsgs,
required this.streams,
required this.userSettings,
required this.realmDefaultExternalAccounts,
Comment on lines 86 to +87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tree isn't clean on rerunning dart run build_runner build, because the generated file has these in a different order.

required this.maxFileUploadSizeMib,
required this.realmUsers,
required this.realmNonActiveUsers,
Expand All @@ -96,6 +97,30 @@ class InitialSnapshot {
Map<String, dynamic> toJson() => _$InitialSnapshotToJson(this);
}

/// An item in `realm_default_external_accounts`.
///
/// For docs, search for "realm_default_external_accounts:"
/// in <https://zulip.com/api/register-queue>.
@JsonSerializable(fieldRename: FieldRename.snake)
class RealmDefaultExternalAccount {
final String name;
final String text;
final String hint;
final String urlPattern;

RealmDefaultExternalAccount({
required this.name,
required this.text,
required this.hint,
required this.urlPattern,
});

factory RealmDefaultExternalAccount.fromJson(Map<String, dynamic> json) =>
_$RealmDefaultExternalAccountFromJson(json);

Map<String, dynamic> toJson() => _$RealmDefaultExternalAccountToJson(this);
}

/// An item in `recent_private_conversations`.
///
/// For docs, search for "recent_private_conversations:"
Expand Down
24 changes: 24 additions & 0 deletions lib/api/model/initial_snapshot.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 99 additions & 9 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ part 'model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class CustomProfileField {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for moving this out of model.dart and into initial_snapshot.dart?

My inclination would be to keep it in model.dart. As this PR shows, this type appears in events as well as in the initial snapshot. I think of this api/model/model.dart as the place for types that appear across multiple areas of the API, whereas those other two api/model/ files events.dart and initial_snapshot.dart are for types that appear specifically in events and in the initial snapshot respectively. (And types that are specific to other particular endpoints generally go in the respective api/route/*.dart files.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize there were other structures in model.dart that lived in two places (although that is clear now). I only moved it thinking that it was initially constructed in initial_snapshot.dart. Moving it back here.

final int id;
final int type; // TODO enum; also TODO(server-6) a value added
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do move this class's definition, definitely should go in a separate commit from converting this field from int to an enum — that sort of separation helps make both of the changes easier to read and review.

(But I think it's actually already in the right place — see above about what file it goes in, and then within this file I think up at the top is good because it's part of realm-wide settings and not specific to a user or a stream etc.)

@JsonKey(unknownEnumValue: CustomProfileFieldType.unknown)
final CustomProfileFieldType type;
final int order;
final String name;
final String hint;
Expand All @@ -32,17 +33,72 @@ class CustomProfileField {
Map<String, dynamic> toJson() => _$CustomProfileFieldToJson(this);
}

/// As in [CustomProfileField.type].
@JsonEnum(fieldRename: FieldRename.snake, valueField: "apiValue")
enum CustomProfileFieldType {
shortText(apiValue: 1),
longText(apiValue: 2),
choice(apiValue: 3),
date(apiValue: 4),
link(apiValue: 5),
user(apiValue: 6),
externalAccount(apiValue: 7),
pronouns(apiValue: 8), // TODO(server-6) newly added
unknown(apiValue: null);

const CustomProfileFieldType({
required this.apiValue
});

final int? apiValue;

int? toJson() => apiValue;
}

/// An item in the realm-level field data for a "choice" custom profile field.
///
/// The value of [CustomProfileField.fieldData] decodes to a
/// `List<CustomProfileFieldChoiceDataItem>` when
/// the [CustomProfileField.type] is [CustomProfileFieldType.choice].
///
/// TODO(server): This isn't really documented. But see chat thread:
/// https://chat.zulip.org/#narrow/stream/378-api-design/topic/custom.20profile.20fields/near/1383005
@JsonSerializable(fieldRename: FieldRename.snake)
class ProfileFieldUserData {
final String value;
final String? renderedValue;
class CustomProfileFieldChoiceDataItem {
final String text;

ProfileFieldUserData({required this.value, this.renderedValue});
const CustomProfileFieldChoiceDataItem({required this.text});

factory ProfileFieldUserData.fromJson(Map<String, dynamic> json) =>
_$ProfileFieldUserDataFromJson(json);
factory CustomProfileFieldChoiceDataItem.fromJson(Map<String, dynamic> json) =>
_$CustomProfileFieldChoiceDataItemFromJson(json);

Map<String, dynamic> toJson() => _$ProfileFieldUserDataToJson(this);
Map<String, dynamic> toJson() => _$CustomProfileFieldChoiceDataItemToJson(this);

static Map<String, CustomProfileFieldChoiceDataItem> parseFieldDataChoices(Map<String, dynamic> json) =>
json.map((k, v) => MapEntry(k, CustomProfileFieldChoiceDataItem.fromJson(v)));
}

/// The realm-level field data for an "external account" custom profile field.
///
/// This is the decoding of [CustomProfileField.fieldData] when
/// the [CustomProfileField.type] is [CustomProfileFieldType.externalAccount].
///
/// TODO(server): This is undocumented. See chat thread:
/// https://chat.zulip.org/#narrow/stream/378-api-design/topic/external.20account.20custom.20profile.20fields/near/1387213
@JsonSerializable(fieldRename: FieldRename.snake)
class CustomProfileFieldExternalAccountData {
final String subtype;
final String? urlPattern;

const CustomProfileFieldExternalAccountData({
required this.subtype,
required this.urlPattern,
});

factory CustomProfileFieldExternalAccountData.fromJson(Map<String, dynamic> json) =>
_$CustomProfileFieldExternalAccountDataFromJson(json);

Map<String, dynamic> toJson() => _$CustomProfileFieldExternalAccountDataToJson(this);
}

/// As in [InitialSnapshot.realmUsers], [InitialSnapshot.realmNonActiveUsers], and [InitialSnapshot.crossRealmBots].
Expand All @@ -69,7 +125,8 @@ class User {
bool isBot;
int? botType; // TODO enum
int? botOwnerId;
int role; // TODO enum
@JsonKey(unknownEnumValue: UserRole.unknown)
UserRole role;
Comment on lines +128 to +129
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's separate out adding this enum as its own commit.

(I.e. adding the class, changing this field to use it, and other closely-connected changes like in RealmUserUpdateEvent, as one commit; but separate from adding the CustomProfileFieldsEvent type, moving the definition of ProfileFieldUserData, adding the new UI, and so on.)

String timezone;
String? avatarUrl; // TODO distinguish null from missing https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20omitted.20vs.2E.20null.20in.20JSON/near/1551759
int avatarVersion;
Expand Down Expand Up @@ -120,6 +177,39 @@ class User {
Map<String, dynamic> toJson() => _$UserToJson(this);
}

/// As in [User.profileData].
@JsonSerializable(fieldRename: FieldRename.snake)
class ProfileFieldUserData {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah, moving this to come after User is a good idea — the rest of this file is organized in general-to-specific order, so good to match that here.

Let's do that in its own commit, though, or I guess combined with adding that handy line of dartdoc, and perhaps combined with any other code moves of a similar nature. That commit can then also be labeled as NFC.

final String value;
final String? renderedValue;

ProfileFieldUserData({required this.value, this.renderedValue});

factory ProfileFieldUserData.fromJson(Map<String, dynamic> json) =>
_$ProfileFieldUserDataFromJson(json);

Map<String, dynamic> toJson() => _$ProfileFieldUserDataToJson(this);
}

/// As in [User.role].
@JsonEnum(valueField: "apiValue")
enum UserRole{
owner(apiValue: 100),
administrator(apiValue: 200),
moderator(apiValue: 300),
member(apiValue: 400),
guest(apiValue: 600),
unknown(apiValue: null);

const UserRole({
required this.apiValue,
});

final int? apiValue;

int? toJson() => apiValue;
}

/// As in `streams` in the initial snapshot.
///
/// Not called `Stream` because dart:async uses that name.
Expand Down
Loading