Skip to content

Commit ea790b7

Browse files
committed
channel_list: Setup "All Channels" page
Fixes parts of zulip#188
1 parent 99865f4 commit ea790b7

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';
@@ -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+
if (store.streams.isNotEmpty) const _ChannelListLinkItem(),
110112

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

200+
class _ChannelListLinkItem extends StatelessWidget {
201+
const _ChannelListLinkItem();
202+
203+
@override
204+
Widget build(BuildContext context) {
205+
final store = PerAccountStoreWidget.of(context);
206+
final notShownStreams = store.streams.length - store.subscriptions.length;
207+
final zulipLocalizations = ZulipLocalizations.of(context);
208+
209+
return SliverToBoxAdapter(
210+
child: Material(
211+
// TODO(#95) need dark-theme color
212+
color: Colors.white,
213+
child: InkWell(
214+
onTap: () => Navigator.push(context,
215+
ChannelListPage.buildRoute(context: context)),
216+
child: Padding(
217+
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
218+
child: Row(
219+
crossAxisAlignment: CrossAxisAlignment.center,
220+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
221+
children: [
222+
Text(
223+
style: const TextStyle(
224+
fontSize: 18,
225+
height: (20 / 18),
226+
// TODO(#95) need dark-theme color
227+
color: Color(0xFF262626),
228+
).merge(weightVariableTextStyle(context, wght: 600)),
229+
zulipLocalizations.browseMoreNChannels(notShownStreams)),
230+
const Icon(Icons.arrow_forward_ios, size: 18),
231+
])))));
232+
}
233+
}
234+
198235
@visibleForTesting
199236
class SubscriptionItem extends StatelessWidget {
200237
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';
@@ -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,23 @@ 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+
71+
testWidgets('link to other channels is not shown if server has no visible channels', (tester) async {
72+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
73+
await setupStreamListPage(tester, streams: [], subscriptions: []);
74+
75+
check(find.text(zulipLocalizations.browseMoreNChannels(0)).evaluate()).isEmpty();
76+
});
77+
5978
testWidgets('basic subscriptions', (tester) async {
6079
await setupStreamListPage(tester, subscriptions: [
6180
eg.subscription(eg.stream(streamId: 1), pinToTop: true),

0 commit comments

Comments
 (0)