Skip to content

Commit 726e2e3

Browse files
committed
channel_list: Setup "All Channels" page
Fixes: zulip#188
1 parent b68009c commit 726e2e3

File tree

5 files changed

+197
-2
lines changed

5 files changed

+197
-2
lines changed

assets/l10n/app_en.arb

Lines changed: 11 additions & 0 deletions
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

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

lib/widgets/subscription_list.dart

Lines changed: 37 additions & 1 deletion
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

Lines changed: 42 additions & 0 deletions
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

Lines changed: 13 additions & 1 deletion
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)