Repository: zulip/zulip-flutter (language: dart)

Issue:
Check group-based permission to subscribe to a channel, as needed
We should check the relevant permission:

- ~~before deciding to show a "Subscribe"  button in the channel action sheet (#1224)~~ (this was done in #1822)
- to give a specific warning "you won't have permission to resubscribe" in the confirmation dialog when unsubscribing from a channel (#1224)

Help-center doc: https://zulip.com/help/configure-who-can-subscribe

### Prerequisites

- #814 


Discussion hints:
nan

Pull request: action_sheet: Check can_subscribe_group for "Subscribe" confirmation dialog
Fixes #1786.

(Also fill in some test coverage for the change in cf83c9ec6 / #1822.)

Test command: flutter test test/widgets/action_sheet_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index 73bcb373b0..89160f5bad 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -143,9 +143,9 @@
       "channelName": {"type": "String", "example": "mobile"}
     }
   },
-  "unsubscribeConfirmationDialogMessageMaybeCannotResubscribe": "Once you leave this channel, you might not be able to rejoin.",
-  "@unsubscribeConfirmationDialogMessageMaybeCannotResubscribe": {
-    "description": "Message for a confirmation dialog for unsubscribing from a channel when you might not have permission to resubscribe."
+  "unsubscribeConfirmationDialogMessageCannotResubscribe": "Once you leave this channel, you will not be able to rejoin.",
+  "@unsubscribeConfirmationDialogMessageCannotResubscribe": {
+    "description": "Message for a confirmation dialog for unsubscribing from a channel when you will not have permission to resubscribe."
   },
   "unsubscribeConfirmationDialogConfirmButton": "Unsubscribe",
   "@unsubscribeConfirmationDialogConfirmButton": {
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index 44fadaa2ec..a3bf9c2cc7 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -341,11 +341,11 @@ abstract class ZulipLocalizations {
   /// **'Unsubscribe from {channelName}?'**
   String unsubscribeConfirmationDialogTitle(String channelName);
 
-  /// Message for a confirmation dialog for unsubscribing from a channel when you might not have permission to resubscribe.
+  /// Message for a confirmation dialog for unsubscribing from a channel when you will not have permission to resubscribe.
   ///
   /// In en, this message translates to:
-  /// **'Once you leave this channel, you might not be able to rejoin.'**
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe;
+  /// **'Once you leave this channel, you will not be able to rejoin.'**
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe;
 
   /// Label for the 'Unsubscribe' button on a confirmation dialog for unsubscribing from a channel.
   ///
diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart
index 6c9ef2fcc5..9c67f11fc6 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -124,8 +124,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Once you leave this channel, you might not be able to rejoin.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Unsubscribe';
diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart
index 9768e579e5..d693740f43 100644
--- a/lib/generated/l10n/zulip_localizations_de.dart
+++ b/lib/generated/l10n/zulip_localizations_de.dart
@@ -126,8 +126,8 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Wenn du diesen Kanal verlässt, kannst du sich vielleicht nicht wieder beitreten.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Deabonnieren';
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index 81ceadcb9c..18e76a9331 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -124,8 +124,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Once you leave this channel, you might not be able to rejoin.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Unsubscribe';
diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart
index 94c4848a9f..2268056038 100644
--- a/lib/generated/l10n/zulip_localizations_fr.dart
+++ b/lib/generated/l10n/zulip_localizations_fr.dart
@@ -126,8 +126,8 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Once you leave this channel, you might not be able to rejoin.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Se désinscrire';
diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart
index a7b2cdd663..cccd1dc8b1 100644
--- a/lib/generated/l10n/zulip_localizations_it.dart
+++ b/lib/generated/l10n/zulip_localizations_it.dart
@@ -125,8 +125,8 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Once you leave this channel, you might not be able to rejoin.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Unsubscribe';
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index 09a9b822b4..5f6fa404d4 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -123,8 +123,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'このチャンネルを退出すると、再び参加できない可能性があります。';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'チャンネルから退出';
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index 8ee2e9c4da..8653de5c7b 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -124,8 +124,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Once you leave this channel, you might not be able to rejoin.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Unsubscribe';
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index fcc527a284..98d6b1de53 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -126,8 +126,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Po opuszczeniu kanału możesz utracić możliwość powrotu.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Odsubskrybuj';
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index fedbacf98f..7effd9fa73 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -126,8 +126,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Покинув этот канал, возможно, вы не сможете присоединиться вновь.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Отписаться';
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 4201d37be2..14310986f6 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -124,8 +124,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Once you leave this channel, you might not be able to rejoin.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Unsubscribe';
diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart
index ac70ca61da..8bfc8810cb 100644
--- a/lib/generated/l10n/zulip_localizations_sl.dart
+++ b/lib/generated/l10n/zulip_localizations_sl.dart
@@ -124,8 +124,8 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Ko zapustite ta kanal, se morda ne boste mogli znova pridružiti.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Prekliči naročnino';
diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart
index ddfb79ac57..f5b955fb55 100644
--- a/lib/generated/l10n/zulip_localizations_uk.dart
+++ b/lib/generated/l10n/zulip_localizations_uk.dart
@@ -127,8 +127,8 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Після того, як ви залишите цей канал, ви, можливо, не зможете приєднатися знову.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Скасувати підписку';
diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart
index f974ce7573..e3f483e560 100644
--- a/lib/generated/l10n/zulip_localizations_zh.dart
+++ b/lib/generated/l10n/zulip_localizations_zh.dart
@@ -124,8 +124,8 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
   }
 
   @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      'Once you leave this channel, you might not be able to rejoin.';
+  String get unsubscribeConfirmationDialogMessageCannotResubscribe =>
+      'Once you leave this channel, you will not be able to rejoin.';
 
   @override
   String get unsubscribeConfirmationDialogConfirmButton => 'Unsubscribe';
@@ -1254,10 +1254,6 @@ class ZulipLocalizationsZhHansCn extends ZulipLocalizationsZh {
     return '确定取消订阅$channelName么?';
   }
 
-  @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      '一旦退出该频道，您可能无法重新加入。';
-
   @override
   String get unsubscribeConfirmationDialogConfirmButton => '取消订阅';
 
@@ -2342,10 +2338,6 @@ class ZulipLocalizationsZhHantTw extends ZulipLocalizationsZh {
     return '確定要取消訂閱 $channelName 嗎？';
   }
 
-  @override
-  String get unsubscribeConfirmationDialogMessageMaybeCannotResubscribe =>
-      '一旦您離開此頻道，可能無法重新加入。';
-
   @override
   String get unsubscribeConfirmationDialogConfirmButton => '取消訂閱';
 
diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart
index 8191f58069..5d233a527f 100644
--- a/lib/widgets/action_sheet.dart
+++ b/lib/widgets/action_sheet.dart
@@ -17,6 +17,7 @@ import '../model/content.dart';
 import '../model/emoji.dart';
 import '../model/internal_link.dart';
 import '../model/narrow.dart';
+import '../model/realm.dart';
 import 'actions.dart';
 import 'button.dart';
 import 'color.dart';
@@ -633,20 +634,21 @@ class UnsubscribeButton extends ActionSheetMenuItemButton {
 
   @override
   void onPressed() async {
-    final subscription = PerAccountStoreWidget.of(pageContext).subscriptions[channelId];
+    final store = PerAccountStoreWidget.of(pageContext);
+    final subscription = store.subscriptions[channelId];
     if (subscription == null) return; // TODO could give feedback
 
-    // TODO(#1786) check group-based permission to subscribe, then replace
-    //   error message with a new one saying "will not" instead of "might not"
     // TODO(future) check if the self-user is a guest and the channel is not web-public
-    final couldResubscribe = !subscription.inviteOnly;
+    final couldResubscribe = !subscription.inviteOnly
+      || store.selfHasPermissionForGroupSetting(subscription.canSubscribeGroup,
+           GroupSettingType.stream, 'can_subscribe_group');
     if (!couldResubscribe) {
       // TODO(#1788) warn if org would lose content access (nobody can subscribe)
       final zulipLocalizations = ZulipLocalizations.of(pageContext);
 
       final dialog = showSuggestedActionDialog(context: pageContext,
         title: zulipLocalizations.unsubscribeConfirmationDialogTitle(subscription.name),
-        message: zulipLocalizations.unsubscribeConfirmationDialogMessageMaybeCannotResubscribe,
+        message: zulipLocalizations.unsubscribeConfirmationDialogMessageCannotResubscribe,
         destructiveActionButton: true,
         actionButtonText: zulipLocalizations.unsubscribeConfirmationDialogConfirmButton);
       if (await dialog.result != true) return;


Test patch:
diff --git a/test/model/test_store.dart b/test/model/test_store.dart
index e706f3139f..faf62412e4 100644
--- a/test/model/test_store.dart
+++ b/test/model/test_store.dart
@@ -319,6 +319,19 @@ extension PerAccountStoreTestExtension on PerAccountStore {
     await handleEvent(ChannelCreateEvent(id: 1, streams: streams));
   }
 
+  Future<void> updateChannel(
+    int channelId,
+    ChannelPropertyName property,
+    Object? value,
+  ) async {
+    await handleEvent(ChannelUpdateEvent(
+      id: 1,
+      streamId: channelId,
+      name: 'some channel name', // (not true, of course, but that's fine)
+      property: property,
+      value: value));
+  }
+
   Future<void> addSubscription(Subscription subscription) async {
     await addSubscriptions([subscription]);
   }
diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart
index 9866b9dc10..e03e102cf5 100644
--- a/test/widgets/action_sheet_test.dart
+++ b/test/widgets/action_sheet_test.dart
@@ -341,14 +341,30 @@ void main() {
         await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
       }
 
-      testWidgets('channel not subscribed', (tester) async {
+      testWidgets('channel not subscribed, with content access', (tester) async {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
         await store.removeSubscription(narrow.streamId);
+        check(store.selfHasContentAccess(someChannel)).isTrue();
         await showFromMsglistAppBar(tester, narrow: narrow);
         checkButton('Subscribe');
       });
 
+      testWidgets('channel not subscribed, without content access', (tester) async {
+        final privateChannel = eg.stream(inviteOnly: true);
+        await prepare();
+        await store.addStream(privateChannel);
+        await store.updateChannel(privateChannel.streamId,
+          ChannelPropertyName.canSubscribeGroup, eg.groupSetting(members: []));
+        await store.updateChannel(privateChannel.streamId,
+          ChannelPropertyName.canAddSubscribersGroup, eg.groupSetting(members: []));
+        final narrow = ChannelNarrow(privateChannel.streamId);
+        check(store.selfHasContentAccess(privateChannel)).isFalse();
+        await showFromMsglistAppBar(tester,
+          channel: privateChannel, narrow: narrow);
+        checkNoButton('Subscribe');
+      });
+
       testWidgets('channel subscribed', (tester) async {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
@@ -584,7 +600,7 @@ void main() {
 
         final (unsubscribeButton, cancelButton) = checkSuggestedActionDialog(tester,
           expectedTitle: 'Unsubscribe from ${channel.name}?',
-          expectedMessage: 'Once you leave this channel, you might not be able to rejoin.',
+          expectedMessage: 'Once you leave this channel, you will not be able to rejoin.',
           expectDestructiveActionButton: true,
           expectedActionButtonText: 'Unsubscribe');
         await tester.tap(find.byWidget(unsubscribeButton));