Skip to content

Commit 17aea85

Browse files
committed
autocomplete: Implement new design for @-mention autocomplete items
Implemented new design for @-mention autocomplete items. Added new `contextMenuItemLabel` and `contextMenuItemMeta` color variables to `designVariables` class. Updated autocomplete tests. Fixes: #913
1 parent 061821c commit 17aea85

File tree

3 files changed

+67
-6
lines changed

3 files changed

+67
-6
lines changed

lib/widgets/autocomplete.dart

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import 'package:flutter/material.dart';
22

3+
import '../api/model/initial_snapshot.dart';
4+
import '../api/model/model.dart';
5+
import '../model/store.dart';
36
import 'content.dart';
47
import 'store.dart';
58
import '../model/autocomplete.dart';
69
import '../model/compose.dart';
710
import '../model/narrow.dart';
811
import 'compose_box.dart';
12+
import 'text.dart';
13+
import 'theme.dart';
914

1015
abstract class AutocompleteField<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult> extends StatefulWidget {
1116
const AutocompleteField({
@@ -193,14 +198,47 @@ class ComposeAutocomplete extends AutocompleteField<MentionAutocompleteQuery, Me
193198
);
194199
}
195200

201+
/// The given user's real email address, if known, for displaying in the UI.
202+
///
203+
/// Returns null if self-user isn't able to see [user]'s real email address.
204+
String? _getDisplayEmailFor(User user, {required PerAccountStore store}) {
205+
if (store.account.zulipFeatureLevel >= 163) { // TODO(server-7)
206+
// A non-null value means self-user has access to [user]'s real email,
207+
// while a null value means it doesn't have access to the email.
208+
// Search for "delivery_email" in https://zulip.com/api/register-queue.
209+
return user.deliveryEmail;
210+
} else {
211+
if (user.deliveryEmail != null) {
212+
// A non-null value means self-user has access to [user]'s real email,
213+
// while a null value doesn't necessarily mean it doesn't have access
214+
// to the email, ....
215+
return user.deliveryEmail;
216+
} else if (store.emailAddressVisibility == EmailAddressVisibility.everyone) {
217+
// ... we have to also check for [PerAccountStore.emailAddressVisibility].
218+
// See:
219+
// * https://github.com/zulip/zulip-mobile/pull/5515#discussion_r997731727
220+
// * https://chat.zulip.org/#narrow/stream/378-api-design/topic/email.20address.20visibility/near/1296133
221+
return user.email;
222+
} else {
223+
return null;
224+
}
225+
}
226+
}
227+
196228
@override
197229
Widget buildItem(BuildContext context, int index, MentionAutocompleteResult option) {
230+
final designVariables = DesignVariables.of(context);
198231
Widget avatar;
199232
String label;
233+
String metaData;
234+
200235
switch (option) {
201236
case UserMentionAutocompleteResult(:var userId):
237+
final store = PerAccountStoreWidget.of(context);
238+
final user = store.users[userId];
202239
avatar = Avatar(userId: userId, size: 32, borderRadius: 3);
203-
label = PerAccountStoreWidget.of(context).users[userId]!.fullName;
240+
label = user!.fullName;
241+
metaData = _getDisplayEmailFor(user, store: store) ?? '';
204242
}
205243
return InkWell(
206244
onTap: () {
@@ -212,8 +250,16 @@ class ComposeAutocomplete extends AutocompleteField<MentionAutocompleteQuery, Me
212250
children: [
213251
avatar,
214252
const SizedBox(width: 8),
215-
Text(label),
216-
])));
253+
Column(
254+
mainAxisSize: MainAxisSize.min,
255+
crossAxisAlignment: CrossAxisAlignment.start,
256+
children: [
257+
Text(label, style: TextStyle(fontSize: 18, height: 1.1, color: designVariables.contextMenuItemLabel) //Creates a line height equavalent to ~20
258+
.merge(weightVariableTextStyle(context, wght: 600))),
259+
Visibility(
260+
visible: metaData.isNotEmpty,
261+
child: Text(metaData, style: TextStyle(height: 1.14, color: designVariables.contextMenuItemMeta)), //Creates a line height equavalent to ~16
262+
)])])));
217263
}
218264
}
219265

lib/widgets/theme.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
131131
subscriptionListHeaderLine: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor(),
132132
subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(),
133133
unreadCountBadgeTextForChannel: Colors.black.withValues(alpha: 0.9),
134+
contextMenuItemLabel: const Color(0xff242631),
135+
contextMenuItemMeta: const Color(0xff626573)
134136
);
135137

136138
DesignVariables.dark() :
@@ -167,6 +169,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
167169
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
168170
subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.75).toColor(),
169171
unreadCountBadgeTextForChannel: Colors.white.withValues(alpha: 0.9),
172+
contextMenuItemLabel: const Color(0xffDFE1E8),
173+
contextMenuItemMeta: const Color(0xff9194A3)
170174
);
171175

172176
DesignVariables._({
@@ -195,6 +199,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
195199
required this.subscriptionListHeaderLine,
196200
required this.subscriptionListHeaderText,
197201
required this.unreadCountBadgeTextForChannel,
202+
required this.contextMenuItemLabel,
203+
required this.contextMenuItemMeta,
198204
});
199205

200206
/// The [DesignVariables] from the context's active theme.
@@ -236,6 +242,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
236242
final Color subscriptionListHeaderLine;
237243
final Color subscriptionListHeaderText;
238244
final Color unreadCountBadgeTextForChannel;
245+
final Color contextMenuItemLabel;
246+
final Color contextMenuItemMeta;
239247

240248
@override
241249
DesignVariables copyWith({
@@ -264,6 +272,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
264272
Color? subscriptionListHeaderLine,
265273
Color? subscriptionListHeaderText,
266274
Color? unreadCountBadgeTextForChannel,
275+
Color? contextMenuItemLabel,
276+
Color? contextMenuItemMeta,
267277
}) {
268278
return DesignVariables._(
269279
background: background ?? this.background,
@@ -291,6 +301,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
291301
subscriptionListHeaderLine: subscriptionListHeaderLine ?? this.subscriptionListHeaderLine,
292302
subscriptionListHeaderText: subscriptionListHeaderText ?? this.subscriptionListHeaderText,
293303
unreadCountBadgeTextForChannel: unreadCountBadgeTextForChannel ?? this.unreadCountBadgeTextForChannel,
304+
contextMenuItemLabel: contextMenuItemLabel ?? this.contextMenuItemLabel,
305+
contextMenuItemMeta: contextMenuItemMeta ?? this.contextMenuItemMeta,
294306
);
295307
}
296308

@@ -325,6 +337,8 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
325337
subscriptionListHeaderLine: Color.lerp(subscriptionListHeaderLine, other.subscriptionListHeaderLine, t)!,
326338
subscriptionListHeaderText: Color.lerp(subscriptionListHeaderText, other.subscriptionListHeaderText, t)!,
327339
unreadCountBadgeTextForChannel: Color.lerp(unreadCountBadgeTextForChannel, other.unreadCountBadgeTextForChannel, t)!,
340+
contextMenuItemMeta: Color.lerp(contextMenuItemMeta, other.contextMenuItemMeta, t)!,
341+
contextMenuItemLabel: Color.lerp(contextMenuItemLabel, other.contextMenuItemLabel, t)!,
328342
);
329343
}
330344
}

test/widgets/autocomplete_test.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,16 @@ void main() {
117117

118118
void checkUserShown(User user, PerAccountStore store, {required bool expected}) {
119119
check(find.text(user.fullName).evaluate().length).equals(expected ? 1 : 0);
120+
check(find.text(user.deliveryEmail!).evaluate().length).equals(expected ? 1 : 0);
120121
final avatarFinder =
121122
findNetworkImage(store.tryResolveUrl(user.avatarUrl!).toString());
122123
check(avatarFinder.evaluate().length).equals(expected ? 1 : 0);
123124
}
124125

125126
testWidgets('options appear, disappear, and change correctly', (tester) async {
126-
final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png');
127-
final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png');
128-
final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png');
127+
final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png', deliveryEmail: '[email protected]');
128+
final user2 = eg.user(userId: 2, fullName: 'User Two', avatarUrl: 'user2.png', deliveryEmail: '[email protected]');
129+
final user3 = eg.user(userId: 3, fullName: 'User Three', avatarUrl: 'user3.png', deliveryEmail: '[email protected]');
129130
final composeInputFinder = await setupToComposeInput(tester, users: [user1, user2, user3]);
130131
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
131132

0 commit comments

Comments
 (0)