diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index 3eff182bca..a689d86550 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -24,6 +24,7 @@ import 'emoji_reaction.dart'; import 'icons.dart'; import 'inset_shadow.dart'; import 'message_list.dart'; +import 'page.dart'; import 'store.dart'; import 'text.dart'; import 'theme.dart'; @@ -163,11 +164,15 @@ class ActionSheetCancelButton extends StatelessWidget { } /// Show a sheet of actions you can take on a topic. +/// +/// Needs a [PageRoot] ancestor. void showTopicActionSheet(BuildContext context, { required int channelId, required TopicName topic, }) { - final store = PerAccountStoreWidget.of(context); + final pageContext = PageRoot.contextOf(context); + + final store = PerAccountStoreWidget.of(pageContext); final subscription = store.subscriptions[channelId]; final optionButtons = []; @@ -237,7 +242,7 @@ void showTopicActionSheet(BuildContext context, { currentVisibilityPolicy: visibilityPolicy, newVisibilityPolicy: to, narrow: TopicNarrow(channelId, topic), - pageContext: context); + pageContext: pageContext); })); if (optionButtons.isEmpty) { @@ -250,7 +255,7 @@ void showTopicActionSheet(BuildContext context, { return; } - _showActionSheet(context, optionButtons: optionButtons); + _showActionSheet(pageContext, optionButtons: optionButtons); } class UserTopicUpdateButton extends ActionSheetMenuItemButton { @@ -376,14 +381,15 @@ class UserTopicUpdateButton extends ActionSheetMenuItemButton { /// /// Must have a [MessageListPage] ancestor. void showMessageActionSheet({required BuildContext context, required Message message}) { - final store = PerAccountStoreWidget.of(context); + final pageContext = PageRoot.contextOf(context); + final store = PerAccountStoreWidget.of(pageContext); // The UI that's conditioned on this won't live-update during this appearance // of the action sheet (we avoid calling composeBoxControllerOf in a build // method; see its doc). // So we rely on the fact that isComposeBoxOffered for any given message list // will be constant through the page's life. - final messageListPage = MessageListPage.ancestorOf(context); + final messageListPage = MessageListPage.ancestorOf(pageContext); final isComposeBoxOffered = messageListPage.composeBoxController != null; final isMessageRead = message.flags.contains(MessageFlag.read); @@ -391,18 +397,18 @@ void showMessageActionSheet({required BuildContext context, required Message mes final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead; final optionButtons = [ - ReactionButtons(message: message, pageContext: context), - StarButton(message: message, pageContext: context), + ReactionButtons(message: message, pageContext: pageContext), + StarButton(message: message, pageContext: pageContext), if (isComposeBoxOffered) - QuoteAndReplyButton(message: message, pageContext: context), + QuoteAndReplyButton(message: message, pageContext: pageContext), if (showMarkAsUnreadButton) - MarkAsUnreadButton(message: message, pageContext: context), - CopyMessageTextButton(message: message, pageContext: context), - CopyMessageLinkButton(message: message, pageContext: context), - ShareButton(message: message, pageContext: context), + MarkAsUnreadButton(message: message, pageContext: pageContext), + CopyMessageTextButton(message: message, pageContext: pageContext), + CopyMessageLinkButton(message: message, pageContext: pageContext), + ShareButton(message: message, pageContext: pageContext), ]; - _showActionSheet(context, optionButtons: optionButtons); + _showActionSheet(pageContext, optionButtons: optionButtons); } abstract class MessageActionSheetMenuItemButton extends ActionSheetMenuItemButton { diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index a27a8051e9..c28116ee15 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -277,7 +277,9 @@ class _MessageListPageState extends State implements MessageLis narrow: ChannelNarrow(streamId))))); } - return Scaffold( + // Insert a PageRoot here, to provide a context that can be used for + // MessageListPage.ancestorOf. + return PageRoot(child: Scaffold( appBar: ZulipAppBar( buildTitle: (willCenterTitle) => MessageListAppBarTitle(narrow: narrow, willCenterTitle: willCenterTitle), @@ -318,7 +320,7 @@ class _MessageListPageState extends State implements MessageLis ))), if (ComposeBox.hasComposeBox(narrow)) ComposeBox(key: _composeBoxKey, narrow: narrow) - ]))); + ])))); } } diff --git a/lib/widgets/page.dart b/lib/widgets/page.dart index bebb37c22c..0ba65fb3d4 100644 --- a/lib/widgets/page.dart +++ b/lib/widgets/page.dart @@ -3,6 +3,30 @@ import 'package:flutter/material.dart'; import 'store.dart'; +/// An [InheritedWidget] for near the root of a page's widget subtree, +/// providing its [BuildContext]. +/// +/// Useful when needing a context that persists through the page's lifespan, +/// e.g. for a show-action-sheet function +/// whose buttons use a context to close the sheet +/// or show an error dialog / snackbar asynchronously. +/// +/// (In this scenario, it would be buggy to use the context of the element +/// that was long-pressed, +/// if the element can unmount as part of handling a Zulip event.) +class PageRoot extends InheritedWidget { + const PageRoot({super.key, required super.child}); + + @override + bool updateShouldNotify(covariant PageRoot oldWidget) => false; + + static BuildContext contextOf(BuildContext context) { + final element = context.getElementForInheritedWidgetOfExactType(); + assert(element != null, 'No PageRoot ancestor'); + return element!; + } +} + /// A page route that always builds the same widget. /// /// This is useful for making the route more transparent for a test to inspect. @@ -42,7 +66,10 @@ mixin AccountPageRouteMixin on PageRoute { accountId: accountId, placeholder: loadingPlaceholderPage ?? const LoadingPlaceholderPage(), routeToRemoveOnLogout: this, - child: super.buildPage(context, animation, secondaryAnimation)); + // PageRoot goes under PerAccountStoreWidget, so the provided context + // can be used for PerAccountStoreWidget.of. + child: PageRoot( + child: super.buildPage(context, animation, secondaryAnimation))); } } diff --git a/test/widgets/test_app.dart b/test/widgets/test_app.dart index 4c76fbb2d8..e431aeaf23 100644 --- a/test/widgets/test_app.dart +++ b/test/widgets/test_app.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:zulip/generated/l10n/zulip_localizations.dart'; +import 'package:zulip/widgets/page.dart'; import 'package:zulip/widgets/store.dart'; import 'package:zulip/widgets/theme.dart'; @@ -77,9 +78,9 @@ class TestZulipApp extends StatelessWidget { navigatorObservers: navigatorObservers ?? const [], home: accountId != null - ? PerAccountStoreWidget(accountId: accountId!, child: child) - : child, - ); + ? PerAccountStoreWidget(accountId: accountId!, + child: PageRoot(child: child)) + : PageRoot(child: child)); })); } }