diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 1f52ac32ba..9ae3da06c6 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -43,6 +43,10 @@ "@switchAccountButton": { "description": "Label for main-menu button leading to the choose-account page." }, + "organizationsButton": "Organizations", + "@organizationsButton": { + "description": "Label for main-menu header button leading to the choose-account page." + }, "tryAnotherAccountMessage": "Your account at {url} is taking a while to load.", "@tryAnotherAccountMessage": { "description": "Message that appears on the loading screen after waiting for some time.", diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index d1d40b2010..f437d7022b 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -197,6 +197,12 @@ abstract class ZulipLocalizations { /// **'Switch account'** String get switchAccountButton; + /// Label for main-menu header button leading to the choose-account page. + /// + /// In en, this message translates to: + /// **'Organizations'** + String get organizationsButton; + /// Message that appears on the loading screen after waiting for some time. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index 1a292881ec..7dae9ed007 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get switchAccountButton => 'تبديل الحساب'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index 83cc660772..cddd0efe59 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get switchAccountButton => 'Konto wechseln'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Dein Account bei $url benötigt einige Zeit zum Laden.'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 80c1c2c475..f38f968fd2 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get switchAccountButton => 'Switch account'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index 7572aa5d3f..41b201c348 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -44,6 +44,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get switchAccountButton => 'Changer de compte'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Votre compte à $url prend du temps à se charger.'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index e42ef1f08e..0adae52cea 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get switchAccountButton => 'Cambia account'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Il caricamento dell\'account su $url sta richiedendo un po\' di tempo.'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index eae3a0c3eb..7961caa81e 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -42,6 +42,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get switchAccountButton => 'アカウントを切り替える'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return '$url のアカウントの読み込みに時間がかかっています。'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index cc2c5e1852..c64b6572d3 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get switchAccountButton => 'Switch account'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index c7c14cf77e..149c380007 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get switchAccountButton => 'Przełącz konto'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Twoje konto na $url wymaga jeszcze chwili na załadowanie.'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index ae8ce816c4..3a93839f46 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get switchAccountButton => 'Сменить учетную запись'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Ваша учетная запись на $url загружается медленно.'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index ca321e40a6..551e30fcb0 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get switchAccountButton => 'Zmeniť účet'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Načítavanie vášho konta na adrese $url chvílu trvá.'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index 3e72b30aac..ca7ab1d1c8 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -42,6 +42,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get switchAccountButton => 'Preklopi račun'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Nalaganje vašega računa iz $url traja dlje kot običajno.'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index 993cb65cdb..56e15acade 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations { @override String get switchAccountButton => 'Змінити обліковий запис'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Ваш обліковий запис на $url завантажується деякий час.'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index 6d62f29892..f084cf1629 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -43,6 +43,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get switchAccountButton => 'Switch account'; + @override + String get organizationsButton => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/model/store.dart b/lib/model/store.dart index 88ca778100..1e7c8946de 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -423,9 +423,15 @@ abstract class PerAccountStoreBase { /// Always equal to `account.realmUrl` and `connection.realmUrl`. Uri get realmUrl => connection.realmUrl; - String? get realmName => account.realmName; - - Uri? get realmIcon => account.realmIcon; + // The `account` is populated with the `realmName` before + // PerAccountStore is created, so this should never be null. + // See `UpdateMachine.load`. + String get realmName => account.realmName!; + + // The `account` is populated with the `realmIcon` before + // PerAccountStore is created, so this should never be null. + // See `UpdateMachine.load`. + Uri get realmIcon => account.realmIcon!; /// Resolve [reference] as a URL relative to [realmUrl]. /// diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 62c09c0857..1602ffc03c 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -10,6 +10,7 @@ import 'app.dart'; import 'app_bar.dart'; import 'button.dart'; import 'color.dart'; +import 'content.dart'; import 'icons.dart'; import 'inbox.dart'; import 'inset_shadow.dart'; @@ -307,29 +308,104 @@ void _showMainMenu(BuildContext context, { builder: (BuildContext _) { return PerAccountStoreWidget( accountId: accountId, - child: SafeArea( - minimum: const EdgeInsets.only(bottom: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible(child: InsetShadowBox( - top: 8, bottom: 8, - color: designVariables.bgBotBar, - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - child: Column(children: menuItems)))), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: AnimatedScaleOnTap( - scaleEnd: 0.95, - duration: Duration(milliseconds: 100), - child: BottomSheetDismissButton( - style: BottomSheetDismissButtonStyle.close))), - ]))); + child: _MainMenu(menuItems: menuItems)); }); } +/// The main-menu sheet. +/// +/// Figma link: +/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=11367-20647&t=lSnHudU6l7NWx0Fa-0 +class _MainMenu extends StatelessWidget { + const _MainMenu({ + required this.menuItems, + }); + + final List menuItems; + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + + return SafeArea( + minimum: const EdgeInsets.only(bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + _MainMenuHeader(), + Flexible(child: InsetShadowBox( + top: 8, bottom: 8, + color: designVariables.bgBotBar, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Column(children: menuItems)))), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: AnimatedScaleOnTap( + scaleEnd: 0.95, + duration: Duration(milliseconds: 100), + child: BottomSheetDismissButton( + style: BottomSheetDismissButtonStyle.close))), + ])); + } +} + +class _MainMenuHeader extends StatelessWidget { + const _MainMenuHeader(); + + @override + Widget build(BuildContext context) { + final zulipLocalizations = ZulipLocalizations.of(context); + final designVariables = DesignVariables.of(context); + final store = PerAccountStoreWidget.of(context); + + final realmIconUrl = store.tryResolveUrl(store.realmIcon.toString()); + + return Padding( + padding: const EdgeInsets.only(top: 6), + child: Row(children: [ + Expanded(child: Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 4, 6), + child: Row(spacing: 8, children: [ + AvatarShape( + size: 28, + borderRadius: 4, + child: realmIconUrl != null + ? RealmContentNetworkImage(realmIconUrl) + : const SizedBox.shrink()), + Expanded(child: Text(store.realmName, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: designVariables.title, + fontSize: 20, + height: 24 / 20, + ).merge(weightVariableTextStyle(context, wght: 600)))), + ]))), + AnimatedScaleOnTap( + duration: const Duration(milliseconds: 100), + scaleEnd: 0.95, + child: TextButton( + onPressed: () { + Navigator.pop(context); // Close the main menu. + Navigator.of(context).push( + MaterialWidgetRoute(page: const ChooseAccountPage())); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.fromLTRB(8, 7, 14, 7), + foregroundColor: designVariables.icon, + backgroundColor: Colors.transparent, + overlayColor: Colors.transparent, + splashFactory: NoSplash.splashFactory, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + tapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: Text(zulipLocalizations.organizationsButton, + style: const TextStyle(fontSize: 19, height: 26 / 19) + .merge(weightVariableTextStyle(context, wght: 600))))), + ])); + } +} + abstract class _MenuButton extends StatelessWidget { const _MenuButton(); diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 1ee0a0ae8e..d215342826 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -23,6 +23,7 @@ import '../flutter_checks.dart'; import '../model/binding.dart'; import '../model/store_checks.dart'; import '../model/test_store.dart'; +import '../test_images.dart'; import '../test_navigation.dart'; import 'checks.dart'; import 'test_app.dart'; @@ -225,6 +226,7 @@ void main () { } testWidgets('navigation states reflect on navigation bar menu buttons', (tester) async { + prepareBoringImageHttpClient(); await prepare(tester); await tapOpenMenuAndAwait(tester); @@ -238,9 +240,11 @@ void main () { await tapOpenMenuAndAwait(tester); checkIconNotSelected(tester, inboxMenuIconFinder); checkIconSelected(tester, channelsMenuIconFinder); + debugNetworkImageHttpClientProvider = null; }); testWidgets('navigation bar menu buttons control navigation states', (tester) async { + prepareBoringImageHttpClient(); await prepare(tester); await tapOpenMenuAndAwait(tester); @@ -256,21 +260,27 @@ void main () { await tapOpenMenuAndAwait(tester); checkIconNotSelected(tester, inboxMenuIconFinder); checkIconSelected(tester, channelsMenuIconFinder); + debugNetworkImageHttpClientProvider = null; }); testWidgets('navigation bar menu buttons dismiss the menu', (tester) async { + prepareBoringImageHttpClient(); await prepare(tester); await tapOpenMenuAndAwait(tester); await tapButtonAndAwaitTransition(tester, channelsMenuIconFinder); + debugNetworkImageHttpClientProvider = null; }); testWidgets('close button dismisses the menu', (tester) async { + prepareBoringImageHttpClient(); await prepare(tester); await tapOpenMenuAndAwait(tester); await tapButtonAndAwaitTransition(tester, find.text('Close')); + debugNetworkImageHttpClientProvider = null; }); testWidgets('menu buttons dismiss the menu', (tester) async { + prepareBoringImageHttpClient(); addTearDown(testBinding.reset); topRoute = null; previousTopRoute = null; @@ -297,21 +307,27 @@ void main () { await tester.pump((topBeforePop as TransitionRoute).reverseTransitionDuration); check(find.byType(BottomSheet)).findsNothing(); + debugNetworkImageHttpClientProvider = null; }); testWidgets('_MyProfileButton', (tester) async { + prepareBoringImageHttpClient(); await prepare(tester); await tapOpenMenuAndAwait(tester); await tapButtonAndAwaitTransition(tester, find.text('My profile')); check(find.byType(ProfilePage)).findsOne(); check(find.text(eg.selfUser.fullName)).findsAny(); + debugNetworkImageHttpClientProvider = null; }); testWidgets('_AboutZulipButton', (tester) async { + tester.view.physicalSize = Size(1080, 1920); + prepareBoringImageHttpClient(); await prepare(tester); await tapOpenMenuAndAwait(tester); await tapButtonAndAwaitTransition(tester, find.byIcon(ZulipIcons.info)); check(find.byType(AboutZulipPage)).findsOne(); + debugNetworkImageHttpClientProvider = null; }); });