Skip to content

Commit efd791d

Browse files
committed
channel_list: Implement unsubscribe from channel
1 parent 1f30bba commit efd791d

File tree

3 files changed

+113
-7
lines changed

3 files changed

+113
-7
lines changed

assets/l10n/app_en.arb

+8
Original file line numberDiff line numberDiff line change
@@ -624,5 +624,13 @@
624624
"errorFailedToSubscribedToChannel": "Failed to subscribe to ",
625625
"@errorFailedToSubscribedToChannel": {
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+
"errorFailedToUnsubscribedFromChannel": "Failed to unsubscribe to ",
633+
"@errorFailedToUnsubscribedFromChannel": {
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
@@ -105,22 +105,22 @@ class ChannelItem extends StatelessWidget {
105105
overflow: TextOverflow.ellipsis),
106106
])),
107107
const SizedBox(width: 8),
108-
if (stream is! Subscription) _ChannelItemSubscribeButton(stream: stream, channelItemContext: context),
108+
_ChannelItemSubscriptionToggle(stream: stream, channelItemContext: context),
109109
]))));
110110
}
111111
}
112112

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

116116
final ZulipStream stream;
117117
final BuildContext channelItemContext;
118118

119119
@override
120-
State<_ChannelItemSubscribeButton> createState() => _ChannelItemSubscribeButtonState();
120+
State<_ChannelItemSubscriptionToggle> createState() => _ChannelItemSubscriptionToggleState();
121121
}
122122

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

126126
void _setIsLoading(bool value) {
@@ -130,15 +130,39 @@ class _ChannelItemSubscribeButtonState extends State<_ChannelItemSubscribeButton
130130

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

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

test/widgets/channel_list_test.dart

+74
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,20 @@ void main() {
9393
return stream;
9494
}
9595

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

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

121131
check(find.byIcon(Icons.add).evaluate()).isNotEmpty();
132+
check(find.byIcon(Icons.check).evaluate()).isEmpty();
122133

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

127138
check(find.byIcon(Icons.add).evaluate()).isEmpty();
139+
check(find.byIcon(Icons.check).evaluate()).isNotEmpty();
128140

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

132144
check(find.byIcon(Icons.add).evaluate()).isNotEmpty();
145+
check(find.byIcon(Icons.check).evaluate()).isEmpty();
133146
});
134147

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

0 commit comments

Comments
 (0)