Skip to content

Commit

Permalink
action_sheet: Add and use PageRoot
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbobbe committed Feb 7, 2025
1 parent efd0a55 commit 432edd5
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 19 deletions.
32 changes: 19 additions & 13 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = <ActionSheetMenuItemButton>[];
Expand Down Expand Up @@ -237,7 +242,7 @@ void showTopicActionSheet(BuildContext context, {
currentVisibilityPolicy: visibilityPolicy,
newVisibilityPolicy: to,
narrow: TopicNarrow(channelId, topic),
pageContext: context);
pageContext: pageContext);
}));

if (optionButtons.isEmpty) {
Expand All @@ -250,7 +255,7 @@ void showTopicActionSheet(BuildContext context, {
return;
}

_showActionSheet(context, optionButtons: optionButtons);
_showActionSheet(pageContext, optionButtons: optionButtons);
}

class UserTopicUpdateButton extends ActionSheetMenuItemButton {
Expand Down Expand Up @@ -376,33 +381,34 @@ 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);
final markAsUnreadSupported = store.connection.zulipFeatureLevel! >= 155; // TODO(server-6)
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 {
Expand Down
6 changes: 4 additions & 2 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,9 @@ class _MessageListPageState extends State<MessageListPage> 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),
Expand Down Expand Up @@ -318,7 +320,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
))),
if (ComposeBox.hasComposeBox(narrow))
ComposeBox(key: _composeBoxKey, narrow: narrow)
])));
]))));
}
}

Expand Down
29 changes: 28 additions & 1 deletion lib/widgets/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<PageRoot>();
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.
Expand Down Expand Up @@ -42,7 +66,10 @@ mixin AccountPageRouteMixin<T extends Object?> on PageRoute<T> {
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)));
}
}

Expand Down
7 changes: 4 additions & 3 deletions test/widgets/test_app.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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));
}));
}
}

0 comments on commit 432edd5

Please sign in to comment.