diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 05598dab42..9048fa98de 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -502,6 +502,18 @@ "@errorBannerCannotPostInChannelLabel": { "description": "Error-banner text replacing the compose box when you do not have permission to send a message to the channel." }, + "composeBoxBannerLabelUnsubscribedWhenCannotSend": "New messages will not appear automatically.", + "@composeBoxBannerLabelUnsubscribedWhenCannotSend": { + "description": "Label text for a compose-box banner when you are viewing an unsubscribed channel in which you do not have permission to send messages." + }, + "composeBoxBannerButtonRefresh": "Refresh", + "@composeBoxBannerButtonRefresh": { + "description": "Label text for the 'Refresh' button in the compose-box banner when you are viewing an unsubscribed channel." + }, + "composeBoxBannerButtonSubscribe": "Subscribe", + "@composeBoxBannerButtonSubscribe": { + "description": "Label text for the 'Subscribe' button in the compose-box banner when you are viewing an unsubscribed channel." + }, "composeBoxBannerLabelEditMessage": "Edit message", "@composeBoxBannerLabelEditMessage": { "description": "Label text for the compose-box banner when you are editing a message." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 984721ff13..7a08e9d986 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -811,6 +811,24 @@ abstract class ZulipLocalizations { /// **'You do not have permission to post in this channel.'** String get errorBannerCannotPostInChannelLabel; + /// Label text for a compose-box banner when you are viewing an unsubscribed channel in which you do not have permission to send messages. + /// + /// In en, this message translates to: + /// **'New messages will not appear automatically.'** + String get composeBoxBannerLabelUnsubscribedWhenCannotSend; + + /// Label text for the 'Refresh' button in the compose-box banner when you are viewing an unsubscribed channel. + /// + /// In en, this message translates to: + /// **'Refresh'** + String get composeBoxBannerButtonRefresh; + + /// Label text for the 'Subscribe' button in the compose-box banner when you are viewing an unsubscribed channel. + /// + /// In en, this message translates to: + /// **'Subscribe'** + String get composeBoxBannerButtonSubscribe; + /// Label text for the compose-box banner when you are editing a message. /// /// 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 323068f599..759cd06a01 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -425,6 +425,16 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Edit message'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index d693740f43..4975e37387 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -441,6 +441,16 @@ class ZulipLocalizationsDe extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'Du hast keine Berechtigung in diesen Kanal zu schreiben.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Nachricht bearbeiten'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 7079d98e3c..4ad9cf4f51 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -425,6 +425,16 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Edit message'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index d06ba5e034..402619efec 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -439,6 +439,16 @@ class ZulipLocalizationsFr extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'Vous n\'avez pas l\'autorisation de poster sur ce canal.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Editer le message'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index cccd1dc8b1..ce18a1d568 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -435,6 +435,16 @@ class ZulipLocalizationsIt extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'Non hai l\'autorizzazione per postare su questo canale.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Modifica messaggio'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index 5f6fa404d4..4e75bf6568 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -415,6 +415,16 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get errorBannerCannotPostInChannelLabel => 'このチャンネルに投稿する権限がありません。'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'メッセージを編集'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 168bfc2757..344bb0372b 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -425,6 +425,16 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Edit message'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 98d6b1de53..45e9fb2cac 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -437,6 +437,16 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'Nie masz uprawnień do dodawania wpisów w tym kanale.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Zmień wiadomość'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 7effd9fa73..817d6b03f8 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -439,6 +439,16 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'У вас нет права писать в этом канале.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Редактирование сообщения'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 5cbbfcc1e4..2504894b25 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -425,6 +425,16 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Edit message'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index 8bfc8810cb..933971c93a 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -449,6 +449,16 @@ class ZulipLocalizationsSl extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'Nimate dovoljenja za objavljanje v tem kanalu.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Uredi sporočilo'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index f5b955fb55..24076bc706 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -439,6 +439,16 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'Ви не маєте дозволу на публікацію в цьому каналі.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Редагування повідомлення'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index 092bcd38e2..4084a19aba 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -425,6 +425,16 @@ class ZulipLocalizationsZh extends ZulipLocalizations { String get errorBannerCannotPostInChannelLabel => 'You do not have permission to post in this channel.'; + @override + String get composeBoxBannerLabelUnsubscribedWhenCannotSend => + 'New messages will not appear automatically.'; + + @override + String get composeBoxBannerButtonRefresh => 'Refresh'; + + @override + String get composeBoxBannerButtonSubscribe => 'Subscribe'; + @override String get composeBoxBannerLabelEditMessage => 'Edit message'; diff --git a/lib/model/message_list.dart b/lib/model/message_list.dart index 91868df2b2..613b1a712e 100644 --- a/lib/model/message_list.dart +++ b/lib/model/message_list.dart @@ -622,9 +622,10 @@ class MessageListView with ChangeNotifier, _MessageSequence { Narrow get narrow => _narrow; Narrow _narrow; - /// Set [narrow] to [newNarrow], reset, [notifyListeners], and [fetchInitial]. - void renarrowAndFetch(Narrow newNarrow) { + /// Set [narrow] and [anchor], reset, [notifyListeners], and [fetchInitial]. + void renarrowAndFetch(Narrow newNarrow, Anchor anchor) { _narrow = newNarrow; + _anchor = anchor; _reset(); notifyListeners(); fetchInitial(); @@ -1173,7 +1174,8 @@ class MessageListView with ChangeNotifier, _MessageSequence { switch (propagateMode) { case PropagateMode.changeAll: case PropagateMode.changeLater: - renarrowAndFetch(newNarrow); + // TODO(#1009) anchor to some visible message, if any + renarrowAndFetch(newNarrow, anchor); case PropagateMode.changeOne: } } diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index 1d41a574d5..59be92482f 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -490,27 +490,7 @@ class SubscribeButton extends ActionSheetMenuItemButton { @override void onPressed() async { - final store = PerAccountStoreWidget.of(pageContext); - final channel = store.streams[channelId]; - if (channel == null || channel is Subscription) return; // TODO could give feedback - - try { - await subscribeToChannel(store.connection, subscriptions: [channel.name]); - } catch (e) { - if (!pageContext.mounted) return; - - String? errorMessage; - switch (e) { - case ZulipApiException(): - errorMessage = e.message; - // TODO(#741) specific messages for common errors, like network errors - // (support with reusable code) - default: - } - - final title = ZulipLocalizations.of(pageContext).subscribeFailedTitle; - showErrorDialog(context: pageContext, title: title, message: errorMessage); - } + await ZulipAction.subscribeToChannel(pageContext, channelId: channelId); } } diff --git a/lib/widgets/actions.dart b/lib/widgets/actions.dart index b75a85ecef..39232847b3 100644 --- a/lib/widgets/actions.dart +++ b/lib/widgets/actions.dart @@ -241,6 +241,32 @@ abstract final class ZulipAction { return fetchedMessage?.content; } + static Future subscribeToChannel(BuildContext context, { + required int channelId, + }) async { + final store = PerAccountStoreWidget.of(context); + final channel = store.streams[channelId]; + if (channel == null || channel is Subscription) return; // TODO could give feedback + + try { + await channels_api.subscribeToChannel(store.connection, subscriptions: [channel.name]); + } catch (e) { + if (!context.mounted) return; + + String? errorMessage; + switch (e) { + case ZulipApiException(): + errorMessage = e.message; + // TODO(#741) specific messages for common errors, like network errors + // (support with reusable code) + default: + } + + final title = ZulipLocalizations.of(context).subscribeFailedTitle; + showErrorDialog(context: context, title: title, message: errorMessage); + } + } + /// Unsubscribe from a channel, possibly after a confirmation dialog, /// showing an error dialog on failure. /// diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 2c867f3081..1cfb308365 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -41,6 +41,18 @@ class ZulipWebUiKitButton extends StatelessWidget { }); case (ZulipWebUiKitButtonAttention.medium, ZulipWebUiKitButtonIntent.neutral): case (ZulipWebUiKitButtonAttention.high, ZulipWebUiKitButtonIntent.neutral): + case (ZulipWebUiKitButtonAttention.minimal, ZulipWebUiKitButtonIntent.warning): + throw UnimplementedError(); + case (ZulipWebUiKitButtonAttention.medium, ZulipWebUiKitButtonIntent.warning): + return WidgetStateColor.fromMap({ + WidgetState.pressed: designVariables.btnBgAttMediumIntWarningActive, + ~WidgetState.pressed: designVariables.btnBgAttMediumIntWarningNormal, + }); + case (ZulipWebUiKitButtonAttention.high, ZulipWebUiKitButtonIntent.warning): + return WidgetStateColor.fromMap({ + WidgetState.pressed: designVariables.btnBgAttHighIntWarningActive, + ~WidgetState.pressed: designVariables.btnBgAttHighIntWarningNormal, + }); case (ZulipWebUiKitButtonAttention.minimal, ZulipWebUiKitButtonIntent.info): throw UnimplementedError(); case (ZulipWebUiKitButtonAttention.medium, ZulipWebUiKitButtonIntent.info): @@ -63,6 +75,12 @@ class ZulipWebUiKitButton extends StatelessWidget { return designVariables.neutralButtonLabel.withFadedAlpha(0.85); case (ZulipWebUiKitButtonAttention.medium, ZulipWebUiKitButtonIntent.neutral): case (ZulipWebUiKitButtonAttention.high, ZulipWebUiKitButtonIntent.neutral): + case (ZulipWebUiKitButtonAttention.minimal, ZulipWebUiKitButtonIntent.warning): + throw UnimplementedError(); + case (ZulipWebUiKitButtonAttention.medium, ZulipWebUiKitButtonIntent.warning): + return designVariables.btnLabelAttMediumIntWarning; + case (ZulipWebUiKitButtonAttention.high, ZulipWebUiKitButtonIntent.warning): + return designVariables.btnLabelAttHighIntWarning; case (ZulipWebUiKitButtonAttention.minimal, ZulipWebUiKitButtonIntent.info): throw UnimplementedError(); case (ZulipWebUiKitButtonAttention.medium, ZulipWebUiKitButtonIntent.info): @@ -195,7 +213,7 @@ enum ZulipWebUiKitButtonAttention { enum ZulipWebUiKitButtonIntent { neutral, - // warning, + warning, // danger, info, // success, diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 4e858c62fe..0d12efc5e6 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -24,6 +24,7 @@ import 'color.dart'; import 'dialog.dart'; import 'icons.dart'; import 'inset_shadow.dart'; +import 'message_list.dart'; import 'page.dart'; import 'store.dart'; import 'text.dart'; @@ -1768,6 +1769,8 @@ class _Banner extends StatelessWidget { final (labelColor, backgroundColor) = switch (intent) { _BannerIntent.info => (designVariables.bannerTextIntInfo, designVariables.bannerBgIntInfo), + _BannerIntent.warning => + (designVariables.btnLabelAttMediumIntWarning, designVariables.bannerBgIntWarning), _BannerIntent.danger => (designVariables.btnLabelAttMediumIntDanger, designVariables.bannerBgIntDanger), }; @@ -1805,9 +1808,42 @@ class _Banner extends StatelessWidget { enum _BannerIntent { info, + warning, danger, } +class _UnsubscribedChannelBannerTrailing extends StatelessWidget { + const _UnsubscribedChannelBannerTrailing({required this.channelId}); + + final int channelId; + + @override + Widget build(BuildContext context) { + // (A BuildContext that's expected to remain mounted until the whole page + // disappears, which may be long after the banner disappears.) + final pageContext = PageRoot.contextOf(context); + + final zulipLocalizations = ZulipLocalizations.of(pageContext); + return Row(mainAxisSize: MainAxisSize.min, spacing: 8, children: [ + ZulipWebUiKitButton( + label: zulipLocalizations.composeBoxBannerButtonRefresh, + intent: ZulipWebUiKitButtonIntent.warning, + onPressed: () { + MessageListPage.ancestorOf(pageContext).refresh(); + }), + ZulipWebUiKitButton( + label: zulipLocalizations.composeBoxBannerButtonSubscribe, + intent: ZulipWebUiKitButtonIntent.warning, + attention: ZulipWebUiKitButtonAttention.high, + onPressed: () async { + await ZulipAction.subscribeToChannel(pageContext, channelId: channelId); + if (!pageContext.mounted) return; + MessageListPage.ancestorOf(pageContext).refresh(); + }), + ]); + } +} + class _EditMessageBannerTrailing extends StatelessWidget { const _EditMessageBannerTrailing({required this.composeBoxState}); @@ -2136,13 +2172,27 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM case ChannelNarrow(:final streamId): case TopicNarrow(:final streamId): final channel = store.streams[streamId]; - if (channel == null || !store.selfCanSendMessage(inChannel: channel, - byDate: DateTime.now())) { + if (channel == null) { return _Banner( intent: _BannerIntent.info, + // TODO this doesn't seem like exactly the right message -- + // it makes it sound like the channel exists, which might not be + // true. (We'll get here if the channel doesn't exist or if it + // exists but we don't have permission to know about it.) label: zulipLocalizations.errorBannerCannotPostInChannelLabel); } + if (!store.selfCanSendMessage(inChannel: channel, byDate: DateTime.now())) { + return (channel is Subscription) + ? _Banner( + intent: _BannerIntent.info, + label: zulipLocalizations.errorBannerCannotPostInChannelLabel) + : _Banner( + intent: _BannerIntent.warning, + label: zulipLocalizations.composeBoxBannerLabelUnsubscribedWhenCannotSend, + trailing: _UnsubscribedChannelBannerTrailing(channelId: streamId)); + } + case DmNarrow(:final otherRecipientIds): final hasDeactivatedUser = otherRecipientIds.any((id) => !(store.getUser(id)?.isActive ?? true)); diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 77e2e8cb24..4802a3b93f 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -137,6 +137,17 @@ abstract class MessageListPageState extends State { /// The narrow for this page's message list. Narrow get narrow; + /// Resets the [MessageListView] model, triggering an initial fetch. + /// + /// If [anchor] isn't passed, reuses the anchor from the last initial fetch. + /// + /// Useful when updates won't arrive through the event system, + /// as when showing an unsubscribed channel. + /// (New-message events aren't sent for unsubscribed channels.) + /// + /// Does nothing if [MessageList] has not mounted yet. + void refresh([Anchor? anchor]); + /// The [ComposeBoxState] for this [MessageListPage]'s compose box, /// if this [MessageListPage] offers a compose box and it has mounted, /// else null. @@ -267,6 +278,14 @@ class _MessageListPageState extends State implements MessageLis @override late Narrow narrow; + @override + void refresh([Anchor? anchor]) { + // TODO If anchor isn't passed, check if there's some onscreen message + // we can anchor to, before defaulting to model.anchor. + // Update the dartdoc on this method with the new behavior. + model?.renarrowAndFetch(narrow, anchor ?? model!.anchor); + } + @override ComposeBoxState? get composeBoxState => _composeBoxKey.currentState; final GlobalKey _composeBoxKey = GlobalKey(); @@ -639,7 +658,8 @@ class MessageListAppBarTitle extends StatelessWidget { case KeywordSearchNarrow(): assert(!willCenterTitle); return _SearchBar(onSubmitted: (narrow) { - MessageListPage.ancestorOf(context).model!.renarrowAndFetch(narrow); + MessageListPage.ancestorOf(context).model! + .renarrowAndFetch(narrow, AnchorCode.newest); }); } } diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index 44517ab2c3..a82909bcae 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -138,6 +138,7 @@ class DesignVariables extends ThemeExtension { background: const Color(0xffffffff), bannerBgIntDanger: const Color(0xfff2e4e4), bannerBgIntInfo: const Color(0xffddecf6), + bannerBgIntWarning: const Color(0xfffaf5dc), bannerTextIntInfo: const Color(0xff06037c), bgBotBar: const Color(0xfff6f6f6), bgContextMenu: const Color(0xfff2f2f2), @@ -150,13 +151,19 @@ class DesignVariables extends ThemeExtension { borderMenuButtonSelected: Colors.black.withValues(alpha: 0.2), btnBgAttHighIntInfoActive: const Color(0xff1e41d3), btnBgAttHighIntInfoNormal: const Color(0xff3c6bff), + btnBgAttHighIntWarningActive: const Color(0xffeba002), + btnBgAttHighIntWarningNormal: const Color(0xfffebe3d), btnBgAttMediumIntInfoActive: const Color(0xff3c6bff).withValues(alpha: 0.22), btnBgAttMediumIntInfoNormal: const Color(0xff3c6bff).withValues(alpha: 0.12), + btnBgAttMediumIntWarningActive: const Color(0xffeba001).withValues(alpha: 0.28), + btnBgAttMediumIntWarningNormal: const Color(0xffeba002).withValues(alpha: 0.18), btnLabelAttHigh: const Color(0xffffffff), + btnLabelAttHighIntWarning: const Color(0xff000000).withValues(alpha: 0.88), btnLabelAttLowIntDanger: const Color(0xffc0070a), btnLabelAttLowIntInfo: const Color(0xff2347c6), btnLabelAttMediumIntDanger: const Color(0xffac0508), btnLabelAttMediumIntInfo: const Color(0xff1027a6), + btnLabelAttMediumIntWarning: const Color(0xff764607), btnShadowAttMed: const Color(0xff000000).withValues(alpha: 0.20), composeBoxBg: const Color(0xffffffff), contextMenuCancelText: const Color(0xff222222), @@ -232,6 +239,7 @@ class DesignVariables extends ThemeExtension { background: const Color(0xff000000), bannerBgIntDanger: const Color(0xff461616), bannerBgIntInfo: const Color(0xff00253d), + bannerBgIntWarning: const Color(0xff332b00), bannerTextIntInfo: const Color(0xffcbdbfd), bgBotBar: const Color(0xff222222), bgContextMenu: const Color(0xff262626), @@ -244,13 +252,19 @@ class DesignVariables extends ThemeExtension { borderMenuButtonSelected: Colors.white.withValues(alpha: 0.1), btnBgAttHighIntInfoActive: const Color(0xff1e41d3), btnBgAttHighIntInfoNormal: const Color(0xff1e41d3), + btnBgAttHighIntWarningActive: const Color(0xffdb920d), + btnBgAttHighIntWarningNormal: const Color(0xffdb920d), btnBgAttMediumIntInfoActive: const Color(0xff97b6fe).withValues(alpha: 0.12), btnBgAttMediumIntInfoNormal: const Color(0xff97b6fe).withValues(alpha: 0.12), + btnBgAttMediumIntWarningActive: const Color(0xffdb920d).withValues(alpha: 0.12), + btnBgAttMediumIntWarningNormal: const Color(0xffdb920d).withValues(alpha: 0.12), btnLabelAttHigh: const Color(0xffffffff).withValues(alpha: 0.85), + btnLabelAttHighIntWarning: const Color(0xff000000).withValues(alpha: 0.90), btnLabelAttLowIntDanger: const Color(0xffff8b7c), btnLabelAttLowIntInfo: const Color(0xff84a8fd), btnLabelAttMediumIntDanger: const Color(0xffff8b7c), btnLabelAttMediumIntInfo: const Color(0xff97b6fe), + btnLabelAttMediumIntWarning: const Color(0xfff8b325), btnShadowAttMed: const Color(0xffffffff).withValues(alpha: 0.21), composeBoxBg: const Color(0xff0f0f0f), contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75), @@ -335,6 +349,7 @@ class DesignVariables extends ThemeExtension { required this.background, required this.bannerBgIntDanger, required this.bannerBgIntInfo, + required this.bannerBgIntWarning, required this.bannerTextIntInfo, required this.bgBotBar, required this.bgContextMenu, @@ -347,13 +362,19 @@ class DesignVariables extends ThemeExtension { required this.borderMenuButtonSelected, required this.btnBgAttHighIntInfoActive, required this.btnBgAttHighIntInfoNormal, + required this.btnBgAttHighIntWarningActive, + required this.btnBgAttHighIntWarningNormal, required this.btnBgAttMediumIntInfoActive, required this.btnBgAttMediumIntInfoNormal, + required this.btnBgAttMediumIntWarningActive, + required this.btnBgAttMediumIntWarningNormal, required this.btnLabelAttHigh, + required this.btnLabelAttHighIntWarning, required this.btnLabelAttLowIntDanger, required this.btnLabelAttLowIntInfo, required this.btnLabelAttMediumIntDanger, required this.btnLabelAttMediumIntInfo, + required this.btnLabelAttMediumIntWarning, required this.btnShadowAttMed, required this.composeBoxBg, required this.contextMenuCancelText, @@ -429,6 +450,7 @@ class DesignVariables extends ThemeExtension { final Color background; final Color bannerBgIntDanger; final Color bannerBgIntInfo; + final Color bannerBgIntWarning; final Color bannerTextIntInfo; final Color bgBotBar; final Color bgContextMenu; @@ -441,13 +463,19 @@ class DesignVariables extends ThemeExtension { final Color borderMenuButtonSelected; final Color btnBgAttHighIntInfoActive; final Color btnBgAttHighIntInfoNormal; + final Color btnBgAttHighIntWarningActive; + final Color btnBgAttHighIntWarningNormal; final Color btnBgAttMediumIntInfoActive; final Color btnBgAttMediumIntInfoNormal; + final Color btnBgAttMediumIntWarningActive; + final Color btnBgAttMediumIntWarningNormal; final Color btnLabelAttHigh; + final Color btnLabelAttHighIntWarning; final Color btnLabelAttLowIntDanger; final Color btnLabelAttLowIntInfo; final Color btnLabelAttMediumIntDanger; final Color btnLabelAttMediumIntInfo; + final Color btnLabelAttMediumIntWarning; final Color btnShadowAttMed; final Color composeBoxBg; final Color contextMenuCancelText; @@ -518,6 +546,7 @@ class DesignVariables extends ThemeExtension { Color? background, Color? bannerBgIntDanger, Color? bannerBgIntInfo, + Color? bannerBgIntWarning, Color? bannerTextIntInfo, Color? bgBotBar, Color? bgContextMenu, @@ -530,13 +559,19 @@ class DesignVariables extends ThemeExtension { Color? borderMenuButtonSelected, Color? btnBgAttHighIntInfoActive, Color? btnBgAttHighIntInfoNormal, + Color? btnBgAttHighIntWarningActive, + Color? btnBgAttHighIntWarningNormal, Color? btnBgAttMediumIntInfoActive, Color? btnBgAttMediumIntInfoNormal, + Color? btnBgAttMediumIntWarningActive, + Color? btnBgAttMediumIntWarningNormal, Color? btnLabelAttHigh, + Color? btnLabelAttHighIntWarning, Color? btnLabelAttLowIntDanger, Color? btnLabelAttLowIntInfo, Color? btnLabelAttMediumIntDanger, Color? btnLabelAttMediumIntInfo, + Color? btnLabelAttMediumIntWarning, Color? btnShadowAttMed, Color? composeBoxBg, Color? contextMenuCancelText, @@ -602,6 +637,7 @@ class DesignVariables extends ThemeExtension { background: background ?? this.background, bannerBgIntDanger: bannerBgIntDanger ?? this.bannerBgIntDanger, bannerBgIntInfo: bannerBgIntInfo ?? this.bannerBgIntInfo, + bannerBgIntWarning: bannerBgIntWarning ?? this.bannerBgIntWarning, bannerTextIntInfo: bannerTextIntInfo ?? this.bannerTextIntInfo, bgBotBar: bgBotBar ?? this.bgBotBar, bgContextMenu: bgContextMenu ?? this.bgContextMenu, @@ -614,13 +650,19 @@ class DesignVariables extends ThemeExtension { borderMenuButtonSelected: borderMenuButtonSelected ?? this.borderMenuButtonSelected, btnBgAttHighIntInfoActive: btnBgAttHighIntInfoActive ?? this.btnBgAttHighIntInfoActive, btnBgAttHighIntInfoNormal: btnBgAttHighIntInfoNormal ?? this.btnBgAttHighIntInfoNormal, + btnBgAttHighIntWarningActive: btnBgAttHighIntWarningActive ?? this.btnBgAttHighIntWarningActive, + btnBgAttHighIntWarningNormal: btnBgAttHighIntWarningNormal ?? this.btnBgAttHighIntWarningNormal, btnBgAttMediumIntInfoActive: btnBgAttMediumIntInfoActive ?? this.btnBgAttMediumIntInfoActive, btnBgAttMediumIntInfoNormal: btnBgAttMediumIntInfoNormal ?? this.btnBgAttMediumIntInfoNormal, + btnBgAttMediumIntWarningActive: btnBgAttMediumIntWarningActive ?? this.btnBgAttMediumIntWarningActive, + btnBgAttMediumIntWarningNormal: btnBgAttMediumIntWarningNormal ?? this.btnBgAttMediumIntWarningNormal, btnLabelAttHigh: btnLabelAttHigh ?? this.btnLabelAttHigh, + btnLabelAttHighIntWarning: btnLabelAttHighIntWarning ?? this.btnLabelAttHighIntWarning, btnLabelAttLowIntDanger: btnLabelAttLowIntDanger ?? this.btnLabelAttLowIntDanger, btnLabelAttLowIntInfo: btnLabelAttLowIntInfo ?? this.btnLabelAttLowIntInfo, btnLabelAttMediumIntDanger: btnLabelAttMediumIntDanger ?? this.btnLabelAttMediumIntDanger, btnLabelAttMediumIntInfo: btnLabelAttMediumIntInfo ?? this.btnLabelAttMediumIntInfo, + btnLabelAttMediumIntWarning: btnLabelAttMediumIntWarning ?? this.btnLabelAttMediumIntWarning, btnShadowAttMed: btnShadowAttMed ?? this.btnShadowAttMed, composeBoxBg: composeBoxBg ?? this.composeBoxBg, contextMenuCancelText: contextMenuCancelText ?? this.contextMenuCancelText, @@ -693,6 +735,7 @@ class DesignVariables extends ThemeExtension { background: Color.lerp(background, other.background, t)!, bannerBgIntDanger: Color.lerp(bannerBgIntDanger, other.bannerBgIntDanger, t)!, bannerBgIntInfo: Color.lerp(bannerBgIntInfo, other.bannerBgIntInfo, t)!, + bannerBgIntWarning: Color.lerp(bannerBgIntWarning, other.bannerBgIntWarning, t)!, bannerTextIntInfo: Color.lerp(bannerTextIntInfo, other.bannerTextIntInfo, t)!, bgBotBar: Color.lerp(bgBotBar, other.bgBotBar, t)!, bgContextMenu: Color.lerp(bgContextMenu, other.bgContextMenu, t)!, @@ -705,13 +748,19 @@ class DesignVariables extends ThemeExtension { borderMenuButtonSelected: Color.lerp(borderMenuButtonSelected, other.borderMenuButtonSelected, t)!, btnBgAttHighIntInfoActive: Color.lerp(btnBgAttHighIntInfoActive, other.btnBgAttHighIntInfoActive, t)!, btnBgAttHighIntInfoNormal: Color.lerp(btnBgAttHighIntInfoNormal, other.btnBgAttHighIntInfoNormal, t)!, + btnBgAttHighIntWarningActive: Color.lerp(btnBgAttHighIntWarningActive, other.btnBgAttHighIntWarningActive, t)!, + btnBgAttHighIntWarningNormal: Color.lerp(btnBgAttHighIntWarningNormal, other.btnBgAttHighIntWarningNormal, t)!, btnBgAttMediumIntInfoActive: Color.lerp(btnBgAttMediumIntInfoActive, other.btnBgAttMediumIntInfoActive, t)!, btnBgAttMediumIntInfoNormal: Color.lerp(btnBgAttMediumIntInfoNormal, other.btnBgAttMediumIntInfoNormal, t)!, + btnBgAttMediumIntWarningActive: Color.lerp(btnBgAttMediumIntWarningActive, other.btnBgAttMediumIntWarningActive, t)!, + btnBgAttMediumIntWarningNormal: Color.lerp(btnBgAttMediumIntWarningNormal, other.btnBgAttMediumIntWarningNormal, t)!, btnLabelAttHigh: Color.lerp(btnLabelAttHigh, other.btnLabelAttHigh, t)!, + btnLabelAttHighIntWarning: Color.lerp(btnLabelAttHighIntWarning, other.btnLabelAttHighIntWarning, t)!, btnLabelAttLowIntDanger: Color.lerp(btnLabelAttLowIntDanger, other.btnLabelAttLowIntDanger, t)!, btnLabelAttLowIntInfo: Color.lerp(btnLabelAttLowIntInfo, other.btnLabelAttLowIntInfo, t)!, btnLabelAttMediumIntDanger: Color.lerp(btnLabelAttMediumIntDanger, other.btnLabelAttMediumIntDanger, t)!, btnLabelAttMediumIntInfo: Color.lerp(btnLabelAttMediumIntInfo, other.btnLabelAttMediumIntInfo, t)!, + btnLabelAttMediumIntWarning: Color.lerp(btnLabelAttMediumIntWarning, other.btnLabelAttMediumIntWarning, t)!, btnShadowAttMed: Color.lerp(btnShadowAttMed, other.btnShadowAttMed, t)!, composeBoxBg: Color.lerp(composeBoxBg, other.composeBoxBg, t)!, contextMenuCancelText: Color.lerp(contextMenuCancelText, other.contextMenuCancelText, t)!, diff --git a/test/model/message_list_test.dart b/test/model/message_list_test.dart index 39f4305ff3..c773c2feaf 100644 --- a/test/model/message_list_test.dart +++ b/test/model/message_list_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -427,6 +428,64 @@ void main() { }); }); + group('renarrowAndFetch', () { + test('smoke', () => awaitFakeAsync((async) async { + final channel = eg.stream(); + + const narrow = CombinedFeedNarrow(); + await prepare(narrow: narrow, stream: channel); + final messages = List.generate(100, + (i) => eg.streamMessage(id: 1000 + i, stream: channel)); + await prepareMessages(foundOldest: false, messages: messages); + + // Start a fetchOlder, so we can check that renarrowAndFetch causes its + // result to be discarded. + connection.prepare( + json: olderResult( + anchor: 1000, foundOldest: false, + messages: List.generate(100, + (i) => eg.streamMessage(id: 900 + i, stream: channel)), + ).toJson(), + delay: Duration(milliseconds: 500), + ); + unawaited(model.fetchOlder()); + checkNotifiedOnce(); + + // Start the renarrowAndFetch. + final newNarrow = ChannelNarrow(channel.streamId); + final newAnchor = NumericAnchor(messages[3].id); + + final result = eg.getMessagesResult( + anchor: newAnchor, + foundOldest: false, foundNewest: false, + messages: messages.sublist(3, 5)); + connection.prepare(json: result.toJson(), delay: Duration(seconds: 1)); + model.renarrowAndFetch(newNarrow, newAnchor); + checkNotifiedOnce(); + check(model) + ..fetched.isFalse() + ..narrow.equals(newNarrow) + ..anchor.equals(newAnchor) + ..messages.isEmpty(); + + // Elapse until the fetchOlder is done but renarrowAndFetch is still + // pending; check that the list is still empty despite the fetchOlder. + async.elapse(Duration(milliseconds: 750)); + check(model) + ..fetched.isFalse() + ..narrow.equals(newNarrow) + ..messages.isEmpty(); + + // Elapse until the renarrowAndFetch completes. + async.elapse(Duration(seconds: 250)); + check(model) + ..fetched.isTrue() + ..narrow.equals(newNarrow) + ..anchor.equals(newAnchor) + ..messages.length.equals(2); + })); + }); + group('fetching more', () { test('fetchOlder smoke', () async { const narrow = CombinedFeedNarrow(); @@ -3327,6 +3386,7 @@ extension MessageListMessageItemChecks on Subject { extension MessageListViewChecks on Subject { Subject get store => has((x) => x.store, 'store'); Subject get narrow => has((x) => x.narrow, 'narrow'); + Subject get anchor => has((x) => x.anchor, 'anchor'); Subject> get messages => has((x) => x.messages, 'messages'); Subject> get outboxMessages => has((x) => x.outboxMessages, 'outboxMessages'); Subject get middleMessage => has((x) => x.middleMessage, 'middleMessage'); diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart index 41d1e80d25..15a0a7f81e 100644 --- a/test/widgets/compose_box_test.dart +++ b/test/widgets/compose_box_test.dart @@ -34,6 +34,7 @@ import '../api/fake_api.dart'; import '../example_data.dart' as eg; import '../flutter_checks.dart'; import '../model/binding.dart'; +import '../model/message_list_test.dart'; import '../model/store_checks.dart'; import '../model/test_store.dart'; import '../model/typing_status_test.dart'; @@ -58,11 +59,14 @@ void main() { required Narrow narrow, User? selfUser, List otherUsers = const [], - List streams = const [], + List? streams, + List subscriptions = const [], List? messages, bool? mandatoryTopics, int? zulipFeatureLevel, }) async { + streams ??= subscriptions; + if (narrow case ChannelNarrow(:var streamId) || TopicNarrow(: var streamId)) { final channel = streams.firstWhereOrNull((s) => s.streamId == streamId); assert(channel != null, @@ -82,6 +86,7 @@ void main() { await testBinding.globalStore.add(selfAccount, eg.initialSnapshot( realmUsers: [selfUser, ...otherUsers], streams: streams, + subscriptions: subscriptions, zulipFeatureLevel: zulipFeatureLevel, realmMandatoryTopics: mandatoryTopics, realmAllowMessageEditing: true, @@ -1407,38 +1412,132 @@ void main() { void checkComposeBox({required bool isShown}) => checkComposeBoxIsShown(isShown, bannerLabel: zulipLocalizations.errorBannerCannotPostInChannelLabel); - final narrowTestCases = [ - ('channel', const ChannelNarrow(1)), - ('topic', eg.topicNarrow(1, 'topic')), - ]; - - for (final (String narrowType, Narrow narrow) in narrowTestCases) { - testWidgets('compose box is shown in $narrowType narrow', (tester) async { + const channelNarrow = ChannelNarrow(1); + final topicNarrow = eg.topicNarrow(1, 'topic'); + + void testComposeBoxShown({ + required Narrow narrow, + required bool isChannelSubscribed, + required bool canSend, + required bool expected, + }) { + final description = [ + narrow.toString(), + 'channel subscribed? $isChannelSubscribed', + 'can send?: $canSend', + ].join(', '); + testWidgets(description, (tester) async { + final channel = eg.stream(streamId: 1, + channelPostPolicy: ChannelPostPolicy.moderators); await prepareComposeBox(tester, narrow: narrow, - selfUser: eg.user(role: UserRole.administrator), - streams: [eg.stream(streamId: 1, - channelPostPolicy: ChannelPostPolicy.moderators)]); - checkComposeBox(isShown: true); + selfUser: eg.user( + role: canSend ? UserRole.administrator : UserRole.member), + streams: [channel], + subscriptions: isChannelSubscribed ? [eg.subscription(channel)] : []); + checkComposeBoxIsShown(expected, + bannerLabel: isChannelSubscribed + ? zulipLocalizations.errorBannerCannotPostInChannelLabel + : zulipLocalizations.composeBoxBannerLabelUnsubscribedWhenCannotSend); }); + } + + testComposeBoxShown( + narrow: channelNarrow, + isChannelSubscribed: true, + canSend: true, + expected: true); + + testComposeBoxShown( + narrow: channelNarrow, + isChannelSubscribed: false, + canSend: true, + expected: true); + + testComposeBoxShown( + narrow: channelNarrow, + isChannelSubscribed: true, + canSend: false, + expected: false); + + testComposeBoxShown( + narrow: channelNarrow, + isChannelSubscribed: false, + canSend: false, + expected: false); + + testComposeBoxShown( + narrow: topicNarrow, + isChannelSubscribed: false, + canSend: false, + expected: false); + + void testRefreshSubscribeButtons({required Narrow narrow}) { + testWidgets('Refresh/Subscribe buttons when cannot send and channel unsubscribed, $narrow', (tester) async { + final channel = eg.stream(streamId: 1, + channelPostPolicy: ChannelPostPolicy.administrators); + final messages = List.generate(100, (i) => eg.streamMessage(id: 1000 + i, + stream: channel, topic: topicNarrow.topic.apiName)); - testWidgets('error banner is shown in $narrowType narrow', (tester) async { await prepareComposeBox(tester, - narrow: narrow, - selfUser: eg.user(role: UserRole.moderator), - streams: [eg.stream(streamId: 1, - channelPostPolicy: ChannelPostPolicy.administrators)]); - checkComposeBox(isShown: false); + narrow: ChannelNarrow(channel.streamId), + selfUser: eg.user(role: UserRole.member), + streams: [channel], + subscriptions: [], + messages: messages); + checkComposeBoxIsShown(false, + bannerLabel: zulipLocalizations.composeBoxBannerLabelUnsubscribedWhenCannotSend); + final model = MessageListPage.ancestorOf(state.context).model!; + check(model) + ..fetched.isTrue()..messages.length.equals(100); + + connection.prepare(json: + eg.newestGetMessagesResult(foundOldest: true, messages: messages).toJson(), + delay: Duration(seconds: 1)); + await tester.tap(find.widgetWithText(ZulipWebUiKitButton, 'Refresh')); + await tester.pump(); + check(model) + ..fetched.isFalse()..messages.length.equals(0); + await tester.pump(Duration(seconds: 1)); + check(model) + ..fetched.isTrue()..messages.length.equals(100); + + connection.takeRequests(); + + // prepare subscribe request, then refresh (get-messages) request + connection + ..prepare(json: {}, delay: Duration(milliseconds: 500)) + ..prepare(json: + eg.newestGetMessagesResult(foundOldest: true, messages: messages).toJson(), + delay: Duration(seconds: 1)); + await tester.tap(find.widgetWithText(ZulipWebUiKitButton, 'Subscribe')); + await tester.pump(); + await tester.pump(Duration.zero); + check(connection.lastRequest).isA() + ..method.equals('POST') + ..url.path.equals('/api/v1/users/me/subscriptions') + ..bodyFields.deepEquals({ + 'subscriptions': jsonEncode([{'name': channel.name}]), + }); + await tester.pump(Duration(milliseconds: 500)); + check(model) + ..fetched.isFalse()..messages.length.equals(0); + await tester.pump(Duration(seconds: 1)); + check(model) + ..fetched.isTrue()..messages.length.equals(100); }); } + testRefreshSubscribeButtons(narrow: channelNarrow); + testRefreshSubscribeButtons(narrow: topicNarrow); + testWidgets('user loses privilege -> compose box is replaced with the banner', (tester) async { final selfUser = eg.user(role: UserRole.administrator); await prepareComposeBox(tester, narrow: const ChannelNarrow(1), selfUser: selfUser, - streams: [eg.stream(streamId: 1, - channelPostPolicy: ChannelPostPolicy.administrators)]); + subscriptions: [eg.subscription(eg.stream(streamId: 1, + channelPostPolicy: ChannelPostPolicy.administrators))]); checkComposeBox(isShown: true); await store.handleEvent(RealmUserUpdateEvent(id: 1, @@ -1452,8 +1551,8 @@ void main() { await prepareComposeBox(tester, narrow: const ChannelNarrow(1), selfUser: selfUser, - streams: [eg.stream(streamId: 1, - channelPostPolicy: ChannelPostPolicy.moderators)]); + subscriptions: [eg.subscription(eg.stream(streamId: 1, + channelPostPolicy: ChannelPostPolicy.moderators))]); checkComposeBox(isShown: false); await store.handleEvent(RealmUserUpdateEvent(id: 1, @@ -1470,7 +1569,7 @@ void main() { await prepareComposeBox(tester, narrow: const ChannelNarrow(1), selfUser: selfUser, - streams: [channel]); + subscriptions: [eg.subscription(channel)]); checkComposeBox(isShown: true); await store.handleEvent(eg.channelUpdateEvent(channel, @@ -1488,7 +1587,7 @@ void main() { await prepareComposeBox(tester, narrow: const ChannelNarrow(1), selfUser: selfUser, - streams: [channel]); + subscriptions: [eg.subscription(channel)]); checkComposeBox(isShown: false); await store.handleEvent(eg.channelUpdateEvent(channel,