diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 1f52ac32ba..c1ac2b54d9 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -39,10 +39,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "switchAccountButton": "Switch account", - "@switchAccountButton": { - "description": "Label for main-menu 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 31d472227d..2e5d4fc8a0 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -199,12 +199,6 @@ abstract class ZulipLocalizations { /// **'Settings'** String get settingsPageTitle; - /// Label for main-menu button leading to the choose-account page. - /// - /// In en, this message translates to: - /// **'Switch account'** - String get switchAccountButton; - /// 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..a61b0a5286 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get settingsPageTitle => 'الإعدادات'; - @override - String get switchAccountButton => 'تبديل الحساب'; - @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 06e2ea1a29..1b8d212f68 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get settingsPageTitle => 'Einstellungen'; - @override - String get switchAccountButton => 'Konto wechseln'; - @override String tryAnotherAccountMessage(Object url) { return 'Dein Account bei $url benötigt einige Zeit zum Laden.'; diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart index ab2e6e686c..76cb35d76f 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsEl extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Switch account'; - @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 80c1c2c475..b8aa14e338 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Switch account'; - @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart index 7058e699fd..865a567eb8 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsEs extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Switch account'; - @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 2e430ea046..b48bcfcce9 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -41,9 +41,6 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get settingsPageTitle => 'Paramètres'; - @override - String get switchAccountButton => 'Changer de compte'; - @override String tryAnotherAccountMessage(Object url) { return 'Votre compte à $url prend du temps à se charger.'; diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart index 16324eede7..bdd0eed92f 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsHe extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Switch account'; - @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart index 4e3da8f3ef..db0aa010e4 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsHu extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Switch account'; - @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 5d5dd0e278..eb9f5fb313 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get settingsPageTitle => 'Impostazioni'; - @override - String get switchAccountButton => 'Cambia account'; - @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..58093c1edf 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -39,9 +39,6 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get settingsPageTitle => '設定'; - @override - String get switchAccountButton => 'アカウントを切り替える'; - @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..070f6d6a6c 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Switch account'; - @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 b038e14fed..726821b69f 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get settingsPageTitle => 'Ustawienia'; - @override - String get switchAccountButton => 'Przełącz konto'; - @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 e13c7cc1a6..5f51e2baab 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get settingsPageTitle => 'Настройки'; - @override - String get switchAccountButton => 'Сменить учетную запись'; - @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..1b4ae6c451 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Zmeniť účet'; - @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 4c34d94983..550ad61438 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -39,9 +39,6 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get settingsPageTitle => 'Nastavitve'; - @override - String get switchAccountButton => 'Preklopi račun'; - @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 d9bc2ae98c..fec6fc3b94 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsUk extends ZulipLocalizations { @override String get settingsPageTitle => 'Налаштування'; - @override - String get switchAccountButton => 'Змінити обліковий запис'; - @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 9a5015cf01..47498f612f 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -40,9 +40,6 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get settingsPageTitle => 'Settings'; - @override - String get switchAccountButton => 'Switch account'; - @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; @@ -1200,9 +1197,6 @@ class ZulipLocalizationsZhHansCn extends ZulipLocalizationsZh { @override String get settingsPageTitle => '设置'; - @override - String get switchAccountButton => '切换账号'; - @override String tryAnotherAccountMessage(Object url) { return '您在 $url 的账号加载时间过长。'; @@ -2305,9 +2299,6 @@ class ZulipLocalizationsZhHantTw extends ZulipLocalizationsZh { @override String get settingsPageTitle => '設定'; - @override - String get switchAccountButton => '切換帳號'; - @override String tryAnotherAccountMessage(Object url) { return '您在 $url 的帳號載入的比較久。'; diff --git a/lib/model/store.dart b/lib/model/store.dart index 88ca778100..90e633a9fa 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -423,14 +423,25 @@ abstract class PerAccountStoreBase { /// Always equal to `account.realmUrl` and `connection.realmUrl`. Uri get realmUrl => connection.realmUrl; - String? get realmName => account.realmName; + // 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!; - Uri? get realmIcon => account.realmIcon; + // 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 [realmIcon] as a URL relative to [realmUrl]. + /// + /// This returns null if resolving fails. + Uri? resolveRealmIconUrl() => _tryResolveUrl(realmUrl, realmIcon); /// Resolve [reference] as a URL relative to [realmUrl]. /// /// This returns null if [reference] fails to parse as a URL. - Uri? tryResolveUrl(String reference) => _tryResolveUrl(realmUrl, reference); + Uri? tryResolveUrl(String reference) => _tryResolveUrlStr(realmUrl, reference); /// Always equal to `connection.zulipFeatureLevel` /// and `account.zulipFeatureLevel`. @@ -459,10 +470,10 @@ abstract class PerAccountStoreBase { int get selfUserId => core.selfUserId; } -const _tryResolveUrl = tryResolveUrl; +const _tryResolveUrlStr = tryResolveUrlStr; /// Like [Uri.resolve], but on failure return null instead of throwing. -Uri? tryResolveUrl(Uri baseUrl, String reference) { +Uri? tryResolveUrlStr(Uri baseUrl, String reference) { try { return baseUrl.resolve(reference); } on FormatException { @@ -470,6 +481,17 @@ Uri? tryResolveUrl(Uri baseUrl, String reference) { } } +const _tryResolveUrl = tryResolveUrl; + +/// Like [Uri.resolve], but on failure return null instead of throwing. +Uri? tryResolveUrl(Uri baseUrl, Uri reference) { + try { + return baseUrl.resolveUri(reference); + } on FormatException { + return null; + } +} + /// Store for the user's data for a given Zulip account. /// /// This should always have a consistent snapshot of the state on the server, diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 62c09c0857..5833aec3d0 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'; @@ -279,7 +280,6 @@ void _showMainMenu(BuildContext context, { _DirectMessagesButton(tabNotifier: tabNotifier), // TODO(#1094): Users const _MyProfileButton(), - const _SwitchAccountButton(), // TODO(#198): Set my status // const SizedBox(height: 8), const _SettingsButton(), @@ -307,29 +307,113 @@ 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=143-10939&t=s7AS3nEgNgjyqHck-4 +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 designVariables = DesignVariables.of(context); + final store = PerAccountStoreWidget.of(context); + + final realmIconUrl = store.resolveRealmIconUrl(); + + final placeholder = ColoredBox(color: designVariables.avatarPlaceholderBg); + final logo = realmIconUrl != null + ? RealmContentNetworkImage( + realmIconUrl, + frameBuilder: (_, child, frame, _) { + if (frame == null) return placeholder; + return child; + }, + errorBuilder: (_, _, _) => placeholder) + : placeholder; + + return Padding( + padding: const EdgeInsets.only(top: 6), + child: AnimatedScaleOnTap( + duration: const Duration(milliseconds: 100), + scaleEnd: 0.95, + child: TextButton( + onPressed: () { + Navigator.pop(context); // Close the main menu. + Navigator.push(context, + MaterialWidgetRoute(page: const ChooseAccountPage())); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + shape: LinearBorder.none, + backgroundColor: Colors.transparent, + overlayColor: Colors.transparent, + splashFactory: NoSplash.splashFactory, + tapTargetSize: MaterialTapTargetSize.shrinkWrap), + child: Row(children: [ + Flexible(child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB(12, 6, 4, 6), + child: Row(spacing: 8, children: [ + AvatarShape( + size: 28, + borderRadius: 4, + child: logo), + Flexible(child: Text(store.realmName, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: designVariables.title, + fontSize: 20, + height: 24 / 20, + ).merge(weightVariableTextStyle(context, wght: 600)))), + ]))), + Padding( + padding: EdgeInsetsDirectional.fromSTEB(8, 7, 14, 7), + child: Icon(ZulipIcons.arrow_left_right, + size: 19, + color: designVariables.icon)), + ])))); + } +} + abstract class _MenuButton extends StatelessWidget { const _MenuButton(); @@ -575,23 +659,6 @@ class _MyProfileButton extends _MenuButton { } } -class _SwitchAccountButton extends _MenuButton { - const _SwitchAccountButton(); - - @override - IconData? get icon => ZulipIcons.arrow_left_right; - - @override - String label(ZulipLocalizations zulipLocalizations) { - return zulipLocalizations.switchAccountButton; - } - - @override - void onPressed(BuildContext context) { - Navigator.of(context).push(MaterialWidgetRoute(page: const ChooseAccountPage())); - } -} - class _SettingsButton extends _MenuButton { const _SettingsButton(); diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index 3241c65e75..09554f48ab 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -401,7 +401,7 @@ void main() { // The image indeed has an invalid URL. final expectedImages = (example.expectedNodes[0] as ImagePreviewNodeList).imagePreviews; check(() => Uri.parse(expectedImages.single.srcUrl)).throws(); - check(tryResolveUrl(eg.realmUrl, expectedImages.single.srcUrl)).isNull(); + check(tryResolveUrlStr(eg.realmUrl, expectedImages.single.srcUrl)).isNull(); // The MessageImagePreview has shown up, // but it doesn't attempt a RealmContentNetworkImage. check(tester.widgetList(find.byType(MessageImagePreview))).isNotEmpty(); diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 1ee0a0ae8e..718db2145e 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 { + prepareBoringImageHttpClient(); await prepare(tester); await tapOpenMenuAndAwait(tester); + await tester.ensureVisible(find.byIcon(ZulipIcons.info)); await tapButtonAndAwaitTransition(tester, find.byIcon(ZulipIcons.info)); check(find.byType(AboutZulipPage)).findsOne(); + debugNetworkImageHttpClientProvider = null; }); });