Skip to content

Commit 88540cc

Browse files
committed
channel_list: Setup "All Channels" page
Fixes parts of zulip#188
1 parent f1dd9de commit 88540cc

File tree

5 files changed

+187
-2
lines changed

5 files changed

+187
-2
lines changed

assets/l10n/app_en.arb

+11
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,13 @@
359359
"num": {"type": "int", "example": "4"}
360360
}
361361
},
362+
"browseMoreNChannels": "Browse {num, plural, =1{1 more channel} other{{num} more channels}}",
363+
"@browseMoreNChannels": {
364+
"description": "Label showing the number of other channels that user can subscribe to",
365+
"placeholders": {
366+
"num": {"type": "int", "example": "4"}
367+
}
368+
},
362369
"errorInvalidResponse": "The server sent an invalid response",
363370
"@errorInvalidResponse": {
364371
"description": "Error message when an API call returned an invalid response."
@@ -472,6 +479,10 @@
472479
"@combinedFeedPageTitle": {
473480
"description": "Title for the page of combined feed."
474481
},
482+
"channelListPageTitle": "All Channels",
483+
"@channelListPageTitle": {
484+
"description": "Title for the page of all channels."
485+
},
475486
"notifGroupDmConversationLabel": "{senderFullName} to you and {numOthers, plural, =1{1 other} other{{numOthers} others}}",
476487
"@notifGroupDmConversationLabel": {
477488
"description": "Label for a group DM conversation notification.",

lib/widgets/channel_list.dart

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
3+
4+
import '../api/model/model.dart';
5+
import '../model/narrow.dart';
6+
import 'icons.dart';
7+
import 'message_list.dart';
8+
import 'page.dart';
9+
import 'store.dart';
10+
11+
class ChannelListPage extends StatefulWidget {
12+
const ChannelListPage({super.key});
13+
14+
static Route<void> buildRoute({int? accountId, BuildContext? context}) {
15+
return MaterialAccountWidgetRoute(accountId: accountId, context: context,
16+
page: const ChannelListPage());
17+
}
18+
19+
@override
20+
State<ChannelListPage> createState() => _ChannelListPageState();
21+
}
22+
23+
class _ChannelListPageState extends State<ChannelListPage> with PerAccountStoreAwareStateMixin<ChannelListPage> {
24+
@override
25+
void onNewStore() => setState(() {
26+
// Just rebuild whenever an update occur.
27+
});
28+
29+
@override
30+
Widget build(BuildContext context) {
31+
final store = PerAccountStoreWidget.of(context);
32+
final zulipLocalizations = ZulipLocalizations.of(context);
33+
final streams = store.streams.values.toList();
34+
return Scaffold(
35+
appBar: AppBar(title: Text(zulipLocalizations.channelListPageTitle)),
36+
body: SafeArea(
37+
child: ListView.builder(
38+
itemCount: streams.length,
39+
itemBuilder: (context, index) => ChannelItem(stream: streams[index]))));
40+
}
41+
}
42+
43+
@visibleForTesting
44+
class ChannelItem extends StatelessWidget {
45+
const ChannelItem({super.key, required this.stream});
46+
47+
final ZulipStream stream;
48+
49+
@override
50+
Widget build(BuildContext context) {
51+
return Material(
52+
color: Colors.white,
53+
child: InkWell(
54+
onTap: () => Navigator.push(context, MessageListPage.buildRoute(context: context,
55+
narrow: ChannelNarrow(stream.streamId))),
56+
child: Padding(
57+
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
58+
child: Row(children: [
59+
Icon(size: 16, iconDataForStream(stream)),
60+
const SizedBox(width: 8),
61+
Expanded(child: Column(
62+
crossAxisAlignment: CrossAxisAlignment.start,
63+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
64+
children: [
65+
Text(stream.name,
66+
style: const TextStyle(
67+
fontSize: 18,
68+
height: (20 / 18),
69+
// TODO(#95) need dark-theme color
70+
color: Color(0xFF262626)),
71+
maxLines: 1,
72+
overflow: TextOverflow.ellipsis),
73+
if (stream.description.isNotEmpty) Text(
74+
stream.description,
75+
style: const TextStyle(
76+
fontSize: 12,
77+
// TODO(#95) need dark-theme color
78+
color: Color(0xCC262626)),
79+
maxLines: 1,
80+
overflow: TextOverflow.ellipsis),
81+
])),
82+
]))));
83+
}
84+
}

lib/widgets/subscription_list.dart

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
23

34
import '../api/model/model.dart';
45
import '../model/narrow.dart';
@@ -7,6 +8,7 @@ import 'icons.dart';
78
import 'message_list.dart';
89
import 'page.dart';
910
import 'store.dart';
11+
import 'channel_list.dart';
1012
import 'text.dart';
1113
import 'theme.dart';
1214
import 'unread_count_badge.dart';
@@ -106,7 +108,7 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> with PerAcc
106108
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: unpinned),
107109
],
108110

109-
// TODO(#188): add button leading to "All Streams" page with ability to subscribe
111+
const _ChannelListLinkItem(),
110112

111113
// This ensures last item in scrollable can settle in an unobstructed area.
112114
const SliverSafeArea(sliver: SliverToBoxAdapter(child: SizedBox.shrink())),
@@ -194,6 +196,40 @@ class _SubscriptionList extends StatelessWidget {
194196
}
195197
}
196198

199+
class _ChannelListLinkItem extends StatelessWidget {
200+
const _ChannelListLinkItem();
201+
202+
@override
203+
Widget build(BuildContext context) {
204+
final store = PerAccountStoreWidget.of(context);
205+
final notShownStreams = store.streams.length - store.subscriptions.length;
206+
final zulipLocalizations = ZulipLocalizations.of(context);
207+
return SliverToBoxAdapter(
208+
child: Material(
209+
// TODO(#95) need dark-theme color
210+
color: Colors.white,
211+
child: InkWell(
212+
onTap: () => Navigator.push(context,
213+
ChannelListPage.buildRoute(context: context)),
214+
child: Padding(
215+
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
216+
child: Row(
217+
crossAxisAlignment: CrossAxisAlignment.center,
218+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
219+
children: [
220+
Text(
221+
style: const TextStyle(
222+
fontSize: 18,
223+
height: (20 / 18),
224+
// TODO(#95) need dark-theme color
225+
color: Color(0xFF262626),
226+
).merge(weightVariableTextStyle(context, wght: 600)),
227+
zulipLocalizations.browseMoreNChannels(notShownStreams)),
228+
const Icon(Icons.arrow_forward_ios, size: 18),
229+
])))));
230+
}
231+
}
232+
197233
@visibleForTesting
198234
class SubscriptionItem extends StatelessWidget {
199235
const SubscriptionItem({

test/widgets/channel_list_test.dart

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:zulip/api/model/model.dart';
4+
import 'package:zulip/widgets/channel_list.dart';
5+
6+
import '../model/binding.dart';
7+
import '../example_data.dart' as eg;
8+
import 'test_app.dart';
9+
10+
void main() {
11+
TestZulipBinding.ensureInitialized();
12+
13+
Future<void> setupChannelListPage(WidgetTester tester, {
14+
required List<ZulipStream> streams, required List<Subscription> subscriptions}) async {
15+
addTearDown(testBinding.reset);
16+
final initialSnapshot = eg.initialSnapshot(
17+
subscriptions: subscriptions,
18+
streams: streams.toList(),
19+
);
20+
await testBinding.globalStore.add(eg.selfAccount, initialSnapshot);
21+
22+
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id, child: const ChannelListPage()));
23+
24+
// global store, per-account store
25+
await tester.pumpAndSettle();
26+
}
27+
28+
int getItemCount() {
29+
return find.byType(ChannelItem).evaluate().length;
30+
}
31+
32+
testWidgets('smoke', (tester) async {
33+
await setupChannelListPage(tester, streams: [], subscriptions: []);
34+
check(getItemCount()).equals(0);
35+
});
36+
37+
testWidgets('basic list', (tester) async {
38+
final streams = List.generate(3, (index) => eg.stream());
39+
await setupChannelListPage(tester, streams: streams, subscriptions: []);
40+
check(getItemCount()).equals(3);
41+
});
42+
}

test/widgets/subscription_list_test.dart

+13-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_test/flutter_test.dart';
44
import 'package:zulip/api/model/initial_snapshot.dart';
55
import 'package:zulip/api/model/model.dart';
6+
import 'package:zulip/model/localizations.dart';
67
import 'package:zulip/widgets/icons.dart';
78
import 'package:zulip/widgets/stream_colors.dart';
89
import 'package:zulip/widgets/subscription_list.dart';
@@ -20,11 +21,12 @@ void main() {
2021
required List<Subscription> subscriptions,
2122
List<UserTopicItem> userTopics = const [],
2223
UnreadMessagesSnapshot? unreadMsgs,
24+
List<ZulipStream>? streams,
2325
}) async {
2426
addTearDown(testBinding.reset);
2527
final initialSnapshot = eg.initialSnapshot(
2628
subscriptions: subscriptions,
27-
streams: subscriptions,
29+
streams: streams ?? subscriptions,
2830
userTopics: userTopics,
2931
unreadMsgs: unreadMsgs,
3032
);
@@ -56,6 +58,16 @@ void main() {
5658
check(isUnpinnedHeaderInTree()).isFalse();
5759
});
5860

61+
testWidgets('link to other channels is shown', (tester) async {
62+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
63+
final streams = List.generate(5, (index) => eg.stream());
64+
await setupStreamListPage(tester,
65+
streams: streams,
66+
subscriptions: [eg.subscription(streams[1])]);
67+
68+
check(find.text(zulipLocalizations.browseMoreNChannels(4)).evaluate()).isNotEmpty();
69+
});
70+
5971
testWidgets('basic subscriptions', (tester) async {
6072
await setupStreamListPage(tester, subscriptions: [
6173
eg.subscription(eg.stream(streamId: 1), pinToTop: true),

0 commit comments

Comments
 (0)