Skip to content

Commit a6f9934

Browse files
committed
store: Add a GlobalStore and corresponding root widget
1 parent 839dd6b commit a6f9934

File tree

3 files changed

+127
-13
lines changed

3 files changed

+127
-13
lines changed

lib/model/store.dart

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,50 @@ import '../api/route/messages.dart';
1313
import '../credential_fixture.dart' as credentials;
1414
import 'message_list.dart';
1515

16+
/// Store for the user's cross-account data.
17+
///
18+
/// This includes data that is independent of the account, like some settings.
19+
/// It also includes a small amount of data for each account: enough to
20+
/// authenticate as the active account, if there is one.
21+
class GlobalStore extends ChangeNotifier {
22+
GlobalStore._({required Map<int, Account> accounts})
23+
: _accounts = accounts;
24+
25+
// For convenience, a number we won't use as an ID in the database table.
26+
static const fixtureAccountId = -1;
27+
28+
// We keep the API simple and synchronous for the bulk of the app's code
29+
// by doing this loading up front before constructing a [GlobalStore].
30+
static Future<GlobalStore> load() async {
31+
const accounts = {fixtureAccountId: _fixtureAccount};
32+
return GlobalStore._(accounts: accounts);
33+
}
34+
35+
final Map<int, Account> _accounts;
36+
37+
// TODO settings (those that are per-device rather than per-account)
38+
// TODO push token, and other data corresponding to GlobalSessionState
39+
40+
// Just an Iterable, not the actual Map, to avoid clients mutating the map.
41+
// Mutations should go through the setters/mutators below.
42+
Iterable<Account> get accounts => _accounts.values;
43+
44+
Account? getAccount(int id) => _accounts[id];
45+
46+
// TODO add setters/mutators; will want to write to database
47+
// Future<void> insertAccount...
48+
// Future<void> updateAccount...
49+
50+
// TODO add a registry of [PerAccountStore]s, like the latter's of [MessageListView]
51+
// That will allow us to have many [PerAccountRoot] widgets for a given
52+
// account, e.g. at the top of each page; and to access server data from
53+
// outside any [PerAccountRoot], e.g. for handling a notification.
54+
}
55+
56+
/// Store for the user's data for a given Zulip account.
57+
///
58+
/// This should always have a consistent snapshot of the state on the server,
59+
/// as maintained by the Zulip event system.
1660
class PerAccountStore extends ChangeNotifier {
1761
PerAccountStore._({
1862
required this.account,
@@ -23,9 +67,10 @@ class PerAccountStore extends ChangeNotifier {
2367
required this.subscriptions,
2468
});
2569

26-
// Load the user's data from storage. (Once we have such a thing.)
27-
static Future<PerAccountStore> load() async {
28-
const account = _fixtureAccount;
70+
/// Load the user's data from the server, and start an event queue going.
71+
///
72+
/// In the future this might load an old snapshot from local storage first.
73+
static Future<PerAccountStore> load(Account account) async {
2974
final connection = ApiConnection(auth: account);
3075

3176
final stopwatch = Stopwatch()..start();
@@ -124,6 +169,7 @@ const Account _fixtureAccount = Account(
124169
apiKey: credentials.api_key,
125170
);
126171

172+
@immutable
127173
class Account implements Auth {
128174
const Account(
129175
{required this.realmUrl, required this.email, required this.apiKey});

lib/widgets/app.dart

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

3+
import '../model/store.dart';
34
import 'compose_box.dart';
45
import 'message_list.dart';
56
import 'store.dart';
@@ -19,12 +20,14 @@ class ZulipApp extends StatelessWidget {
1920
// Or try this tool to see the whole palette:
2021
// https://m3.material.io/theme-builder#/custom
2122
colorScheme: ColorScheme.fromSeed(seedColor: kZulipBrandColor));
22-
// Just one account for now.
23-
return PerAccountRoot(
24-
child: MaterialApp(
25-
title: 'Zulip',
26-
theme: theme,
27-
home: const HomePage()));
23+
return DataRoot(
24+
child: PerAccountRoot(
25+
// Just one account for now.
26+
accountId: GlobalStore.fixtureAccountId,
27+
child: MaterialApp(
28+
title: 'Zulip',
29+
theme: theme,
30+
home: const HomePage())));
2831
}
2932
}
3033

lib/widgets/store.dart

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,62 @@ import 'package:flutter/material.dart';
22

33
import '../model/store.dart';
44

5+
class DataRoot extends StatefulWidget {
6+
const DataRoot({super.key, required this.child});
7+
8+
final Widget child;
9+
10+
@override
11+
State<DataRoot> createState() => _DataRootState();
12+
}
13+
14+
class _DataRootState extends State<DataRoot> {
15+
GlobalStore? store;
16+
17+
@override
18+
void initState() {
19+
super.initState();
20+
(() async {
21+
final store = await GlobalStore.load();
22+
setState(() {
23+
this.store = store;
24+
});
25+
})();
26+
}
27+
28+
@override
29+
Widget build(BuildContext context) {
30+
final store = this.store;
31+
// TODO: factor out the use of LoadingPage to be configured by the widget, like [widget.child] is
32+
if (store == null) return const LoadingPage();
33+
return GlobalStoreWidget(store: store, child: widget.child);
34+
}
35+
}
36+
37+
class GlobalStoreWidget extends InheritedNotifier<GlobalStore> {
38+
const GlobalStoreWidget(
39+
{super.key, required GlobalStore store, required super.child})
40+
: super(notifier: store);
41+
42+
GlobalStore get store => notifier!;
43+
44+
static GlobalStore of(BuildContext context) {
45+
final widget =
46+
context.dependOnInheritedWidgetOfExactType<GlobalStoreWidget>();
47+
assert(widget != null, 'No GlobalStoreWidget ancestor');
48+
return widget!.store;
49+
}
50+
51+
@override
52+
bool updateShouldNotify(covariant GlobalStoreWidget oldWidget) =>
53+
store != oldWidget.store;
54+
}
55+
556
class PerAccountRoot extends StatefulWidget {
6-
const PerAccountRoot({super.key, required this.child});
57+
const PerAccountRoot(
58+
{super.key, required this.accountId, required this.child});
759

60+
final int accountId;
861
final Widget child;
962

1063
@override
@@ -15,10 +68,22 @@ class _PerAccountRootState extends State<PerAccountRoot> {
1568
PerAccountStore? store;
1669

1770
@override
18-
void initState() {
19-
super.initState();
71+
void didChangeDependencies() {
72+
super.didChangeDependencies();
73+
final globalStore = GlobalStoreWidget.of(context);
74+
final account = globalStore.getAccount(widget.accountId);
75+
assert(account != null, 'Account not found on global store');
76+
if (store != null) {
77+
// The data we use to auth to the server should be unchanged;
78+
// changing those should mean a new account ID in our database.
79+
assert(account!.realmUrl == store!.account.realmUrl);
80+
assert(account!.email == store!.account.email);
81+
assert(account!.apiKey == store!.account.apiKey);
82+
// TODO if Account has anything else change, update the PerAccountStore for that
83+
return;
84+
}
2085
(() async {
21-
final store = await PerAccountStore.load();
86+
final store = await PerAccountStore.load(account!);
2287
setState(() {
2388
this.store = store;
2489
});

0 commit comments

Comments
 (0)