Skip to content

Commit 432edd5

Browse files
committed
action_sheet: Add and use PageRoot
1 parent efd0a55 commit 432edd5

File tree

4 files changed

+55
-19
lines changed

4 files changed

+55
-19
lines changed

lib/widgets/action_sheet.dart

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'emoji_reaction.dart';
2424
import 'icons.dart';
2525
import 'inset_shadow.dart';
2626
import 'message_list.dart';
27+
import 'page.dart';
2728
import 'store.dart';
2829
import 'text.dart';
2930
import 'theme.dart';
@@ -163,11 +164,15 @@ class ActionSheetCancelButton extends StatelessWidget {
163164
}
164165

165166
/// Show a sheet of actions you can take on a topic.
167+
///
168+
/// Needs a [PageRoot] ancestor.
166169
void showTopicActionSheet(BuildContext context, {
167170
required int channelId,
168171
required TopicName topic,
169172
}) {
170-
final store = PerAccountStoreWidget.of(context);
173+
final pageContext = PageRoot.contextOf(context);
174+
175+
final store = PerAccountStoreWidget.of(pageContext);
171176
final subscription = store.subscriptions[channelId];
172177

173178
final optionButtons = <ActionSheetMenuItemButton>[];
@@ -237,7 +242,7 @@ void showTopicActionSheet(BuildContext context, {
237242
currentVisibilityPolicy: visibilityPolicy,
238243
newVisibilityPolicy: to,
239244
narrow: TopicNarrow(channelId, topic),
240-
pageContext: context);
245+
pageContext: pageContext);
241246
}));
242247

243248
if (optionButtons.isEmpty) {
@@ -250,7 +255,7 @@ void showTopicActionSheet(BuildContext context, {
250255
return;
251256
}
252257

253-
_showActionSheet(context, optionButtons: optionButtons);
258+
_showActionSheet(pageContext, optionButtons: optionButtons);
254259
}
255260

256261
class UserTopicUpdateButton extends ActionSheetMenuItemButton {
@@ -376,33 +381,34 @@ class UserTopicUpdateButton extends ActionSheetMenuItemButton {
376381
///
377382
/// Must have a [MessageListPage] ancestor.
378383
void showMessageActionSheet({required BuildContext context, required Message message}) {
379-
final store = PerAccountStoreWidget.of(context);
384+
final pageContext = PageRoot.contextOf(context);
385+
final store = PerAccountStoreWidget.of(pageContext);
380386

381387
// The UI that's conditioned on this won't live-update during this appearance
382388
// of the action sheet (we avoid calling composeBoxControllerOf in a build
383389
// method; see its doc).
384390
// So we rely on the fact that isComposeBoxOffered for any given message list
385391
// will be constant through the page's life.
386-
final messageListPage = MessageListPage.ancestorOf(context);
392+
final messageListPage = MessageListPage.ancestorOf(pageContext);
387393
final isComposeBoxOffered = messageListPage.composeBoxController != null;
388394

389395
final isMessageRead = message.flags.contains(MessageFlag.read);
390396
final markAsUnreadSupported = store.connection.zulipFeatureLevel! >= 155; // TODO(server-6)
391397
final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;
392398

393399
final optionButtons = [
394-
ReactionButtons(message: message, pageContext: context),
395-
StarButton(message: message, pageContext: context),
400+
ReactionButtons(message: message, pageContext: pageContext),
401+
StarButton(message: message, pageContext: pageContext),
396402
if (isComposeBoxOffered)
397-
QuoteAndReplyButton(message: message, pageContext: context),
403+
QuoteAndReplyButton(message: message, pageContext: pageContext),
398404
if (showMarkAsUnreadButton)
399-
MarkAsUnreadButton(message: message, pageContext: context),
400-
CopyMessageTextButton(message: message, pageContext: context),
401-
CopyMessageLinkButton(message: message, pageContext: context),
402-
ShareButton(message: message, pageContext: context),
405+
MarkAsUnreadButton(message: message, pageContext: pageContext),
406+
CopyMessageTextButton(message: message, pageContext: pageContext),
407+
CopyMessageLinkButton(message: message, pageContext: pageContext),
408+
ShareButton(message: message, pageContext: pageContext),
403409
];
404410

405-
_showActionSheet(context, optionButtons: optionButtons);
411+
_showActionSheet(pageContext, optionButtons: optionButtons);
406412
}
407413

408414
abstract class MessageActionSheetMenuItemButton extends ActionSheetMenuItemButton {

lib/widgets/message_list.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,9 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
277277
narrow: ChannelNarrow(streamId)))));
278278
}
279279

280-
return Scaffold(
280+
// Insert a PageRoot here, to provide a context that can be used for
281+
// MessageListPage.ancestorOf.
282+
return PageRoot(child: Scaffold(
281283
appBar: ZulipAppBar(
282284
buildTitle: (willCenterTitle) =>
283285
MessageListAppBarTitle(narrow: narrow, willCenterTitle: willCenterTitle),
@@ -318,7 +320,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
318320
))),
319321
if (ComposeBox.hasComposeBox(narrow))
320322
ComposeBox(key: _composeBoxKey, narrow: narrow)
321-
])));
323+
]))));
322324
}
323325
}
324326

lib/widgets/page.dart

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,30 @@ import 'package:flutter/material.dart';
33

44
import 'store.dart';
55

6+
/// An [InheritedWidget] for near the root of a page's widget subtree,
7+
/// providing its [BuildContext].
8+
///
9+
/// Useful when needing a context that persists through the page's lifespan,
10+
/// e.g. for a show-action-sheet function
11+
/// whose buttons use a context to close the sheet
12+
/// or show an error dialog / snackbar asynchronously.
13+
///
14+
/// (In this scenario, it would be buggy to use the context of the element
15+
/// that was long-pressed,
16+
/// if the element can unmount as part of handling a Zulip event.)
17+
class PageRoot extends InheritedWidget {
18+
const PageRoot({super.key, required super.child});
19+
20+
@override
21+
bool updateShouldNotify(covariant PageRoot oldWidget) => false;
22+
23+
static BuildContext contextOf(BuildContext context) {
24+
final element = context.getElementForInheritedWidgetOfExactType<PageRoot>();
25+
assert(element != null, 'No PageRoot ancestor');
26+
return element!;
27+
}
28+
}
29+
630
/// A page route that always builds the same widget.
731
///
832
/// This is useful for making the route more transparent for a test to inspect.
@@ -42,7 +66,10 @@ mixin AccountPageRouteMixin<T extends Object?> on PageRoute<T> {
4266
accountId: accountId,
4367
placeholder: loadingPlaceholderPage ?? const LoadingPlaceholderPage(),
4468
routeToRemoveOnLogout: this,
45-
child: super.buildPage(context, animation, secondaryAnimation));
69+
// PageRoot goes under PerAccountStoreWidget, so the provided context
70+
// can be used for PerAccountStoreWidget.of.
71+
child: PageRoot(
72+
child: super.buildPage(context, animation, secondaryAnimation)));
4673
}
4774
}
4875

test/widgets/test_app.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22

33
import 'package:zulip/generated/l10n/zulip_localizations.dart';
4+
import 'package:zulip/widgets/page.dart';
45
import 'package:zulip/widgets/store.dart';
56
import 'package:zulip/widgets/theme.dart';
67

@@ -77,9 +78,9 @@ class TestZulipApp extends StatelessWidget {
7778
navigatorObservers: navigatorObservers ?? const [],
7879

7980
home: accountId != null
80-
? PerAccountStoreWidget(accountId: accountId!, child: child)
81-
: child,
82-
);
81+
? PerAccountStoreWidget(accountId: accountId!,
82+
child: PageRoot(child: child))
83+
: PageRoot(child: child));
8384
}));
8485
}
8586
}

0 commit comments

Comments
 (0)