Skip to content

Commit 69e19aa

Browse files
committed
ui: Extract ZulipAppBar for loading indicator.
Ideally we may have test to exhaustively ensure that all pages specific to a single PerAccountStore use ZulipAppBar. Some pages with `AppBar`s are skipped, such as AboutZulip (no PerAccountStore access) and lightboxes (progress indicator is occupied for other purposes, and the AppBar can be hidden). Fixes zulip#465. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 391167b commit 69e19aa

File tree

8 files changed

+92
-9
lines changed

8 files changed

+92
-9
lines changed

lib/widgets/app.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
88
import '../model/localizations.dart';
99
import '../model/narrow.dart';
1010
import 'about_zulip.dart';
11+
import 'app_bar.dart';
1112
import 'inbox.dart';
1213
import 'login.dart';
1314
import 'message_list.dart';
@@ -179,9 +180,10 @@ class ChooseAccountPage extends StatelessWidget {
179180
assert(!PerAccountStoreWidget.debugExistsOf(context));
180181
final globalStore = GlobalStoreWidget.of(context);
181182
return Scaffold(
182-
appBar: AppBar(
183+
appBar: ZulipAppBar(
183184
title: Text(zulipLocalizations.chooseAccountPageTitle),
184-
actions: const [ChooseAccountPageOverflowButton()]),
185+
actions: const [ChooseAccountPageOverflowButton()],
186+
isLoading: false),
185187
body: SafeArea(
186188
minimum: const EdgeInsets.fromLTRB(8, 0, 8, 8),
187189
child: Center(
@@ -252,7 +254,9 @@ class HomePage extends StatelessWidget {
252254
}
253255

254256
return Scaffold(
255-
appBar: AppBar(title: const Text("Home")),
257+
appBar: ZulipAppBar(
258+
title: const Text("Home"),
259+
isLoading: store.isLoading),
256260
body: Center(
257261
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
258262
DefaultTextStyle.merge(

lib/widgets/app_bar.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:flutter/material.dart';
2+
3+
/// A custom [AppBar] with a loading indicator.
4+
///
5+
/// This should be used for most of the pages with access to [PerAccountStore].
6+
// However, there are some exceptions (add more if necessary):
7+
// - `lib/widgets/lightbox.dart`
8+
class ZulipAppBar extends AppBar {
9+
ZulipAppBar({
10+
super.key,
11+
required super.title,
12+
super.backgroundColor,
13+
super.shape,
14+
super.actions,
15+
required bool isLoading,
16+
}) : super(
17+
bottom: PreferredSize(
18+
preferredSize: const Size.fromHeight(4.0),
19+
child: (isLoading)
20+
? LinearProgressIndicator(backgroundColor: backgroundColor, minHeight: 4.0)
21+
: const SizedBox.shrink()));
22+
}

lib/widgets/inbox.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '../api/model/model.dart';
44
import '../model/narrow.dart';
55
import '../model/recent_dm_conversations.dart';
66
import '../model/unreads.dart';
7+
import 'app_bar.dart';
78
import 'icons.dart';
89
import 'message_list.dart';
910
import 'page.dart';
@@ -160,7 +161,9 @@ class _InboxPageState extends State<InboxPage> with PerAccountStoreAwareStateMix
160161
}
161162

162163
return Scaffold(
163-
appBar: AppBar(title: const Text('Inbox')),
164+
appBar: ZulipAppBar(
165+
title: const Text('Inbox'),
166+
isLoading: store.isLoading),
164167
body: SafeArea(
165168
// Don't pad the bottom here; we want the list content to do that.
166169
bottom: false,

lib/widgets/message_list.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../model/store.dart';
1313
import '../model/typing_status.dart';
1414
import 'action_sheet.dart';
1515
import 'actions.dart';
16+
import 'app_bar.dart';
1617
import 'compose_box.dart';
1718
import 'content.dart';
1819
import 'dialog.dart';
@@ -256,7 +257,9 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
256257
}
257258

258259
return Scaffold(
259-
appBar: AppBar(title: MessageListAppBarTitle(narrow: widget.narrow),
260+
appBar: ZulipAppBar(
261+
title: MessageListAppBarTitle(narrow: widget.narrow),
262+
isLoading: store.isLoading,
260263
backgroundColor: appBarBackgroundColor,
261264
shape: removeAppBarBottomBorder
262265
? const Border()

lib/widgets/profile.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
66
import '../api/model/model.dart';
77
import '../model/content.dart';
88
import '../model/narrow.dart';
9+
import 'app_bar.dart';
910
import 'content.dart';
1011
import 'message_list.dart';
1112
import 'page.dart';
@@ -69,7 +70,9 @@ class ProfilePage extends StatelessWidget {
6970
];
7071

7172
return Scaffold(
72-
appBar: AppBar(title: Text(user.fullName)),
73+
appBar: ZulipAppBar(
74+
title: Text(user.fullName),
75+
isLoading: store.isLoading),
7376
body: SingleChildScrollView(
7477
child: Center(
7578
child: ConstrainedBox(
@@ -87,8 +90,11 @@ class _ProfileErrorPage extends StatelessWidget {
8790

8891
@override
8992
Widget build(BuildContext context) {
93+
final store = PerAccountStoreWidget.of(context);
9094
return Scaffold(
91-
appBar: AppBar(title: const Text('Error')),
95+
appBar: ZulipAppBar(
96+
title: const Text('Error'),
97+
isLoading: store.isLoading),
9298
body: const SingleChildScrollView(
9399
child: Padding(
94100
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 32),

lib/widgets/recent_dm_conversations.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
44
import '../model/narrow.dart';
55
import '../model/recent_dm_conversations.dart';
66
import '../model/unreads.dart';
7+
import 'app_bar.dart';
78
import 'content.dart';
89
import 'icons.dart';
910
import 'message_list.dart';
@@ -55,10 +56,13 @@ class _RecentDmConversationsPageState extends State<RecentDmConversationsPage> w
5556

5657
@override
5758
Widget build(BuildContext context) {
59+
final store = PerAccountStoreWidget.of(context);
5860
final zulipLocalizations = ZulipLocalizations.of(context);
5961
final sorted = model!.sorted;
6062
return Scaffold(
61-
appBar: AppBar(title: Text(zulipLocalizations.recentDmConversationsPageTitle)),
63+
appBar: ZulipAppBar(
64+
title: Text(zulipLocalizations.recentDmConversationsPageTitle),
65+
isLoading: store.isLoading),
6266
body: SafeArea(
6367
// Don't pad the bottom here; we want the list content to do that.
6468
bottom: false,

lib/widgets/subscription_list.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import '../api/model/model.dart';
44
import '../model/narrow.dart';
55
import '../model/unreads.dart';
6+
import 'app_bar.dart';
67
import 'icons.dart';
78
import 'message_list.dart';
89
import 'page.dart';
@@ -89,7 +90,9 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> with PerAcc
8990
_sortSubs(unpinned);
9091

9192
return Scaffold(
92-
appBar: AppBar(title: const Text("Channels")),
93+
appBar: ZulipAppBar(
94+
title: const Text("Channels"),
95+
isLoading: store.isLoading),
9396
body: SafeArea(
9497
// Don't pad the bottom here; we want the list content to do that.
9598
bottom: false,

test/widgets/app_bar_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:zulip/widgets/app_bar.dart';
5+
import 'package:zulip/widgets/profile.dart';
6+
7+
import '../example_data.dart' as eg;
8+
import '../model/binding.dart';
9+
import '../model/test_store.dart';
10+
import 'test_app.dart';
11+
12+
void main() {
13+
TestZulipBinding.ensureInitialized();
14+
15+
testWidgets('show progress indicator when loading', (tester) async {
16+
addTearDown(testBinding.reset);
17+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
18+
19+
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
20+
await store.addUser(eg.selfUser);
21+
22+
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
23+
child: ProfilePage(userId: eg.selfUser.userId)));
24+
25+
final finder = find.descendant(
26+
of: find.byType(ZulipAppBar),
27+
matching: find.byType(LinearProgressIndicator));
28+
29+
await tester.pumpAndSettle();
30+
final rectBefore = tester.getRect(find.byType(ZulipAppBar));
31+
check(finder.evaluate()).isEmpty();
32+
store.isLoading = true;
33+
34+
await tester.pump();
35+
check(tester.getRect(find.byType(ZulipAppBar))).equals(rectBefore);
36+
check(finder.evaluate()).single;
37+
});
38+
}

0 commit comments

Comments
 (0)