diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 5afc9544d2..8e2a332ddc 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -31,6 +31,26 @@ "@upgradeWelcomeDialogDismiss": { "description": "Label for button dismissing dialog shown on first upgrade from the legacy Zulip app." }, + "introModalDismissButton": "Got it", + "@introModalDismissButton": { + "description": "Label for button dismissing intro modals." + }, + "inboxIntroModalTitle": "Welcome to your inbox!", + "@inboxIntroModalTitle": { + "description": "Title for the inbox intro modal." + }, + "inboxIntroModalMessage": "You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.", + "@inboxIntroModalMessage": { + "description": "Message content for the inbox intro modal." + }, + "combinedFeedIntroModalTitle": "Welcome to your combined feed!", + "@combinedFeedIntroModalTitle": { + "description": "Title for the combined feed intro modal." + }, + "combinedFeedIntroModalMessage": "You'll see a feed of all the unmuted messages you've received. You can click on a colored header bar to view a conversation.", + "@combinedFeedIntroModalMessage": { + "description": "Message content for the combined feed intro modal." + }, "chooseAccountPageTitle": "Choose account", "@chooseAccountPageTitle": { "description": "Title for the page to choose between Zulip accounts." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 36a83ac3bb..69df65821f 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -189,6 +189,36 @@ abstract class ZulipLocalizations { /// **'Let\'s go'** String get upgradeWelcomeDialogDismiss; + /// Label for button dismissing intro modals. + /// + /// In en, this message translates to: + /// **'Got it'** + String get introModalDismissButton; + + /// Title for the inbox intro modal. + /// + /// In en, this message translates to: + /// **'Welcome to your inbox!'** + String get inboxIntroModalTitle; + + /// Message content for the inbox intro modal. + /// + /// In en, this message translates to: + /// **'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'** + String get inboxIntroModalMessage; + + /// Title for the combined feed intro modal. + /// + /// In en, this message translates to: + /// **'Welcome to your combined feed!'** + String get combinedFeedIntroModalTitle; + + /// Message content for the combined feed intro modal. + /// + /// In en, this message translates to: + /// **'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'** + String get combinedFeedIntroModalMessage; + /// Title for the page to choose between Zulip accounts. /// /// 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 0de3988707..b0976b88f0 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'هيا بنا'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'اختر حساب'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index cb5793ef39..e23fc4f412 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Los gehts'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Konto auswählen'; diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart index 37510410f9..984a012859 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsEl extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 57a6a13d75..6ba37edfab 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart index 163fb4e6a4..faba01f4a4 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsEs extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index 43ed1eb3ad..eff8ba8886 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -35,6 +35,23 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Allons-y'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choisir un compte'; diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart index 4a813be1b0..3a30455fab 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsHe extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart index 6b3805bf99..a7c195236e 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsHu extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 0c06cd4b38..6cc4039898 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Andiamo'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Scegli account'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index 5ae5bbc394..341204e3f9 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -33,6 +33,23 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'はじめよう'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'アカウントを選択'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index d828f44d95..a9cdd8dde6 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 5eb1ffd41f..be51b23274 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Zaczynajmy'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Wybierz konto'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 3fc9bd6832..614a84fc92 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Приступим'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Выберите учетную запись'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 4841efd265..fcd0cac52f 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Zvoliť účet'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index c4c6d91ed1..e698aed2b3 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -33,6 +33,23 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Začnimo'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Izberite račun'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index d18fd0fd53..eb4061deec 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsUk extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Ходімо'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Обрати обліковий запис'; diff --git a/lib/generated/l10n/zulip_localizations_vi.dart b/lib/generated/l10n/zulip_localizations_vi.dart index 36f0f7ded5..2ab09530be 100644 --- a/lib/generated/l10n/zulip_localizations_vi.dart +++ b/lib/generated/l10n/zulip_localizations_vi.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsVi extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index 085866b1d8..15f0f43f84 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -34,6 +34,23 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get upgradeWelcomeDialogDismiss => 'Let\'s go'; + @override + String get introModalDismissButton => 'Got it'; + + @override + String get inboxIntroModalTitle => 'Welcome to your inbox!'; + + @override + String get inboxIntroModalMessage => + 'You’ll see a list of conversations where you have unread messages, organized by channel. Each conversation is labeled with a topic by the person who started it.'; + + @override + String get combinedFeedIntroModalTitle => 'Welcome to your combined feed!'; + + @override + String get combinedFeedIntroModalMessage => + 'You\'ll see a feed of all the unmuted messages you\'ve received. You can click on a colored header bar to view a conversation.'; + @override String get chooseAccountPageTitle => 'Choose account'; diff --git a/lib/model/settings.dart b/lib/model/settings.dart index a85a1f6f74..f48c0e837d 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -176,6 +176,10 @@ enum BoolGlobalSetting { /// A pseudo-setting recording whether the user has been shown the /// welcome dialog for upgrading from the legacy app. upgradeWelcomeDialogShown(GlobalSettingType.internal, false), + inboxIntroModalShown(GlobalSettingType.internal, false), + combinedFeedIntroModalShown(GlobalSettingType.internal, false), + + /// An experimental flag to enable rendering KaTeX even when some /// errors are encountered. diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index 4dacf8c0be..4644de4a61 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -71,6 +71,7 @@ Widget? _adaptiveContent(Widget? content) { // *top* of that page; shrug.) textAlign: TextAlign.start, child: content); + } } @@ -185,7 +186,9 @@ class UpgradeWelcomeDialog extends StatelessWidget { final navigator = await ZulipApp.navigator; final context = navigator.context; assert(context.mounted); - if (!context.mounted) return; // TODO(linter): this is impossible as there's no actual async gap, but the use_build_context_synchronously lint doesn't see that + if (!context.mounted) { + return; // TODO(linter): this is impossible as there's no actual async gap, but the use_build_context_synchronously lint doesn't see that + } final globalSettings = GlobalStoreWidget.settingsOf(context); switch (globalSettings.legacyUpgradeState) { @@ -218,7 +221,7 @@ class UpgradeWelcomeDialog extends StatelessWidget { } static const String _announcementUrl = - 'https://blog.zulip.com/flutter-mobile-app-launch'; + 'https://blog.zulip.com/flutter-mobile-app-launch'; @override Widget build(BuildContext context) { @@ -227,12 +230,12 @@ class UpgradeWelcomeDialog extends StatelessWidget { title: Text(zulipLocalizations.upgradeWelcomeDialogTitle), content: _adaptiveContent( Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(zulipLocalizations.upgradeWelcomeDialogMessage), - GestureDetector( + Text(zulipLocalizations.upgradeWelcomeDialogMessage), + GestureDetector( onTap: () => PlatformActions.launchUrl(context, Uri.parse(_announcementUrl)), - child: Text( - style: TextStyle(color: ContentTheme.of(context).colorLink), + child: Text( + style: TextStyle(color: ContentTheme.of(context).colorLink), zulipLocalizations.upgradeWelcomeDialogLinkText)), ])), actions: [ @@ -243,3 +246,68 @@ class UpgradeWelcomeDialog extends StatelessWidget { ]); } } + +class IntroModal extends StatelessWidget { + const IntroModal({ + super.key, + required this.title, + required this.message, + }); + + final String title; + final String message; + + @override + Widget build(BuildContext context) { + final zulipLocalizations = ZulipLocalizations.of(context); + return AlertDialog.adaptive( + title: Text(title), + content: _adaptiveContent(Text(message)), + actions: [ + _adaptiveAction( + onPressed: () => Navigator.pop(context), + isDefaultAction: true, + text: zulipLocalizations.introModalDismissButton) + ]); + } +} + +Future showInboxIntroModal(BuildContext context) async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final store = GlobalStoreWidget.settingsOf(context); + if (store.getBool(BoolGlobalSetting.inboxIntroModalShown)) { + return; + } + + final zulipLocalizations = ZulipLocalizations.of(context); + final future = showDialog( + context: context, + builder: (context) => IntroModal( + title: zulipLocalizations.inboxIntroModalTitle, + message: zulipLocalizations.inboxIntroModalMessage, + ), + ); + await future; + await store.setBool(BoolGlobalSetting.inboxIntroModalShown, true); + }); +} + +Future showCombinedFeedIntroModal(BuildContext context) async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final store = GlobalStoreWidget.settingsOf(context); + if (store.getBool(BoolGlobalSetting.combinedFeedIntroModalShown)) { + return; + } + + final zulipLocalizations = ZulipLocalizations.of(context); + final future = showDialog( + context: context, + builder: (context) => IntroModal( + title: zulipLocalizations.combinedFeedIntroModalTitle, + message: zulipLocalizations.combinedFeedIntroModalMessage, + ), + ); + await future; + await store.setBool(BoolGlobalSetting.combinedFeedIntroModalShown, true); + }); +} diff --git a/lib/widgets/inbox.dart b/lib/widgets/inbox.dart index ce7b200327..5f453a5809 100644 --- a/lib/widgets/inbox.dart +++ b/lib/widgets/inbox.dart @@ -6,6 +6,7 @@ import '../model/narrow.dart'; import '../model/recent_dm_conversations.dart'; import '../model/unreads.dart'; import 'action_sheet.dart'; +import 'dialog.dart'; import 'icons.dart'; import 'message_list.dart'; import 'page.dart'; @@ -55,6 +56,7 @@ class _InboxPageState extends State with PerAccountStoreAwareStat recentDmConversationsModel?.removeListener(_modelChanged); recentDmConversationsModel = newStore.recentDmConversationsView ..addListener(_modelChanged); + showInboxIntroModal(context); } @override diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 24fa8c077b..80b4e955de 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -22,6 +22,7 @@ import 'button.dart'; import 'color.dart'; import 'compose_box.dart'; import 'content.dart'; +import 'dialog.dart'; import 'emoji_reaction.dart'; import 'icons.dart'; import 'page.dart'; @@ -320,6 +321,9 @@ class _MessageListPageState extends State implements MessageLis void initState() { super.initState(); narrow = widget.initNarrow; + if (narrow is CombinedFeedNarrow) { + showCombinedFeedIntroModal(context); + } } void _narrowChanged(Narrow newNarrow) { diff --git a/test/example_data.dart b/test/example_data.dart index c9beaef72f..f7327f26c8 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -160,12 +160,12 @@ ServerEmojiData _immutableServerEmojiData({ } final ServerEmojiData serverEmojiDataPopular = _immutableServerEmojiData(codeToNames: { - '1f44d': ['+1', 'thumbs_up', 'like'], - '1f389': ['tada'], - '1f642': ['slight_smile'], - '2764': ['heart', 'love', 'love_you'], - '1f6e0': ['working_on_it', 'hammer_and_wrench', 'tools'], - '1f419': ['octopus'], + '1f44d': ['+1', 'thumbs_up', 'like'], + '1f389': ['tada'], + '1f642': ['slight_smile'], + '2764': ['heart', 'love', 'love_you'], + '1f6e0': ['working_on_it', 'hammer_and_wrench', 'tools'], + '1f419': ['octopus'], }); ServerEmojiData serverEmojiDataPopularPlus(ServerEmojiData data) { @@ -187,12 +187,12 @@ ServerEmojiData serverEmojiDataPopularPlus(ServerEmojiData data) { /// zulip/zulip@9feba0f16f is a Server 11 commit. // TODO(server-11) can drop this final ServerEmojiData serverEmojiDataPopularLegacy = _immutableServerEmojiData(codeToNames: { - '1f44d': ['+1', 'thumbs_up', 'like'], - '1f389': ['tada'], - '1f642': ['smile'], - '2764': ['heart', 'love', 'love_you'], - '1f6e0': ['working_on_it', 'hammer_and_wrench', 'tools'], - '1f419': ['octopus'], + '1f44d': ['+1', 'thumbs_up', 'like'], + '1f389': ['tada'], + '1f642': ['smile'], + '2764': ['heart', 'love', 'love_you'], + '1f6e0': ['working_on_it', 'hammer_and_wrench', 'tools'], + '1f419': ['octopus'], }); /// A fresh user-group ID, from a random but always strictly increasing sequence. @@ -350,24 +350,24 @@ const _account = account; /// discarded by [TestZulipBinding.reset]). That was the cause of issue #1712. class _ImmutableUser extends User { _ImmutableUser.copyUser(User user) : super( - // When adding a field here, be sure to add the corresponding setter below. - userId: user.userId, - deliveryEmail: user.deliveryEmail, - email: user.email, - fullName: user.fullName, - dateJoined: user.dateJoined, - isActive: user.isActive, - isBot: user.isBot, - botType: user.botType, - botOwnerId: user.botOwnerId, - role: user.role, - timezone: user.timezone, - avatarUrl: user.avatarUrl, - avatarVersion: user.avatarVersion, + // When adding a field here, be sure to add the corresponding setter below. + userId: user.userId, + deliveryEmail: user.deliveryEmail, + email: user.email, + fullName: user.fullName, + dateJoined: user.dateJoined, + isActive: user.isActive, + isBot: user.isBot, + botType: user.botType, + botOwnerId: user.botOwnerId, + role: user.role, + timezone: user.timezone, + avatarUrl: user.avatarUrl, + avatarVersion: user.avatarVersion, profileData: user.profileData == null ? null : Map.unmodifiable(user.profileData!), - isSystemBot: user.isSystemBot, - // When adding a field here, be sure to add the corresponding setter below. - ); + isSystemBot: user.isSystemBot, + // When adding a field here, be sure to add the corresponding setter below. + ); static final Error _error = UnsupportedError( 'Cannot mutate immutable User.\n' @@ -735,21 +735,21 @@ StreamMessage streamMessage({ // because (a) this is only for tests; (b) the types do get checked // dynamically in the constructor, so any ill-typing won't propagate further. return StreamMessage.fromJson(deepToJson({ - ..._messagePropertiesBase, - ..._messagePropertiesFromSender(sender), - ..._messagePropertiesFromContent(content, contentMarkdown), - 'display_recipient': effectiveStream.name, - 'stream_id': effectiveStream.streamId, - 'reactions': reactions == null ? [] : Reactions(reactions), - 'flags': flags ?? [], - 'id': id ?? _nextMessageId(), - 'last_edit_timestamp': lastEditTimestamp, - 'subject': topic ?? _defaultTopic, - 'submessages': submessages ?? [], - 'timestamp': timestamp ?? 1678139636, - 'type': 'stream', - 'match_content': matchContent, - 'match_subject': matchTopic, + ..._messagePropertiesBase, + ..._messagePropertiesFromSender(sender), + ..._messagePropertiesFromContent(content, contentMarkdown), + 'display_recipient': effectiveStream.name, + 'stream_id': effectiveStream.streamId, + 'reactions': reactions == null ? [] : Reactions(reactions), + 'flags': flags ?? [], + 'id': id ?? _nextMessageId(), + 'last_edit_timestamp': lastEditTimestamp, + 'subject': topic ?? _defaultTopic, + 'submessages': submessages ?? [], + 'timestamp': timestamp ?? 1678139636, + 'type': 'stream', + 'match_content': matchContent, + 'match_subject': matchTopic, }) as Map); } @@ -777,20 +777,20 @@ DmMessage dmMessage({ _checkPositive(id, 'message ID'); assert(!to.any((user) => user.userId == from.userId)); return DmMessage.fromJson(deepToJson({ - ..._messagePropertiesBase, - ..._messagePropertiesFromSender(from), - ..._messagePropertiesFromContent(content, contentMarkdown), - 'display_recipient': [from, ...to] + ..._messagePropertiesBase, + ..._messagePropertiesFromSender(from), + ..._messagePropertiesFromContent(content, contentMarkdown), + 'display_recipient': [from, ...to] .map((u) => {'id': u.userId, 'email': u.email, 'full_name': u.fullName}) - .toList(growable: false), - 'reactions': [], - 'flags': flags ?? [], - 'id': id ?? _nextMessageId(), - 'last_edit_timestamp': lastEditTimestamp, - 'subject': '', - 'submessages': submessages ?? [], - 'timestamp': timestamp ?? 1678139636, - 'type': 'private', + .toList(growable: false), + 'reactions': [], + 'flags': flags ?? [], + 'id': id ?? _nextMessageId(), + 'last_edit_timestamp': lastEditTimestamp, + 'subject': '', + 'submessages': submessages ?? [], + 'timestamp': timestamp ?? 1678139636, + 'type': 'private', }) as Map); } @@ -920,13 +920,13 @@ StreamOutboxMessage streamOutboxMessage({ }) { final effectiveStream = stream ?? _stream(streamId: defaultStreamMessageStreamId); return OutboxMessage.fromConversation( - StreamConversation( + StreamConversation( effectiveStream.streamId, TopicName(topic ?? 'topic'), - displayRecipient: null, - ), - localMessageId: localMessageId ?? _nextLocalMessageId++, - selfUserId: selfUserId ?? selfUser.userId, - timestamp: timestamp ?? utcTimestamp(), + displayRecipient: null, + ), + localMessageId: localMessageId ?? _nextLocalMessageId++, + selfUserId: selfUserId ?? selfUser.userId, + timestamp: timestamp ?? utcTimestamp(), contentMarkdown: content ?? 'content') as StreamOutboxMessage; } @@ -940,10 +940,10 @@ DmOutboxMessage dmOutboxMessage({ final allRecipientIds = [from, ...to].map((user) => user.userId).toList()..sort(); return OutboxMessage.fromConversation( - DmConversation(allRecipientIds: allRecipientIds), - localMessageId: localMessageId ?? _nextLocalMessageId++, - selfUserId: from.userId, - timestamp: timestamp ?? utcTimestamp(), + DmConversation(allRecipientIds: allRecipientIds), + localMessageId: localMessageId ?? _nextLocalMessageId++, + selfUserId: from.userId, + timestamp: timestamp ?? utcTimestamp(), contentMarkdown: content ?? 'content') as DmOutboxMessage; } @@ -1198,7 +1198,7 @@ UpdateMessageFlagsRemoveEvent updateMessageFlagsRemoveEvent( .otherRecipientIds, ), }, - ); + ); }))); } @@ -1300,7 +1300,12 @@ TestGlobalStore globalStore({ }) { return TestGlobalStore( globalSettings: globalSettings, - boolGlobalSettings: boolGlobalSettings, + boolGlobalSettings: { + // Prevent intro modals from appearing in tests by default. + BoolGlobalSetting.inboxIntroModalShown: true, + BoolGlobalSetting.combinedFeedIntroModalShown: true, + ...?boolGlobalSettings, + }, intGlobalSettings: intGlobalSettings, accounts: accounts, ); @@ -1393,11 +1398,11 @@ InitialSnapshot initialSnapshot({ serverPresencePingIntervalSeconds: serverPresencePingIntervalSeconds ?? 60, serverPresenceOfflineThresholdSeconds: serverPresenceOfflineThresholdSeconds ?? 140, serverTypingStartedExpiryPeriodMilliseconds: - serverTypingStartedExpiryPeriodMilliseconds ?? 15000, + serverTypingStartedExpiryPeriodMilliseconds ?? 15000, serverTypingStoppedWaitPeriodMilliseconds: - serverTypingStoppedWaitPeriodMilliseconds ?? 5000, + serverTypingStoppedWaitPeriodMilliseconds ?? 5000, serverTypingStartedWaitPeriodMilliseconds: - serverTypingStartedWaitPeriodMilliseconds ?? 10000, + serverTypingStartedWaitPeriodMilliseconds ?? 10000, mutedUsers: mutedUsers ?? [], presences: presences ?? {}, realmEmoji: realmEmoji ?? {}, diff --git a/test/model/binding.dart b/test/model/binding.dart index c7fe2bf639..bbf76c730d 100644 --- a/test/model/binding.dart +++ b/test/model/binding.dart @@ -156,6 +156,7 @@ class TestZulipBinding extends ZulipBinding { _canLaunchUrlCalls = null; return result ?? []; } + List? _canLaunchUrlCalls; @override @@ -196,6 +197,7 @@ class TestZulipBinding extends ZulipBinding { _launchUrlCalls = null; return result ?? []; } + List<({Uri url, url_launcher.LaunchMode mode})>? _launchUrlCalls; @override @@ -209,10 +211,12 @@ class TestZulipBinding extends ZulipBinding { throw FlutterError.fromParts([ ErrorSummary( 'TestZulipBinding.launchUrl called ' - 'with launchUrlResult: false and non-null launchUrlException'), + 'with launchUrlResult: false and non-null launchUrlException', + ), ErrorHint( 'Tests should either set launchUrlResult or launchUrlException, ' - 'but not both.'), + 'but not both.', + ), ]); } @@ -224,7 +228,8 @@ class TestZulipBinding extends ZulipBinding { } @override - Future supportsCloseForLaunchMode(url_launcher.LaunchMode mode) async => true; + Future supportsCloseForLaunchMode(url_launcher.LaunchMode mode) async => + true; void _resetCloseInAppWebView() { _closeInAppWebViewCallCount = 0; @@ -236,6 +241,7 @@ class TestZulipBinding extends ZulipBinding { _closeInAppWebViewCallCount = 0; return result; } + int _closeInAppWebViewCallCount = 0; @override @@ -251,7 +257,10 @@ class TestZulipBinding extends ZulipBinding { /// The value that `ZulipBinding.instance.deviceInfo` should return. BaseDeviceInfo deviceInfoResult = _defaultDeviceInfoResult; - static const _defaultDeviceInfoResult = AndroidDeviceInfo(sdkInt: 33, release: '13'); + static const _defaultDeviceInfoResult = AndroidDeviceInfo( + sdkInt: 33, + release: '13', + ); void _resetDeviceInfo() { deviceInfoResult = _defaultDeviceInfoResult; @@ -304,7 +313,8 @@ class TestZulipBinding extends ZulipBinding { } @override - Stream get firebaseMessagingOnMessage => firebaseMessaging.onMessage.stream; + Stream get firebaseMessagingOnMessage => + firebaseMessaging.onMessage.stream; @override void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler) { @@ -318,12 +328,12 @@ class TestZulipBinding extends ZulipBinding { @override FakeAndroidNotificationHostApi get androidNotificationHost => - (_androidNotificationHostApi ??= FakeAndroidNotificationHostApi()); + (_androidNotificationHostApi ??= FakeAndroidNotificationHostApi()); FakeAndroidNotificationHostApi? _androidNotificationHostApi; @override FakeNotificationPigeonApi get notificationPigeonApi => - (_notificationPigeonApi ??= FakeNotificationPigeonApi()); + (_notificationPigeonApi ??= FakeNotificationPigeonApi()); FakeNotificationPigeonApi? _notificationPigeonApi; /// The value that `ZulipBinding.instance.pickFiles()` should return. @@ -343,20 +353,15 @@ class TestZulipBinding extends ZulipBinding { /// either this method or [reset]. /// /// See also [pickFilesResult]. - List<({ - bool? allowMultiple, - bool? withReadStream, - FileType? type, - })> takePickFilesCalls() { + List<({bool? allowMultiple, bool? withReadStream, FileType? type})> + takePickFilesCalls() { final result = _pickFilesCalls; _pickFilesCalls = null; return result ?? []; } - List<({ - bool? allowMultiple, - bool? withReadStream, - FileType? type, - })>? _pickFilesCalls; + + List<({bool? allowMultiple, bool? withReadStream, FileType? type})>? + _pickFilesCalls; @override Future pickFiles({ @@ -364,7 +369,11 @@ class TestZulipBinding extends ZulipBinding { bool? withReadStream, FileType? type, }) async { - (_pickFilesCalls ??= []).add((allowMultiple: allowMultiple, withReadStream: withReadStream, type: type)); + (_pickFilesCalls ??= []).add(( + allowMultiple: allowMultiple, + withReadStream: withReadStream, + type: type, + )); return pickFilesResult; } @@ -385,25 +394,23 @@ class TestZulipBinding extends ZulipBinding { /// either this method or [reset]. /// /// See also [pickImageResult]. - List<({ - ImageSource source, - bool requestFullMetadata, - })> takePickImageCalls() { + List<({ImageSource source, bool requestFullMetadata})> takePickImageCalls() { final result = _pickImageCalls; _pickImageCalls = null; return result ?? []; } - List<({ - ImageSource source, - bool requestFullMetadata, - })>? _pickImageCalls; + + List<({ImageSource source, bool requestFullMetadata})>? _pickImageCalls; @override Future pickImage({ required ImageSource source, bool requestFullMetadata = true, }) async { - (_pickImageCalls ??= []).add((source: source, requestFullMetadata: requestFullMetadata)); + (_pickImageCalls ??= []).add(( + source: source, + requestFullMetadata: requestFullMetadata, + )); return pickImageResult; } @@ -423,7 +430,8 @@ class TestZulipBinding extends ZulipBinding { @override // TODO(#1787) implement androidIntentEvents and write related tests - Stream get androidIntentEvents => throw UnimplementedError(); + Stream get androidIntentEvents => + throw UnimplementedError(); } class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { @@ -450,6 +458,7 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { _requestPermissionCalls = []; return result; } + List _requestPermissionCalls = []; @override @@ -493,16 +502,18 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { String? _token; final StreamController _tokenController = - StreamController.broadcast(); + StreamController.broadcast(); @override Future getToken({String? vapidKey}) async { assert(vapidKey == null); if (_token == null) { - assert(_initialToken != null, + assert( + _initialToken != null, 'Tests that call [NotificationService.start], or otherwise cause' ' a call to `ZulipBinding.instance.firebaseMessaging.getToken`,' - ' must set `testBinding.firebaseMessagingInitialToken` first.'); + ' must set `testBinding.firebaseMessagingInitialToken` first.', + ); // This causes [onTokenRefresh] to fire, just like the real [getToken] // does when no token exists (e.g., on first run after install). @@ -541,7 +552,8 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { /// /// Calling [StreamController.add] on this will cause a call /// to any handler registered through that method. - StreamController onBackgroundMessage = StreamController.broadcast(); + StreamController onBackgroundMessage = + StreamController.broadcast(); } typedef FirebaseMessagingRequestPermissionCall = ({ @@ -585,6 +597,7 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi { _createdChannels = []; return result; } + List _createdChannels = []; @override @@ -607,6 +620,7 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi { _deletedChannels = []; return result; } + List _deletedChannels = []; @override @@ -629,7 +643,8 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi { } @override - Future> listStoredSoundsInNotificationsDirectory() async { + Future> + listStoredSoundsInNotificationsDirectory() async { return _storedNotificationSounds.toList(growable: false); } @@ -637,12 +652,15 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi { /// /// This returns a list of the arguments to all calls made /// to [copySoundResourceToMediaStore] since the last call to this method. - List takeCopySoundResourceToMediaStoreCalls() { + List + takeCopySoundResourceToMediaStoreCalls() { final result = _copySoundResourceToMediaStoreCalls; _copySoundResourceToMediaStoreCalls = []; return result; } - List _copySoundResourceToMediaStoreCalls = []; + + List _copySoundResourceToMediaStoreCalls = + []; @override Future copySoundResourceToMediaStore({ @@ -651,13 +669,17 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi { }) async { _copySoundResourceToMediaStoreCalls.add(( targetFileDisplayName: targetFileDisplayName, - sourceResourceName: sourceResourceName)); + sourceResourceName: sourceResourceName, + )); final url = fakeStoredNotificationSoundUrl(sourceResourceName); - _storedNotificationSounds.add(StoredNotificationSound( - fileName: targetFileDisplayName, - isOwned: true, - contentUrl: url)); + _storedNotificationSounds.add( + StoredNotificationSound( + fileName: targetFileDisplayName, + isOwned: true, + contentUrl: url, + ), + ); return url; } @@ -670,9 +692,11 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi { _notifyCalls = []; return result; } + List _notifyCalls = []; - Iterable get activeNotifications => _activeNotifications.values; + Iterable get activeNotifications => + _activeNotifications.values; final Map<(int, String?), StatusBarNotification> _activeNotifications = {}; final Map _activeNotificationsMessagingStyle = {}; @@ -723,41 +747,52 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi { _activeNotifications[(id, tag)] = StatusBarNotification( id: id, notification: Notification(group: groupKey ?? '', extras: extras ?? {}), - tag: tag); + tag: tag, + ); _activeNotificationsMessagingStyle[tag] = messagingStyle == null - ? null - : MessagingStyle( - user: messagingStyle.user, - conversationTitle: messagingStyle.conversationTitle, - isGroupConversation: messagingStyle.isGroupConversation, - messages: messagingStyle.messages.map((message) => - MessagingStyleMessage( - text: message.text, - timestampMs: message.timestampMs, - person: Person( - key: message.person.key, - name: message.person.name, - iconBitmap: null)), - ).toList(growable: false)); + ? null + : MessagingStyle( + user: messagingStyle.user, + conversationTitle: messagingStyle.conversationTitle, + isGroupConversation: messagingStyle.isGroupConversation, + messages: messagingStyle.messages + .map( + (message) => MessagingStyleMessage( + text: message.text, + timestampMs: message.timestampMs, + person: Person( + key: message.person.key, + name: message.person.name, + iconBitmap: null, + ), + ), + ) + .toList(growable: false), + ); } } @override - Future getActiveNotificationMessagingStyleByTag(String tag) async => - _activeNotificationsMessagingStyle[tag]; + Future getActiveNotificationMessagingStyleByTag( + String tag, + ) async => _activeNotificationsMessagingStyle[tag]; @override - Future> getActiveNotifications({required List desiredExtras}) async { - return _activeNotifications.values.map((statusNotif) { - final notificationExtras = statusNotif.notification.extras; - statusNotif.notification.extras = { - for (final key in desiredExtras) - if (notificationExtras[key] != null) - key: notificationExtras[key]!, - }; - return statusNotif; - }).toList(growable: false); + Future> getActiveNotifications({ + required List desiredExtras, + }) async { + return _activeNotifications.values + .map((statusNotif) { + final notificationExtras = statusNotif.notification.extras; + statusNotif.notification.extras = { + for (final key in desiredExtras) + if (notificationExtras[key] != null) + key: notificationExtras[key]!, + }; + return statusNotif; + }) + .toList(growable: false); } @override @@ -777,9 +812,10 @@ class FakeNotificationPigeonApi implements NotificationPigeonApi { @override Future getNotificationDataFromLaunch() async => - _notificationDataFromLaunch; + _notificationDataFromLaunch; - StreamController? _notificationTapEventsStreamController; + StreamController? + _notificationTapEventsStreamController; void addNotificationTapEvent(NotificationTapEvent event) { _notificationTapEventsStreamController!.add(event); diff --git a/test/widgets/dialog_test.dart b/test/widgets/dialog_test.dart index 5e273a5dab..35f343cd4c 100644 --- a/test/widgets/dialog_test.dart +++ b/test/widgets/dialog_test.dart @@ -7,7 +7,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:zulip/model/settings.dart'; import 'package:zulip/widgets/app.dart'; import 'package:zulip/widgets/dialog.dart'; - +import '../example_data.dart' as eg; import '../model/binding.dart'; import 'dialog_checks.dart'; import 'test_app.dart'; @@ -23,108 +23,180 @@ void main() { Future prepare(WidgetTester tester) async { addTearDown(testBinding.reset); - await tester.pumpWidget(const TestZulipApp( - child: Scaffold(body: Placeholder()))); + await tester.pumpWidget( + const TestZulipApp(child: Scaffold(body: Placeholder())), + ); await tester.pump(); context = tester.element(find.byType(Placeholder)); } group('showErrorDialog', () { - testWidgets('show error dialog', (tester) async { - await prepare(tester); - - showErrorDialog(context: context, title: title, message: message); - await tester.pump(); - checkErrorDialog(tester, expectedTitle: title, expectedMessage: message); - }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); - - testWidgets('user closes error dialog', (tester) async { - await prepare(tester); - - showErrorDialog(context: context, title: title, message: message); - await tester.pump(); - - final button = checkErrorDialog(tester, expectedTitle: title); - await tester.tap(find.byWidget(button)); - await tester.pump(); - checkNoDialog(tester); - }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); - - testWidgets('tap "Learn more" button', (tester) async { - await prepare(tester); - - final learnMoreButtonUrl = Uri.parse('https://foo.example'); - showErrorDialog(context: context, title: title, learnMoreButtonUrl: learnMoreButtonUrl); - await tester.pump(); - checkErrorDialog(tester, expectedTitle: title); - - await tester.tap(find.text('Learn more')); - final expectedMode = switch (defaultTargetPlatform) { - TargetPlatform.android => LaunchMode.inAppBrowserView, - TargetPlatform.iOS => LaunchMode.externalApplication, - _ => throw StateError('attempted to test with $defaultTargetPlatform'), - }; - check(testBinding.takeLaunchUrlCalls()).single - .equals((url: learnMoreButtonUrl, mode: expectedMode)); - }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); - - testWidgets('only one SingleChildScrollView created', (tester) async { - await prepare(tester); - - showErrorDialog(context: context, title: title, message: message); - await tester.pump(); - checkErrorDialog(tester, expectedTitle: title, expectedMessage: message); - - check(find.ancestor(of: find.text(message), - matching: find.byType(SingleChildScrollView))).findsOne(); - }, variant: TargetPlatformVariant.all()); + testWidgets( + 'show error dialog', + (tester) async { + await prepare(tester); + + showErrorDialog(context: context, title: title, message: message); + await tester.pump(); + checkErrorDialog( + tester, + expectedTitle: title, + expectedMessage: message, + ); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); + + testWidgets( + 'user closes error dialog', + (tester) async { + await prepare(tester); + + showErrorDialog(context: context, title: title, message: message); + await tester.pump(); + + final button = checkErrorDialog(tester, expectedTitle: title); + await tester.tap(find.byWidget(button)); + await tester.pump(); + checkNoDialog(tester); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); + + testWidgets( + 'tap "Learn more" button', + (tester) async { + await prepare(tester); + + final learnMoreButtonUrl = Uri.parse('https://foo.example'); + showErrorDialog( + context: context, + title: title, + learnMoreButtonUrl: learnMoreButtonUrl, + ); + await tester.pump(); + checkErrorDialog(tester, expectedTitle: title); + + await tester.tap(find.text('Learn more')); + final expectedMode = switch (defaultTargetPlatform) { + TargetPlatform.android => LaunchMode.inAppBrowserView, + TargetPlatform.iOS => LaunchMode.externalApplication, + _ => throw StateError( + 'attempted to test with $defaultTargetPlatform', + ), + }; + check( + testBinding.takeLaunchUrlCalls(), + ).single.equals((url: learnMoreButtonUrl, mode: expectedMode)); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); + + testWidgets( + 'only one SingleChildScrollView created', + (tester) async { + await prepare(tester); + + showErrorDialog(context: context, title: title, message: message); + await tester.pump(); + checkErrorDialog( + tester, + expectedTitle: title, + expectedMessage: message, + ); + + check( + find.ancestor( + of: find.text(message), + matching: find.byType(SingleChildScrollView), + ), + ).findsOne(); + }, + variant: TargetPlatformVariant.all(), + ); }); group('showSuggestedActionDialog', () { - testWidgets('tap action button', (tester) async { - addTearDown(testBinding.reset); - await tester.pumpWidget(TestZulipApp()); - await tester.pump(); - final element = tester.element(find.byType(Placeholder)); - - final dialog = showSuggestedActionDialog(context: element, - title: 'Continue?', - message: 'Do the thing?', - actionButtonText: 'Sure'); - await tester.pump(); - await tester.tap(find.text('Sure')); - await check(dialog.result).completes((it) => it.equals(true)); - }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); - - testWidgets('tap cancel', (tester) async { - addTearDown(testBinding.reset); - await tester.pumpWidget(TestZulipApp()); - await tester.pump(); - final element = tester.element(find.byType(Placeholder)); - - final dialog = showSuggestedActionDialog(context: element, - title: 'Continue?', - message: 'Do the thing?', - actionButtonText: 'Sure'); - await tester.pump(); - await tester.tap(find.text('Cancel')); - await check(dialog.result).completes((it) => it.equals(null)); - }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); - - testWidgets('tap outside dialog area', (tester) async { - addTearDown(testBinding.reset); - await tester.pumpWidget(TestZulipApp()); - await tester.pump(); - final element = tester.element(find.byType(Placeholder)); - - final dialog = showSuggestedActionDialog(context: element, - title: 'Continue?', - message: 'Do the thing?', - actionButtonText: 'Sure'); - await tester.pump(); - await tester.tapAt(tester.getTopLeft(find.byType(TestZulipApp))); - await check(dialog.result).completes((it) => it.equals(null)); - }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); + testWidgets( + 'tap action button', + (tester) async { + addTearDown(testBinding.reset); + await tester.pumpWidget(TestZulipApp()); + await tester.pump(); + final element = tester.element(find.byType(Placeholder)); + + final dialog = showSuggestedActionDialog( + context: element, + title: 'Continue?', + message: 'Do the thing?', + actionButtonText: 'Sure', + ); + await tester.pump(); + await tester.tap(find.text('Sure')); + await check(dialog.result).completes((it) => it.equals(true)); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); + + testWidgets( + 'tap cancel', + (tester) async { + addTearDown(testBinding.reset); + await tester.pumpWidget(TestZulipApp()); + await tester.pump(); + final element = tester.element(find.byType(Placeholder)); + + final dialog = showSuggestedActionDialog( + context: element, + title: 'Continue?', + message: 'Do the thing?', + actionButtonText: 'Sure', + ); + await tester.pump(); + await tester.tap(find.text('Cancel')); + await check(dialog.result).completes((it) => it.equals(null)); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); + + testWidgets( + 'tap outside dialog area', + (tester) async { + addTearDown(testBinding.reset); + await tester.pumpWidget(TestZulipApp()); + await tester.pump(); + final element = tester.element(find.byType(Placeholder)); + + final dialog = showSuggestedActionDialog( + context: element, + title: 'Continue?', + message: 'Do the thing?', + actionButtonText: 'Sure', + ); + await tester.pump(); + await tester.tapAt(tester.getTopLeft(find.byType(TestZulipApp))); + await check(dialog.result).completes((it) => it.equals(null)); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); }); testWidgets('only one SingleChildScrollView created', (tester) async { @@ -133,37 +205,119 @@ void main() { await tester.pump(); final element = tester.element(find.byType(Placeholder)); - showSuggestedActionDialog(context: element, + showSuggestedActionDialog( + context: element, title: 'Continue?', message: 'Do the thing?', - actionButtonText: 'Sure'); + actionButtonText: 'Sure', + ); await tester.pump(); - check(find.ancestor(of: find.text('Do the thing?'), - matching: find.byType(SingleChildScrollView))).findsOne(); + check( + find.ancestor( + of: find.text('Do the thing?'), + matching: find.byType(SingleChildScrollView), + ), + ).findsOne(); }, variant: TargetPlatformVariant.all()); group('UpgradeWelcomeDialog', () { // TODO(#1594): test LegacyUpgradeState and BoolGlobalSetting.upgradeWelcomeDialogShown - testWidgets('only one SingleChildScrollView created', (tester) async { - final transitionDurationObserver = TransitionDurationObserver(); + testWidgets( + 'only one SingleChildScrollView created', + (tester) async { + final transitionDurationObserver = TransitionDurationObserver(); + addTearDown(testBinding.reset); + + // Real ZulipApp needed because the show-dialog function calls + // `await ZulipApp.navigator`. + await tester.pumpWidget( + ZulipApp(navigatorObservers: [transitionDurationObserver]), + ); + await tester.pump(); + + await testBinding.globalStore.settings.debugSetLegacyUpgradeState( + LegacyUpgradeState.found, + ); + + UpgradeWelcomeDialog.maybeShow(); + await transitionDurationObserver.pumpPastTransition(tester); + + final expectedMessage = + 'You’ll find a familiar experience in a faster, sleeker package.'; + check( + find.ancestor( + of: find.text(expectedMessage), + matching: find.byType(SingleChildScrollView), + ), + ).findsOne(); + }, + variant: TargetPlatformVariant.all(), + ); + }); + + group('IntroModal', () { + testWidgets('IntroModal widget displays correctly', (tester) async { addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - // Real ZulipApp needed because the show-dialog function calls - // `await ZulipApp.navigator`. - await tester.pumpWidget(ZulipApp(navigatorObservers: [transitionDurationObserver])); - await tester.pump(); + const modal = IntroModal(title: 'Test Title', message: 'Test Message'); - await testBinding.globalStore.settings - .debugSetLegacyUpgradeState(LegacyUpgradeState.found); + await tester.pumpWidget(TestZulipApp(child: modal)); + await tester.pumpAndSettle(); - UpgradeWelcomeDialog.maybeShow(); - await transitionDurationObserver.pumpPastTransition(tester); + check(find.text('Test Title')).findsOne(); + check(find.text('Test Message')).findsOne(); + check(find.text('Got it')).findsOne(); + }); - final expectedMessage = 'You’ll find a familiar experience in a faster, sleeker package.'; - check(find.ancestor(of: find.text(expectedMessage), - matching: find.byType(SingleChildScrollView))).findsOne(); - }, variant: TargetPlatformVariant.all()); + testWidgets('settings track inbox modal shown state', (tester) async { + addTearDown(testBinding.reset); + // Reset to false since tests now default to true + await testBinding.globalStore.settings.setBool( + BoolGlobalSetting.inboxIntroModalShown, + false, + ); + check( + testBinding.globalStore.settings.getBool( + BoolGlobalSetting.inboxIntroModalShown, + ), + ).isFalse(); + await testBinding.globalStore.settings.setBool( + BoolGlobalSetting.inboxIntroModalShown, + true, + ); + check( + testBinding.globalStore.settings.getBool( + BoolGlobalSetting.inboxIntroModalShown, + ), + ).isTrue(); + }); + + testWidgets('settings track combined feed modal shown state', ( + tester, + ) async { + addTearDown(testBinding.reset); + // Reset to false since tests now default to true + await testBinding.globalStore.settings.setBool( + BoolGlobalSetting.combinedFeedIntroModalShown, + false, + ); + check( + testBinding.globalStore.settings.getBool( + BoolGlobalSetting.combinedFeedIntroModalShown, + ), + ).isFalse(); + await testBinding.globalStore.settings.setBool( + BoolGlobalSetting.combinedFeedIntroModalShown, + true, + ); + check( + testBinding.globalStore.settings.getBool( + BoolGlobalSetting.combinedFeedIntroModalShown, + ), + ).isTrue(); + }); }); }