Skip to content

Commit a9d9ef8

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 #465.
1 parent 5380574 commit a9d9ef8

File tree

8 files changed

+89
-7
lines changed

8 files changed

+89
-7
lines changed

lib/widgets/app.dart

Lines changed: 4 additions & 1 deletion
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';
@@ -252,7 +253,9 @@ class HomePage extends StatelessWidget {
252253
}
253254

254255
return Scaffold(
255-
appBar: AppBar(title: const Text("Home")),
256+
appBar: ZulipAppBar(
257+
title: const Text("Home"),
258+
isLoading: store.isLoading),
256259
body: Center(
257260
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
258261
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';
@@ -268,7 +269,9 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
268269
}
269270

270271
return Scaffold(
271-
appBar: AppBar(title: MessageListAppBarTitle(narrow: narrow),
272+
appBar: ZulipAppBar(
273+
title: MessageListAppBarTitle(narrow: narrow),
274+
isLoading: store.isLoading,
272275
backgroundColor: appBarBackgroundColor,
273276
shape: removeAppBarBottomBorder
274277
? const Border()

lib/widgets/profile.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../api/model/model.dart';
88
import '../model/content.dart';
99
import '../model/narrow.dart';
1010
import '../model/store.dart';
11+
import 'app_bar.dart';
1112
import 'content.dart';
1213
import 'message_list.dart';
1314
import 'page.dart';
@@ -102,7 +103,9 @@ class ProfilePage extends StatelessWidget {
102103
];
103104

104105
return Scaffold(
105-
appBar: AppBar(title: Text(user.fullName)),
106+
appBar: ZulipAppBar(
107+
title: Text(user.fullName),
108+
isLoading: store.isLoading),
106109
body: SingleChildScrollView(
107110
child: Center(
108111
child: ConstrainedBox(
@@ -120,8 +123,11 @@ class _ProfileErrorPage extends StatelessWidget {
120123

121124
@override
122125
Widget build(BuildContext context) {
126+
final store = PerAccountStoreWidget.of(context);
123127
return Scaffold(
124-
appBar: AppBar(title: const Text('Error')),
128+
appBar: ZulipAppBar(
129+
title: const Text('Error'),
130+
isLoading: store.isLoading),
125131
body: const SingleChildScrollView(
126132
child: Padding(
127133
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)