Skip to content

Commit 71199d6

Browse files
committed
channel_list: Setup "All channels" page
Fixes parts of zulip#188
1 parent a46a175 commit 71199d6

File tree

5 files changed

+185
-2
lines changed

5 files changed

+185
-2
lines changed

Diff for: assets/l10n/app_en.arb

+11
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@
363363
"num": {"type": "int", "example": "4"}
364364
}
365365
},
366+
"browseMoreNChannels": "Browse {num, plural, =0{all channels} =1{1 more channel} other{{num} more channels}}",
367+
"@browseMoreNChannels": {
368+
"description": "Label showing the number of other channels that user can subscribe to",
369+
"placeholders": {
370+
"num": {"type": "int", "example": "4"}
371+
}
372+
},
366373
"errorInvalidResponse": "The server sent an invalid response",
367374
"@errorInvalidResponse": {
368375
"description": "Error message when an API call returned an invalid response."
@@ -480,6 +487,10 @@
480487
"@mentionsPageTitle": {
481488
"description": "Title for the page of @-mentions."
482489
},
490+
"channelListPageTitle": "All channels",
491+
"@channelListPageTitle": {
492+
"description": "Title for the page of all channels."
493+
},
483494
"notifGroupDmConversationLabel": "{senderFullName} to you and {numOthers, plural, =1{1 other} other{{numOthers} others}}",
484495
"@notifGroupDmConversationLabel": {
485496
"description": "Label for a group DM conversation notification.",

Diff for: lib/widgets/channel_list.dart

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 StatelessWidget {
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+
Widget build(BuildContext context) {
21+
final store = PerAccountStoreWidget.of(context);
22+
final zulipLocalizations = ZulipLocalizations.of(context);
23+
final streams = store.streams.values.toList();
24+
return Scaffold(
25+
appBar: AppBar(title: Text(zulipLocalizations.channelListPageTitle)),
26+
body: SafeArea(
27+
child: ListView.builder(
28+
itemCount: streams.length,
29+
itemBuilder: (context, index) => ChannelItem(stream: streams[index]))));
30+
}
31+
}
32+
33+
@visibleForTesting
34+
class ChannelItem extends StatelessWidget {
35+
const ChannelItem({super.key, required this.stream});
36+
37+
final ZulipStream stream;
38+
39+
@override
40+
Widget build(BuildContext context) {
41+
return Material(
42+
color: Colors.white,
43+
child: InkWell(
44+
onTap: () => Navigator.push(context, MessageListPage.buildRoute(context: context,
45+
narrow: ChannelNarrow(stream.streamId))),
46+
child: Padding(
47+
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
48+
child: Row(children: [
49+
Icon(size: 16, iconDataForStream(stream)),
50+
const SizedBox(width: 8),
51+
Expanded(child: Column(
52+
crossAxisAlignment: CrossAxisAlignment.start,
53+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
54+
children: [
55+
Text(stream.name,
56+
style: const TextStyle(
57+
fontSize: 18,
58+
height: (20 / 18),
59+
// TODO(#95) need dark-theme color
60+
color: Color(0xFF262626)),
61+
maxLines: 1,
62+
overflow: TextOverflow.ellipsis),
63+
if (stream.description.isNotEmpty) Text(
64+
stream.description,
65+
style: const TextStyle(
66+
fontSize: 12,
67+
// TODO(#95) need dark-theme color
68+
color: Color(0xCC262626)),
69+
maxLines: 1,
70+
overflow: TextOverflow.ellipsis),
71+
])),
72+
]))));
73+
}
74+
}

Diff for: lib/widgets/subscription_list.dart

+38-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';
@@ -8,6 +9,7 @@ import 'icons.dart';
89
import 'message_list.dart';
910
import 'page.dart';
1011
import 'store.dart';
12+
import 'channel_list.dart';
1113
import 'text.dart';
1214
import 'theme.dart';
1315
import 'unread_count_badge.dart';
@@ -107,7 +109,7 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> with PerAcc
107109
_SubscriptionList(unreadsModel: unreadsModel, subscriptions: unpinned),
108110
],
109111

110-
// TODO(#188): add button leading to "All Streams" page with ability to subscribe
112+
if (store.streams.isNotEmpty) const _ChannelListLinkItem(),
111113

112114
// This ensures last item in scrollable can settle in an unobstructed area.
113115
const SliverSafeArea(sliver: SliverToBoxAdapter(child: SizedBox.shrink())),
@@ -199,6 +201,41 @@ class _SubscriptionList extends StatelessWidget {
199201
}
200202
}
201203

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

Diff for: 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+
}

Diff for: test/widgets/subscription_list_test.dart

+20-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/channel_colors.dart';
89
import 'package:zulip/widgets/subscription_list.dart';
@@ -21,11 +22,12 @@ void main() {
2122
required List<Subscription> subscriptions,
2223
List<UserTopicItem> userTopics = const [],
2324
UnreadMessagesSnapshot? unreadMsgs,
25+
List<ZulipStream>? streams,
2426
}) async {
2527
addTearDown(testBinding.reset);
2628
final initialSnapshot = eg.initialSnapshot(
2729
subscriptions: subscriptions,
28-
streams: subscriptions,
30+
streams: streams ?? subscriptions,
2931
userTopics: userTopics,
3032
unreadMsgs: unreadMsgs,
3133
);
@@ -57,6 +59,23 @@ void main() {
5759
check(isUnpinnedHeaderInTree()).isFalse();
5860
});
5961

62+
testWidgets('link to other channels is shown', (tester) async {
63+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
64+
final streams = List.generate(5, (index) => eg.stream());
65+
await setupStreamListPage(tester,
66+
streams: streams,
67+
subscriptions: [eg.subscription(streams[1])]);
68+
69+
check(find.text(zulipLocalizations.browseMoreNChannels(4)).evaluate()).isNotEmpty();
70+
});
71+
72+
testWidgets('link to other channels is not shown if server has no visible channels', (tester) async {
73+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
74+
await setupStreamListPage(tester, streams: [], subscriptions: []);
75+
76+
check(find.text(zulipLocalizations.browseMoreNChannels(0)).evaluate()).isEmpty();
77+
});
78+
6079
testWidgets('basic subscriptions', (tester) async {
6180
await setupStreamListPage(tester, subscriptions: [
6281
eg.subscription(eg.stream(streamId: 1), pinToTop: true),

0 commit comments

Comments
 (0)