From 0b733eea1deaacb8e2b9bba15ae614cfbc24a463 Mon Sep 17 00:00:00 2001 From: Sayed Mahmood Sayedi Date: Tue, 6 Aug 2024 08:51:31 +0430 Subject: [PATCH 1/3] api: Add `emailAddressVisibility` to `InitialSnapshot` This is for backward compatibility to the Zulip server versions prior to Zulip 7.0 (FL 163) where there was realm-level `email_address_visibility` policy for which a user can see other user's real email address. Search for "email_address_visibility" in https://zulip.com/api/register-queue. This will become handy in the next commit(s) where the user's real email is shown in it's profile page. Note: This field is removed in Zulip 7.0 (FL 163) and replaced with: * https://zulip.com/api/update-settings#parameter-email_address_visibility * https://zulip.com/api/update-realm-user-settings-defaults#parameter-email_address_visibility --- lib/api/model/initial_snapshot.dart | 19 +++++++++++++++++++ lib/api/model/initial_snapshot.g.dart | 12 ++++++++++++ test/example_data.dart | 2 ++ 3 files changed, 33 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 70010dac76..4e25d953b0 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -24,6 +24,16 @@ class InitialSnapshot { final List customProfileFields; + /// The realm-level policy, on pre-FL 163 servers, for visibility of real email addresses. + /// + /// Search for "email_address_visibility" in https://zulip.com/api/register-queue. + /// + /// This field is removed in Zulip 7.0 (FL 163) and replaced with a user-level + /// setting: + /// * https://zulip.com/api/update-settings#parameter-email_address_visibility + /// * https://zulip.com/api/update-realm-user-settings-defaults#parameter-email_address_visibility + final EmailAddressVisibility? emailAddressVisibility; // TODO(server-7): remove + // TODO(server-8): Remove the default values. @JsonKey(defaultValue: 15000) final int serverTypingStartedExpiryPeriodMilliseconds; @@ -94,6 +104,7 @@ class InitialSnapshot { required this.zulipMergeBase, required this.alertWords, required this.customProfileFields, + required this.emailAddressVisibility, required this.serverTypingStartedExpiryPeriodMilliseconds, required this.serverTypingStoppedWaitPeriodMilliseconds, required this.serverTypingStartedWaitPeriodMilliseconds, @@ -117,6 +128,14 @@ class InitialSnapshot { Map toJson() => _$InitialSnapshotToJson(this); } +enum EmailAddressVisibility { + @JsonValue(1) everyone, + @JsonValue(2) members, + @JsonValue(3) admins, + @JsonValue(4) nobody, + @JsonValue(5) moderators, +} + /// An item in `realm_default_external_accounts`. /// /// For docs, search for "realm_default_external_accounts:" diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index 19415e6da2..01dc1ea850 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -21,6 +21,8 @@ InitialSnapshot _$InitialSnapshotFromJson(Map json) => customProfileFields: (json['custom_profile_fields'] as List) .map((e) => CustomProfileField.fromJson(e as Map)) .toList(), + emailAddressVisibility: $enumDecodeNullable( + _$EmailAddressVisibilityEnumMap, json['email_address_visibility']), serverTypingStartedExpiryPeriodMilliseconds: (json['server_typing_started_expiry_period_milliseconds'] as num?) ?.toInt() ?? @@ -86,6 +88,8 @@ Map _$InitialSnapshotToJson(InitialSnapshot instance) => 'zulip_merge_base': instance.zulipMergeBase, 'alert_words': instance.alertWords, 'custom_profile_fields': instance.customProfileFields, + 'email_address_visibility': + _$EmailAddressVisibilityEnumMap[instance.emailAddressVisibility], 'server_typing_started_expiry_period_milliseconds': instance.serverTypingStartedExpiryPeriodMilliseconds, 'server_typing_stopped_wait_period_milliseconds': @@ -106,6 +110,14 @@ Map _$InitialSnapshotToJson(InitialSnapshot instance) => 'cross_realm_bots': instance.crossRealmBots, }; +const _$EmailAddressVisibilityEnumMap = { + EmailAddressVisibility.everyone: 1, + EmailAddressVisibility.members: 2, + EmailAddressVisibility.admins: 3, + EmailAddressVisibility.nobody: 4, + EmailAddressVisibility.moderators: 5, +}; + RealmDefaultExternalAccount _$RealmDefaultExternalAccountFromJson( Map json) => RealmDefaultExternalAccount( diff --git a/test/example_data.dart b/test/example_data.dart index 90f52c8b72..c191aaf8b8 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -575,6 +575,7 @@ InitialSnapshot initialSnapshot({ String? zulipMergeBase, List? alertWords, List? customProfileFields, + EmailAddressVisibility? emailAddressVisibility, int? serverTypingStartedExpiryPeriodMilliseconds, int? serverTypingStoppedWaitPeriodMilliseconds, int? serverTypingStartedWaitPeriodMilliseconds, @@ -599,6 +600,7 @@ InitialSnapshot initialSnapshot({ zulipMergeBase: zulipMergeBase ?? recentZulipVersion, alertWords: alertWords ?? ['klaxon'], customProfileFields: customProfileFields ?? [], + emailAddressVisibility: EmailAddressVisibility.everyone, serverTypingStartedExpiryPeriodMilliseconds: serverTypingStartedExpiryPeriodMilliseconds ?? 15000, serverTypingStoppedWaitPeriodMilliseconds: From 506a89c85cf3d09e4e743b1a19db0d3473f66e28 Mon Sep 17 00:00:00 2001 From: Sayed Mahmood Sayedi Date: Tue, 6 Aug 2024 08:54:41 +0430 Subject: [PATCH 2/3] store: Add `emailAddressVisibility` to `PerAccountStore` --- lib/model/store.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/model/store.dart b/lib/model/store.dart index 94653b11bf..4237524e61 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -236,6 +236,7 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore { realmDefaultExternalAccounts: initialSnapshot.realmDefaultExternalAccounts, realmEmoji: initialSnapshot.realmEmoji, customProfileFields: _sortCustomProfileFields(initialSnapshot.customProfileFields), + emailAddressVisibility: initialSnapshot.emailAddressVisibility, accountId: accountId, selfUserId: account.userId, userSettings: initialSnapshot.userSettings, @@ -269,6 +270,7 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore { required this.realmDefaultExternalAccounts, required this.realmEmoji, required this.customProfileFields, + required this.emailAddressVisibility, required this.accountId, required this.selfUserId, required this.userSettings, @@ -311,6 +313,8 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore { final Map realmDefaultExternalAccounts; Map realmEmoji; List customProfileFields; + /// For docs, please see [InitialSnapshot.emailAddressVisibility]. + final EmailAddressVisibility? emailAddressVisibility; // TODO(#668): update this realm setting //////////////////////////////// // Data attached to the self-account on the realm. From ba952c364cc0667a0a9526e8f356acdb8adcd2f4 Mon Sep 17 00:00:00 2001 From: Sayed Mahmood Sayedi Date: Mon, 5 Aug 2024 12:07:00 +0430 Subject: [PATCH 3/3] profile: Display user email Fixes: #291 --- lib/widgets/profile.dart | 35 +++++++++++++++++++++++++++++++++- test/widgets/profile_test.dart | 4 +++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/widgets/profile.dart b/lib/widgets/profile.dart index afadc289b0..08326c541c 100644 --- a/lib/widgets/profile.dart +++ b/lib/widgets/profile.dart @@ -3,9 +3,11 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; +import '../api/model/initial_snapshot.dart'; import '../api/model/model.dart'; import '../model/content.dart'; import '../model/narrow.dart'; +import '../model/store.dart'; import 'content.dart'; import 'message_list.dart'; import 'page.dart'; @@ -33,6 +35,33 @@ class ProfilePage extends StatelessWidget { page: ProfilePage(userId: userId)); } + /// The given user's real email address, if known, for displaying in the UI. + /// + /// Returns null if self-user isn't able to see [user]'s real email address. + String? _getDisplayEmailFor(User user, {required PerAccountStore store}) { + if (store.account.zulipFeatureLevel >= 163) { // TODO(server-7) + // A non-null value means self-user has access to [user]'s real email, + // while a null value means it doesn't have access to the email. + // Search for "delivery_email" in https://zulip.com/api/register-queue. + return user.deliveryEmail; + } else { + if (user.deliveryEmail != null) { + // A non-null value means self-user has access to [user]'s real email, + // while a null value doesn't necessarily mean it doesn't have access + // to the email, .... + return user.deliveryEmail; + } else if (store.emailAddressVisibility == EmailAddressVisibility.everyone) { + // ... we have to also check for [PerAccountStore.emailAddressVisibility]. + // See: + // * https://github.com/zulip/zulip-mobile/pull/5515#discussion_r997731727 + // * https://chat.zulip.org/#narrow/stream/378-api-design/topic/email.20address.20visibility/near/1296133 + return user.email; + } else { + return null; + } + } + } + @override Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); @@ -42,6 +71,7 @@ class ProfilePage extends StatelessWidget { return const _ProfileErrorPage(); } + final displayEmail = _getDisplayEmailFor(user, store: store); final items = [ Center( child: Avatar(userId: userId, size: 200, borderRadius: 200 / 8)), @@ -50,7 +80,10 @@ class ProfilePage extends StatelessWidget { textAlign: TextAlign.center, style: _TextStyles.primaryFieldText .merge(weightVariableTextStyle(context, wght: 700))), - // TODO(#291) render email field + if (displayEmail != null) + Text(displayEmail, + textAlign: TextAlign.center, + style: _TextStyles.primaryFieldText), Text(roleToLabel(user.role, zulipLocalizations), textAlign: TextAlign.center, style: _TextStyles.primaryFieldText), diff --git a/test/widgets/profile_test.dart b/test/widgets/profile_test.dart index 55e90f6340..76435d32ab 100644 --- a/test/widgets/profile_test.dart +++ b/test/widgets/profile_test.dart @@ -71,12 +71,14 @@ void main() { group('ProfilePage', () { testWidgets('page builds; profile page renders', (WidgetTester tester) async { - final user = eg.user(userId: 1, fullName: 'test user'); + final user = eg.user(userId: 1, fullName: 'test user', + deliveryEmail: 'testuser@example.com'); await setupPage(tester, users: [user], pageUserId: user.userId); check(because: 'find user avatar', find.byType(Avatar).evaluate()).length.equals(1); check(because: 'find user name', find.text('test user').evaluate()).isNotEmpty(); + check(because: 'find user delivery email', find.text('testuser@example.com').evaluate()).isNotEmpty(); }); testWidgets('page builds; profile page renders with profileData', (WidgetTester tester) async {