Skip to content

Commit 24a7abb

Browse files
committed
wip; nav: Implement main menu sheet redesign
This also updates labelCounterUnread to match its latest value in Figma since we added it initially. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 933370a commit 24a7abb

File tree

7 files changed

+322
-3
lines changed

7 files changed

+322
-3
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,10 @@
545545
"@starredMessagesPageTitle": {
546546
"description": "Title for the page of starred messages."
547547
},
548+
"menuSheetOrganizationsLabel": "Organizations",
549+
"@menuSheetOrganizationsLabel": {
550+
"description": "Label for the button to choose an organization"
551+
},
548552
"notifGroupDmConversationLabel": "{senderFullName} to you and {numOthers, plural, =1{1 other} other{{numOthers} others}}",
549553
"@notifGroupDmConversationLabel": {
550554
"description": "Label for a group DM conversation notification.",

lib/generated/l10n/zulip_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,12 @@ abstract class ZulipLocalizations {
823823
/// **'Starred messages'**
824824
String get starredMessagesPageTitle;
825825

826+
/// Label for the button to choose an organization
827+
///
828+
/// In en, this message translates to:
829+
/// **'Organizations'**
830+
String get menuSheetOrganizationsLabel;
831+
826832
/// Label for a group DM conversation notification.
827833
///
828834
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
431431
@override
432432
String get starredMessagesPageTitle => 'Starred messages';
433433

434+
@override
435+
String get menuSheetOrganizationsLabel => 'Organizations';
436+
434437
@override
435438
String notifGroupDmConversationLabel(String senderFullName, int numOthers) {
436439
String _temp0 = intl.Intl.pluralLogic(

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
431431
@override
432432
String get starredMessagesPageTitle => 'Starred messages';
433433

434+
@override
435+
String get menuSheetOrganizationsLabel => 'Organizations';
436+
434437
@override
435438
String notifGroupDmConversationLabel(String senderFullName, int numOthers) {
436439
String _temp0 = intl.Intl.pluralLogic(

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
431431
@override
432432
String get starredMessagesPageTitle => 'Starred messages';
433433

434+
@override
435+
String get menuSheetOrganizationsLabel => 'Organizations';
436+
434437
@override
435438
String notifGroupDmConversationLabel(String senderFullName, int numOthers) {
436439
String _temp0 = intl.Intl.pluralLogic(

lib/widgets/home.dart

Lines changed: 267 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
2+
import 'dart:async';
3+
14
import 'package:flutter/material.dart';
25

6+
import '../generated/l10n/zulip_localizations.dart';
7+
import '../model/binding.dart';
8+
import 'action_sheet.dart';
9+
import 'app.dart';
310
import 'icons.dart';
411
import 'inbox.dart';
12+
import 'inset_shadow.dart';
513
import 'page.dart';
614
import 'recent_dm_conversations.dart';
15+
import 'store.dart';
716
import 'subscription_list.dart';
17+
import 'text.dart';
818
import 'theme.dart';
919

1020
class HomePage extends StatefulWidget {
@@ -29,7 +39,6 @@ class _HomePageState extends State<HomePage> {
2939
SubscriptionListPageBody(),
3040
// Users
3141
RecentDmConversationsPageBody(),
32-
// Menu
3342
];
3443

3544
Widget Function(int pageIndex) navigationButtonBuilder(IconData icon, String tooltip) {
@@ -48,7 +57,6 @@ class _HomePageState extends State<HomePage> {
4857
navigationButtonBuilder(ZulipIcons.hash_italic, 'Channels'),
4958
// navigationButtonBuilder(ZulipIcons.contacts, 'Users'),
5059
navigationButtonBuilder(ZulipIcons.user, 'Direct messages'),
51-
// navigationButtonBuilder(ZulipIcons.menu, 'Menu'),
5260
];
5361

5462
final designVariables = DesignVariables.of(context);
@@ -73,10 +81,267 @@ class _HomePageState extends State<HomePage> {
7381
children: [
7482
for (final (pageIndex, buildButton) in navigationButtonBuilders.indexed)
7583
Expanded(child: buildButton(pageIndex)),
84+
Expanded(
85+
child: NavigationButton(
86+
icon: ZulipIcons.menu, tooltip: 'Menu', selected: false,
87+
onPressed: () => showMenu(context))),
7688
])))));
7789
}
7890
}
7991

92+
void showMenu(BuildContext context) {
93+
final store = PerAccountStoreWidget.of(context);
94+
final designVariables = DesignVariables.of(context);
95+
final menuItems = <Widget>[
96+
// Search
97+
// const SizedBox(height: 8),
98+
_MenuButton(selected: false, pageContext: context),
99+
_MenuButton(selected: true, pageContext: context),
100+
_MenuButton(selected: false, pageContext: context),
101+
_MenuButton(selected: false, pageContext: context),
102+
_MenuButton(selected: false, pageContext: context),
103+
_MenuButton(selected: false, pageContext: context),
104+
_MenuButton(selected: false, pageContext: context),
105+
_MenuButton(selected: false, pageContext: context),
106+
_MenuButton(selected: false, pageContext: context),
107+
_MenuButton(selected: false, pageContext: context),
108+
_MenuButton(selected: false, pageContext: context),
109+
_MenuButton(selected: false, pageContext: context),
110+
_MenuButton(selected: false, pageContext: context),
111+
_MenuButton(selected: false, pageContext: context),
112+
_MenuButton(selected: false, pageContext: context),
113+
_MenuButton(selected: false, pageContext: context),
114+
// Inbox
115+
// Recent conversations
116+
// Mentions
117+
// Starred messages
118+
// Drafts
119+
// Direct messages
120+
// Streams
121+
// Users
122+
// My profile
123+
// Set my status
124+
// const SizedBox(height: 8),
125+
// Settings
126+
// Notifications
127+
const SizedBox(height: 8),
128+
const _VersionInfo(),
129+
];
130+
131+
showModalBottomSheet<void>(
132+
context: context,
133+
// Clip.hardEdge looks bad; Clip.antiAliasWithSaveLayer looks pixel-perfect
134+
// on my iPhone 13 Pro but is marked as "much slower":
135+
// https://api.flutter.dev/flutter/dart-ui/Clip.html
136+
clipBehavior: Clip.antiAlias,
137+
useSafeArea: true,
138+
isScrollControlled: true,
139+
backgroundColor: designVariables.bgBotBar,
140+
builder: (BuildContext _) {
141+
return SafeArea(
142+
minimum: const EdgeInsets.only(bottom: 8),
143+
child: Column(
144+
crossAxisAlignment: CrossAxisAlignment.stretch,
145+
mainAxisSize: MainAxisSize.max,
146+
children: [
147+
_MenuHeading(title: store.realmUrl.toString()),
148+
Expanded(child: InsetShadowBox(
149+
top: 8, bottom: 8,
150+
color: designVariables.bgBotBar,
151+
child: SingleChildScrollView(
152+
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
153+
child: Column(
154+
mainAxisAlignment: MainAxisAlignment.start,
155+
children: menuItems)))),
156+
const Padding(
157+
padding: EdgeInsets.symmetric(horizontal: 16),
158+
child: Scaled(
159+
scaleEnd: 0.95,
160+
duration: Duration(milliseconds: 100),
161+
child: SizedBox(height: 44, child: ActionSheetCancelButton()))),
162+
]));
163+
});
164+
}
165+
166+
class _MenuHeading extends StatelessWidget {
167+
const _MenuHeading({required this.title});
168+
169+
final String title;
170+
171+
@override
172+
Widget build(BuildContext context) {
173+
final designVariables = DesignVariables.of(context);
174+
175+
return ConstrainedBox(
176+
constraints: const BoxConstraints(minHeight: 46),
177+
child: Padding(
178+
padding: const EdgeInsets.only(top: 6),
179+
child: Row(children: [
180+
// TODO: fetch organization icon and display name from the server settings
181+
Expanded(
182+
child: Padding(
183+
padding: const EdgeInsetsDirectional.fromSTEB(12, 6, 4, 6),
184+
child: Row(spacing: 8, children: [
185+
Icon(ZulipIcons.smile, size: 28, color: designVariables.iconSelected),
186+
Expanded(child: Text(title,
187+
overflow: TextOverflow.ellipsis,
188+
style: TextStyle(
189+
color: designVariables.title,
190+
fontSize: 20,
191+
height: 24 / 20,
192+
).merge(weightVariableTextStyle(context, wght: 600)))),
193+
]))),
194+
ConstrainedBox(constraints: const BoxConstraints(minHeight: 40),
195+
child: const _OrganizationsButton()),
196+
])));
197+
}
198+
}
199+
200+
class _OrganizationsButton extends StatelessWidget {
201+
const _OrganizationsButton();
202+
203+
@override
204+
Widget build(BuildContext context) {
205+
final designVariables = DesignVariables.of(context);
206+
return TextButton(onPressed: () =>
207+
unawaited(Navigator.pushReplacement(
208+
context, MaterialWidgetRoute(page: const ChooseAccountPage()))),
209+
style: TextButton.styleFrom(
210+
// Placeholder color
211+
foregroundColor: designVariables.contextMenuCancelText,
212+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
213+
splashFactory: NoSplash.splashFactory,
214+
padding: const EdgeInsetsDirectional.fromSTEB(8, 7, 14, 7)),
215+
child: Text('Organizations', style: TextStyle(
216+
color: designVariables.icon,
217+
fontSize: 19,
218+
height: 26 / 19,
219+
).merge(weightVariableTextStyle(context, wght: 600))));
220+
}
221+
}
222+
223+
224+
class _MenuButton extends ActionSheetMenuItemButton {
225+
const _MenuButton({required this.selected, required super.pageContext});
226+
227+
final bool selected;
228+
229+
@override
230+
// TODO: implement icon
231+
IconData get icon => ZulipIcons.attach_file;
232+
233+
@override
234+
String label(ZulipLocalizations zulipLocalizations) {
235+
return 'Attach file';
236+
}
237+
238+
@override
239+
void onPressed() {
240+
// TODO: implement onPressed
241+
}
242+
243+
@override
244+
Widget build(BuildContext context) {
245+
final designVariables = DesignVariables.of(context);
246+
final zulipLocalizations = ZulipLocalizations.of(context);
247+
248+
final borderSide = BorderSide(width: 1,
249+
strokeAlign: BorderSide.strokeAlignOutside,
250+
color: designVariables.borderMenuButtonSelected);
251+
final buttonStyle = TextButton.styleFrom(
252+
padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 8),
253+
foregroundColor: designVariables.labelMenuButton,
254+
splashFactory: NoSplash.splashFactory,
255+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
256+
).copyWith(
257+
backgroundColor: WidgetStateColor.fromMap({
258+
WidgetState.pressed: designVariables.bgMenuButtonActive,
259+
~WidgetState.pressed: (selected) ? designVariables.bgMenuButtonSelected
260+
: Colors.transparent,
261+
}),
262+
side: WidgetStateBorderSide.fromMap({
263+
WidgetState.pressed: null,
264+
~WidgetState.pressed: (selected) ? borderSide : null,
265+
}));
266+
267+
return Scaled(
268+
duration: const Duration(milliseconds: 100),
269+
scaleEnd: 0.95,
270+
child: SizedBox(height: 44,
271+
child: TextButton(
272+
onPressed: onPressed,
273+
style: buttonStyle,
274+
child: Row(spacing: 8, children: [
275+
Icon(icon, size: 24,
276+
color: (selected) ? designVariables.iconSelected
277+
: designVariables.icon),
278+
Expanded(child: Text(label(zulipLocalizations),
279+
textAlign: TextAlign.start,
280+
style: const TextStyle(fontSize: 19, height: 26 / 19)
281+
.merge(weightVariableTextStyle(context, wght: (selected) ? 600 : 400)))),
282+
const _MenuItemCounter(value: 100, variant: _MenuItemCounterVariant.quantity),
283+
]))));
284+
}
285+
}
286+
287+
enum _MenuItemCounterVariant {
288+
unreads,
289+
quantity,
290+
}
291+
292+
class _MenuItemCounter extends StatelessWidget {
293+
const _MenuItemCounter({required this.value, required this.variant});
294+
295+
final int value;
296+
final _MenuItemCounterVariant variant;
297+
298+
@override
299+
Widget build(BuildContext context) {
300+
final designVariables = DesignVariables.of(context);
301+
final Color bgColor, textColor;
302+
switch (variant) {
303+
case _MenuItemCounterVariant.unreads:
304+
bgColor = designVariables.bgCounterUnread;
305+
textColor = designVariables.labelCounterUnread;
306+
case _MenuItemCounterVariant.quantity:
307+
bgColor = Colors.transparent;
308+
textColor = designVariables.labelCounterQuantity;
309+
}
310+
return Container(height: 24,
311+
padding: const EdgeInsets.fromLTRB(6, 1, 6, 2),
312+
decoration: ShapeDecoration(color: bgColor,
313+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))),
314+
child: Text(value.toString(), style: TextStyle(
315+
fontSize: 18,
316+
height: 21 / 18,
317+
color: textColor,
318+
).merge(weightVariableTextStyle(context, wght: 600))));
319+
}
320+
}
321+
322+
class _VersionInfo extends StatelessWidget {
323+
const _VersionInfo();
324+
325+
@override
326+
Widget build(BuildContext context) {
327+
final designVariables = DesignVariables.of(context);
328+
final packageInfo = ZulipBinding.instance.syncPackageInfo;
329+
final String versionString;
330+
if (packageInfo == null) {
331+
versionString = 'App Version unknown';
332+
} else {
333+
final PackageInfo(:version, :buildNumber) = packageInfo;
334+
versionString = 'App Version $version+$buildNumber';
335+
}
336+
return Padding(
337+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
338+
child: Text(versionString, style: TextStyle(
339+
fontSize: 17,
340+
height: 24 / 17,
341+
color: designVariables.labelSearchPrompt)));
342+
}
343+
}
344+
80345
class Scaled extends StatefulWidget {
81346
const Scaled({
82347
super.key,

0 commit comments

Comments
 (0)