Skip to content

Commit f604530

Browse files
committed
channel_list: Implement unsubscribe from channel
1 parent 793e9bf commit f604530

File tree

3 files changed

+112
-7
lines changed

3 files changed

+112
-7
lines changed

assets/l10n/app_en.arb

+8
Original file line numberDiff line numberDiff line change
@@ -624,5 +624,13 @@
624624
"errorFailedToSubscribeToChannel": "Failed to subscribe to ",
625625
"@errorFailedToSubscribeToChannel": {
626626
"description": "An error message when subscribe action fails"
627+
},
628+
"messageUnsubscribedFromChannel": "You've unsubscribed from ",
629+
"@messageUnsubscribedFromChannel": {
630+
"description": "A message shown to inform user that unsubscribe action passes"
631+
},
632+
"errorFailedToUnsubscribeFromChannel": "Failed to unsubscribe to ",
633+
"@errorFailedToUnsubscribeFromChannel": {
634+
"description": "An error message when unsubscribe action fails"
627635
}
628636
}

lib/widgets/channel_list.dart

+31-7
Original file line numberDiff line numberDiff line change
@@ -104,22 +104,22 @@ class ChannelItem extends StatelessWidget {
104104
overflow: TextOverflow.ellipsis),
105105
])),
106106
const SizedBox(width: 8),
107-
if (stream is! Subscription) _ChannelItemSubscribeButton(stream: stream, channelItemContext: context),
107+
_ChannelItemSubscriptionToggle(stream: stream, channelItemContext: context),
108108
]))));
109109
}
110110
}
111111

112-
class _ChannelItemSubscribeButton extends StatefulWidget {
113-
const _ChannelItemSubscribeButton({required this.stream, required this.channelItemContext});
112+
class _ChannelItemSubscriptionToggle extends StatefulWidget {
113+
const _ChannelItemSubscriptionToggle({required this.stream, required this.channelItemContext});
114114

115115
final ZulipStream stream;
116116
final BuildContext channelItemContext;
117117

118118
@override
119-
State<_ChannelItemSubscribeButton> createState() => _ChannelItemSubscribeButtonState();
119+
State<_ChannelItemSubscriptionToggle> createState() => _ChannelItemSubscriptionToggleState();
120120
}
121121

122-
class _ChannelItemSubscribeButtonState extends State<_ChannelItemSubscribeButton> {
122+
class _ChannelItemSubscriptionToggleState extends State<_ChannelItemSubscriptionToggle> {
123123
bool _isLoading = false;
124124

125125
void _setIsLoading(bool value) {
@@ -129,15 +129,39 @@ class _ChannelItemSubscribeButtonState extends State<_ChannelItemSubscribeButton
129129

130130
@override
131131
Widget build(BuildContext context) {
132+
final colorScheme = Theme.of(context).colorScheme;
133+
final (icon, color, onPressed) = widget.stream is Subscription
134+
? (Icons.check, colorScheme.primary, _unsubscribeFromChannel)
135+
: (Icons.add, null, _subscribeToChannel);
136+
132137
return IconButton(
133-
icon: const Icon(Icons.add),
138+
color: color,
139+
icon: Icon(icon),
134140
onPressed: _isLoading ? null : () async {
135141
_setIsLoading(true);
136-
await _subscribeToChannel(context, widget.stream);
142+
await onPressed(context, widget.stream);
137143
_setIsLoading(false);
138144
});
139145
}
140146

147+
Future<void> _unsubscribeFromChannel(BuildContext context, ZulipStream stream) async {
148+
final store = PerAccountStoreWidget.of(context);
149+
final connection = store.connection;
150+
final zulipLocalizations = ZulipLocalizations.of(context);
151+
try {
152+
final res = await unsubscribeFromChannels(connection, [stream]);
153+
if (!context.mounted) return;
154+
if (res.removed.contains(stream.name)) {
155+
_showSnackbarWithChannelTitle(context, zulipLocalizations.messageUnsubscribedFromChannel, stream);
156+
} else if (res.notRemoved.contains(stream.name)) {
157+
_showSnackbarWithChannelTitle(context, zulipLocalizations.errorFailedToUnsubscribeFromChannel, stream);
158+
}
159+
} catch (e) {
160+
if (!context.mounted) return;
161+
_showSnackbarWithChannelTitle(context, zulipLocalizations.errorFailedToUnsubscribeFromChannel, stream);
162+
}
163+
}
164+
141165
Future<void> _subscribeToChannel(BuildContext context, ZulipStream stream) async {
142166
final store = PerAccountStoreWidget.of(context);
143167
final connection = store.connection;

test/widgets/channel_list_test.dart

+73
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,20 @@ void main() {
9292
return stream;
9393
}
9494

95+
Future<Subscription> prepareSingleSubscription(WidgetTester tester) async {
96+
final stream = eg.subscription(eg.stream());
97+
await setupChannelListPage(tester, streams: [stream], subscriptions: [stream]);
98+
return stream;
99+
}
100+
95101
Future<void> tapSubscribeButton(WidgetTester tester) async {
96102
await tester.tap(find.byIcon(Icons.add));
97103
}
98104

105+
Future<void> tapUnsubscribeButton(WidgetTester tester) async {
106+
await tester.tap(find.byIcon(Icons.check));
107+
}
108+
99109
Future<void> waitAndCheckSnackbarIsShown(WidgetTester tester, String message, String channelName) async {
100110
await tester.pump(Duration.zero);
101111
await tester.pumpAndSettle();
@@ -118,17 +128,20 @@ void main() {
118128
alreadySubscribed: {}).toJson());
119129

120130
check(find.byIcon(Icons.add).evaluate()).isNotEmpty();
131+
check(find.byIcon(Icons.check).evaluate()).isEmpty();
121132

122133
await store.handleEvent(SubscriptionAddEvent(id: 1,
123134
subscriptions: [eg.subscription(stream)]));
124135
await tester.pumpAndSettle();
125136

126137
check(find.byIcon(Icons.add).evaluate()).isEmpty();
138+
check(find.byIcon(Icons.check).evaluate()).isNotEmpty();
127139

128140
await store.handleEvent(SubscriptionRemoveEvent(id: 2, streamIds: [stream.streamId]));
129141
await tester.pumpAndSettle();
130142

131143
check(find.byIcon(Icons.add).evaluate()).isNotEmpty();
144+
check(find.byIcon(Icons.check).evaluate()).isEmpty();
132145
});
133146

134147
testWidgets('is disabled while loading', (WidgetTester tester) async {
@@ -236,5 +249,65 @@ void main() {
236249
zulipLocalizations.errorFailedToSubscribeToChannel, stream.name);
237250
});
238251
});
252+
253+
group('unsubscribe', () {
254+
testWidgets('is shown only for streams that user is subscribed to', (tester) async {
255+
final streams = [eg.stream(), eg.stream(), eg.subscription(eg.stream())];
256+
final subscriptions = [streams[2] as Subscription];
257+
await setupChannelListPage(tester, streams: streams, subscriptions: subscriptions);
258+
259+
check(find.byIcon(Icons.check).evaluate().length).equals(1);
260+
});
261+
262+
testWidgets('smoke api', (tester) async {
263+
final stream = await prepareSingleSubscription(tester);
264+
connection.prepare(json: UnsubscribeFromChannelsResult(
265+
removed: [stream.name],
266+
notRemoved: []).toJson());
267+
await tapUnsubscribeButton(tester);
268+
269+
await tester.pump(Duration.zero);
270+
await tester.pumpAndSettle();
271+
check(connection.lastRequest).isA<http.Request>()
272+
..method.equals('DELETE')
273+
..url.path.equals('/api/v1/users/me/subscriptions')
274+
..bodyFields.deepEquals({
275+
'subscriptions': jsonEncode([stream.name])
276+
});
277+
});
278+
279+
testWidgets('shows a snackbar when subscription passes', (WidgetTester tester) async {
280+
final stream = await prepareSingleSubscription(tester);
281+
connection.prepare(json: UnsubscribeFromChannelsResult(
282+
removed: [stream.name],
283+
notRemoved: []).toJson());
284+
await tapUnsubscribeButton(tester);
285+
286+
await waitAndCheckSnackbarIsShown(tester,
287+
zulipLocalizations.messageUnsubscribedFromChannel, stream.name);
288+
});
289+
290+
testWidgets('shows a snackbar when subscription fails', (WidgetTester tester) async {
291+
final stream = await prepareSingleSubscription(tester);
292+
connection.prepare(json: UnsubscribeFromChannelsResult(
293+
removed: [],
294+
notRemoved: [stream.name]).toJson());
295+
await tapUnsubscribeButton(tester);
296+
297+
await waitAndCheckSnackbarIsShown(tester,
298+
zulipLocalizations.errorFailedToUnsubscribeFromChannel, stream.name);
299+
});
300+
301+
testWidgets('catch-all api errors', (WidgetTester tester) async {
302+
final stream = await prepareSingleSubscription(tester);
303+
connection.prepare(exception: http.ClientException('Oops'));
304+
await tapUnsubscribeButton(tester);
305+
await tester.pump(Duration.zero);
306+
await tester.pumpAndSettle();
307+
308+
await waitAndCheckSnackbarIsShown(tester,
309+
zulipLocalizations.errorFailedToUnsubscribeFromChannel, stream.name);
310+
});
311+
});
239312
});
240313
}

0 commit comments

Comments
 (0)