Skip to content

channel_list: All channels screen #832

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
46 changes: 46 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,24 @@
"@topicValidationErrorMandatoryButEmpty": {
"description": "Topic validation error when topic is required but was empty."
},
"subscribedToNChannels": "Subscribed to {num, plural, =0{no channels} =1{1 channel} other{{num} channels}}",
"@subscribedToNChannels": {
"description": "Test page label showing number of channels user is subscribed to.",
"placeholders": {
"num": {"type": "int", "example": "4"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have a link to the discussion on the design here, so that we know this UI is based on the legacy mobile app.

}
},
"browseNMoreChannels": "Browse {num, plural, =1{1 more channel} other{{num} more channels}}",
"@browseNMoreChannels": {
"description": "Label showing the number of other channels that user can subscribe to",
"placeholders": {
"num": {"type": "int", "example": "4"}
}
},
"browseAllChannels": "Browse all channels",
"@browseAllChannels": {
"description": "Label for the option to show all channels, this is only shown if user is already subscribed to all visible channels"
},
"errorInvalidResponse": "The server sent an invalid response",
"@errorInvalidResponse": {
"description": "Error message when an API call returned an invalid response."
Expand Down Expand Up @@ -532,6 +550,14 @@
"@starredMessagesPageTitle": {
"description": "Title for the page of starred messages."
},
"channelListPageTitle": "All channels",
"@channelListPageTitle": {
"description": "Title for the page of all channels."
},
"noChannelsFound": "There are no channels you can view in this organization.",
"@noChannelsFound": {
"description": "Message when no channels are found"
},
"notifGroupDmConversationLabel": "{senderFullName} to you and {numOthers, plural, =1{1 other} other{{numOthers} others}}",
"@notifGroupDmConversationLabel": {
"description": "Label for a group DM conversation notification.",
Expand Down Expand Up @@ -586,5 +612,25 @@
"errorNotificationOpenAccountMissing": "The account associated with this notification no longer exists.",
"@errorNotificationOpenAccountMissing": {
"description": "Error message when the account associated with the notification is not found"
},
"messageSubscribedToChannel": "You've just subscribed to ",
"@messageSubscribedToChannel": {
"description": "A message shown to inform user that subscription is successful"
},
"messageAlreadySubscribedToChannel": "You're already subscribed to ",
"@messageAlreadySubscribedToChannel": {
"description": "A message shown to inform user that subscription is already made"
},
"errorFailedToSubscribeToChannel": "Failed to subscribe to ",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use placeholders for these strings.

"@errorFailedToSubscribeToChannel": {
"description": "An error message when subscribe action fails"
},
"messageUnsubscribedFromChannel": "You've unsubscribed from ",
"@messageUnsubscribedFromChannel": {
"description": "A message shown to inform user that unsubscribe action passes"
},
"errorFailedToUnsubscribeFromChannel": "Failed to unsubscribe to ",
"@errorFailedToUnsubscribeFromChannel": {
"description": "An error message when unsubscribe action fails"
}
}
18 changes: 18 additions & 0 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,24 @@ class Subscription extends ZulipStream {

@override
Map<String, dynamic> toJson() => _$SubscriptionToJson(this);

/// Returns a plain `ZulipStream` with same values as `this`
///
/// This is helpful when unsubscribing from a stream.
ZulipStream toStream() => ZulipStream(
streamId: streamId,
name: name,
description: description,
renderedDescription: renderedDescription,
dateCreated: dateCreated,
firstMessageId: firstMessageId,
inviteOnly: inviteOnly,
isWebPublic: isWebPublic,
historyPublicToSubscribers: historyPublicToSubscribers,
messageRetentionDays: messageRetentionDays,
channelPostPolicy: channelPostPolicy,
canRemoveSubscribersGroup: canRemoveSubscribersGroup,
streamWeeklyTraffic: streamWeeklyTraffic);
}

@JsonEnum(fieldRename: FieldRename.snake, valueField: "apiValue")
Expand Down
47 changes: 47 additions & 0 deletions lib/api/route/channels.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart';

import '../core.dart';
import '../model/model.dart';
part 'channels.g.dart';

/// https://zulip.com/api/get-stream-topics
Expand Down Expand Up @@ -38,3 +39,49 @@ class GetStreamTopicsEntry {

Map<String, dynamic> toJson() => _$GetStreamTopicsEntryToJson(this);
}

/// https://zulip.com/api/subscribe
Future<SubscribeToChannelsResult> subscribeToChannels(ApiConnection connection, List<ZulipStream> streams) {
return connection.post('subscribe', SubscribeToChannelsResult.fromJson,
'users/me/subscriptions', {'subscriptions': streams.map((e) => {'name': e.name}).toList()});
}

@JsonSerializable(fieldRename: FieldRename.snake)
class SubscribeToChannelsResult {
final Map<String, List<String>> subscribed;
final Map<String, List<String>> alreadySubscribed;
final List<String>? unauthorized;

SubscribeToChannelsResult({
required this.subscribed,
required this.alreadySubscribed,
this.unauthorized,
});

factory SubscribeToChannelsResult.fromJson(Map<String, dynamic> json) =>
_$SubscribeToChannelsResultFromJson(json);

Map<String, dynamic> toJson() => _$SubscribeToChannelsResultToJson(this);
}

/// https://zulip.com/api/usubscribe
Future<UnsubscribeFromChannelsResult> unsubscribeFromChannels(ApiConnection connection, List<ZulipStream> streams) {
return connection.delete('unsubscribe', UnsubscribeFromChannelsResult.fromJson,
'users/me/subscriptions', {'subscriptions': streams.map((e) => e.name).toList()});
}

@JsonSerializable(fieldRename: FieldRename.snake)
class UnsubscribeFromChannelsResult {
final List<String> removed;
final List<String> notRemoved;

UnsubscribeFromChannelsResult({
required this.removed,
required this.notRemoved,
});

factory UnsubscribeFromChannelsResult.fromJson(Map<String, dynamic> json) =>
_$UnsubscribeFromChannelsResultFromJson(json);

Map<String, dynamic> toJson() => _$UnsubscribeFromChannelsResultToJson(this);
}
42 changes: 42 additions & 0 deletions lib/api/route/channels.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions lib/model/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,15 @@ class ChannelStoreImpl with ChannelStore {

case SubscriptionRemoveEvent():
for (final streamId in event.streamIds) {
final subscription = streams[streamId];
if (subscription == null || subscription is! Subscription) { // TODO(log)
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is only possible when the server behaves unexpectedly. So it might fit better in a non-nfc commit explaining how this can happen.

}
assert(streamsByName[subscription.name] is Subscription);
assert(subscriptions.containsKey(streamId));
final unsubscribedStream = subscription.toStream();
streams[streamId] = unsubscribedStream;
streamsByName[subscription.name] = unsubscribedStream;
subscriptions.remove(streamId);
}

Expand Down
Loading