diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index db99685724..f245c25bd8 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../model/store.dart'; import 'compose_box.dart'; import 'message_list.dart'; +import 'page.dart'; import 'store.dart'; class ZulipApp extends StatelessWidget { @@ -11,23 +12,23 @@ class ZulipApp extends StatelessWidget { @override Widget build(BuildContext context) { final theme = ThemeData( - // This applies Material 3's color system to produce a palette of - // appropriately matching and contrasting colors for use in a UI. - // The Zulip brand color is a starting point, but doesn't end up as - // one that's directly used. (After all, we didn't design it for that - // purpose; we designed a logo.) See docs: - // https://api.flutter.dev/flutter/material/ColorScheme/ColorScheme.fromSeed.html - // Or try this tool to see the whole palette: - // https://m3.material.io/theme-builder#/custom - colorScheme: ColorScheme.fromSeed(seedColor: kZulipBrandColor)); + // This applies Material 3's color system to produce a palette of + // appropriately matching and contrasting colors for use in a UI. + // The Zulip brand color is a starting point, but doesn't end up as + // one that's directly used. (After all, we didn't design it for that + // purpose; we designed a logo.) See docs: + // https://api.flutter.dev/flutter/material/ColorScheme/ColorScheme.fromSeed.html + // Or try this tool to see the whole palette: + // https://m3.material.io/theme-builder#/custom + colorScheme: ColorScheme.fromSeed(seedColor: kZulipBrandColor)); return GlobalStoreWidget( - child: PerAccountStoreWidget( - // Just one account for now. - accountId: LiveGlobalStore.fixtureAccountId, - child: MaterialApp( - title: 'Zulip', - theme: theme, - home: const HomePage()))); + child: MaterialApp( + title: 'Zulip', + theme: theme, + home: const PerAccountStoreWidget( + // Just one account for now. + accountId: LiveGlobalStore.fixtureAccountId, + child: HomePage()))); } } @@ -45,36 +46,34 @@ class HomePage extends StatelessWidget { final store = PerAccountStoreWidget.of(context); InlineSpan bold(String text) => TextSpan( - text: text, style: const TextStyle(fontWeight: FontWeight.bold)); + text: text, style: const TextStyle(fontWeight: FontWeight.bold)); return Scaffold( - appBar: AppBar(title: const Text("Home")), - body: Center( - child: - Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + appBar: AppBar(title: const Text("Home")), + body: Center( + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ DefaultTextStyle.merge( - style: const TextStyle(fontSize: 18), - child: Column(children: [ - const Text('🚧 Under construction 🚧'), - const SizedBox(height: 12), - Text.rich(TextSpan( - text: 'Connected to: ', - children: [bold(store.account.realmUrl)])), - Text.rich(TextSpan( - text: 'Zulip server version: ', - children: [bold(store.zulip_version)])), - Text.rich(TextSpan(text: 'Subscribed to ', children: [ - bold(store.subscriptions.length.toString()), - const TextSpan(text: ' streams'), - ])), + style: const TextStyle(fontSize: 18), + child: Column(children: [ + const Text('🚧 Under construction 🚧'), + const SizedBox(height: 12), + Text.rich(TextSpan( + text: 'Connected to: ', + children: [bold(store.account.realmUrl)])), + Text.rich(TextSpan( + text: 'Zulip server version: ', + children: [bold(store.zulip_version)])), + Text.rich(TextSpan(text: 'Subscribed to ', children: [ + bold(store.subscriptions.length.toString()), + const TextSpan(text: ' streams'), ])), + ])), const SizedBox(height: 16), ElevatedButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const MessageListPage())), - child: const Text("All messages")) + onPressed: () => Navigator.push(context, + MaterialAccountPageRoute(context: context, builder: (context) => + const MessageListPage())), + child: const Text("All messages")), ]))); } } @@ -85,21 +84,22 @@ class MessageListPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("All messages")), - body: Builder( - builder: (BuildContext context) => Center( - child: Column(children: [ - MediaQuery.removePadding( - // Scaffold knows about the app bar, and so has run this - // BuildContext, which is under `body`, through - // MediaQuery.removePadding with `removeTop: true`. - context: context, + appBar: AppBar(title: const Text("All messages")), + body: Builder( + builder: (BuildContext context) => Center( + child: Column(children: [ + MediaQuery.removePadding( + // Scaffold knows about the app bar, and so has run this + // BuildContext, which is under `body`, through + // MediaQuery.removePadding with `removeTop: true`. + context: context, - // The compose box pads the bottom inset. - removeBottom: true, + // The compose box pads the bottom inset. + removeBottom: true, - child: const Expanded( - child: MessageList())), - const StreamComposeBox()])))); + child: const Expanded( + child: MessageList())), + const StreamComposeBox(), + ])))); } } diff --git a/lib/widgets/page.dart b/lib/widgets/page.dart new file mode 100644 index 0000000000..4e2a836498 --- /dev/null +++ b/lib/widgets/page.dart @@ -0,0 +1,50 @@ + +import 'package:flutter/material.dart'; + +import 'store.dart'; + +mixin AccountPageRouteMixin on PageRoute { + int get accountId; + + @override + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + return PerAccountStoreWidget( + accountId: accountId, + child: super.buildPage(context, animation, secondaryAnimation)); + } +} + +class MaterialAccountPageRoute extends MaterialPageRoute with AccountPageRouteMixin { + MaterialAccountPageRoute({ + required BuildContext context, + required super.builder, + super.settings, + super.maintainState, + super.fullscreenDialog, + super.allowSnapshotting, + }) : accountId = PerAccountStoreWidget.accountIdOf(context); + + @override + final int accountId; +} + +class AccountPageRouteBuilder extends PageRouteBuilder with AccountPageRouteMixin { + AccountPageRouteBuilder({ + required BuildContext context, + super.settings, + required super.pageBuilder, + super.transitionsBuilder, + super.transitionDuration, + super.reverseTransitionDuration, + super.opaque, + super.barrierDismissible, + super.barrierColor, + super.barrierLabel, + super.maintainState, + super.fullscreenDialog, + super.allowSnapshotting, + }) : accountId = PerAccountStoreWidget.accountIdOf(context); + + @override + final int accountId; +} diff --git a/lib/widgets/store.dart b/lib/widgets/store.dart index 21ae889689..1386f2bfa9 100644 --- a/lib/widgets/store.dart +++ b/lib/widgets/store.dart @@ -132,9 +132,10 @@ class PerAccountStoreWidget extends StatefulWidget { /// [BuildContext.dependOnInheritedWidgetOfExactType]. /// /// See also: - /// * [InheritedNotifier], which provides the "dependency" mechanism. + /// * [accountIdOf], for the account ID corresponding to the same data. /// * [GlobalStoreWidget.of], for the app's data beyond that of a /// particular account. + /// * [InheritedNotifier], which provides the "dependency" mechanism. static PerAccountStore of(BuildContext context) { final widget = context .dependOnInheritedWidgetOfExactType<_PerAccountStoreInheritedWidget>(); @@ -142,6 +143,27 @@ class PerAccountStoreWidget extends StatefulWidget { return widget!.store; } + /// Our account ID for the relevant account for this widget. + /// + /// As with [of], the data is taken from the closest [PerAccountStoreWidget] + /// that encloses the given context. Throws an error if there is no enclosing + /// [PerAccountStoreWidget]. + /// + /// Unlike [of], this method does not create a dependency relationship, and + /// updates to the [PerAccountStoreWidget] will not cause the calling widget + /// to be rebuilt. As a result, this should not be called from build methods, + /// but is appropriate to use in interaction event handlers. For more, see + /// [BuildContext.findAncestorWidgetOfExactType]. + /// + /// Like [of], the cost of this method is O(1) with a small constant factor. + static int accountIdOf(BuildContext context) { + final element = context.getElementForInheritedWidgetOfExactType<_PerAccountStoreInheritedWidget>(); + assert(element != null, 'No PerAccountStoreWidget ancestor'); + final widget = element!.findAncestorWidgetOfExactType(); + assert(widget != null); + return widget!.accountId; + } + @override State createState() => _PerAccountStoreWidgetState(); } diff --git a/pubspec.lock b/pubspec.lock index 3c29011fcd..b140d5d10b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: