Repository: zulip/zulip

Issue:
Changing a stream from private to public requires a client reload to view
When a stream is changed from private to public, users who were not invited to the private stream need to reload the client in order to view the now-public stream.

If the user does not refresh the client, they get a `This stream does not exist or is private.` error when trying to access the channel via hashtag reference (`#name-of-channel`) in chat. If they attempt to navigate to it manually, it shows as `deactivated`.


Discussion:
Hello @zulip/server-streams members, this issue was labeled with the "area: stream settings" label, so you may want to check it out!

<!-- areaLabelAddition -->

Thanks for the report @agbaber !

CZO discussion thread: https://chat.zulip.org/#narrow/stream/9-issues/topic/live.20update.20for.20stream.20access.20issue.20.20.2322194
@zulipbot claim
Hello @sahil839!

Thanks for your interest in Zulip! You have attempted to claim an issue without the label "help wanted". You can only claim and submit pull requests for issues with the [help wanted](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22help+wanted%22) label.

If this is your first time here, we recommend reading our [guide for new contributors](https://zulip.readthedocs.io/en/latest/overview/contributing.html) before getting started.


Resolving patch:
diff --git a/zerver/actions/streams.py b/zerver/actions/streams.py
--- a/zerver/actions/streams.py
+++ b/zerver/actions/streams.py
@@ -944,6 +944,43 @@ def do_change_stream_permission(
                 ).decode(),
             )
 
+    notify_stream_creation_ids = set()
+    if old_invite_only_value and not stream.invite_only:
+        # We need to send stream creation event to users who can access the
+        # stream now but were not able to do so previously. So, we can exclude
+        # subscribers, users who were previously subscribed to the stream and
+        # realm admins from the non-guest user list.
+        previously_subscribed_user_ids = Subscription.objects.filter(
+            recipient_id=stream.recipient_id, active=False, is_user_active=True
+        ).values_list("user_profile_id", flat=True)
+        stream_subscriber_user_ids = get_active_subscriptions_for_stream_id(
+            stream.id, include_deactivated_users=False
+        ).values_list("user_profile_id", flat=True)
+
+        old_can_access_stream_user_ids = (
+            set(stream_subscriber_user_ids)
+            | set(previously_subscribed_user_ids)
+            | {user.id for user in stream.realm.get_admin_users_and_bots()}
+        )
+        non_guest_user_ids = set(active_non_guest_user_ids(stream.realm_id))
+        notify_stream_creation_ids = non_guest_user_ids - old_can_access_stream_user_ids
+        send_stream_creation_event(stream, list(notify_stream_creation_ids))
+
+        # Add subscribers info to the stream object. We need to send peer_add
+        # events to users who were previously subscribed to the streams as
+        # they did not had subscribers data.
+        old_subscribers_access_user_ids = set(stream_subscriber_user_ids) | {
+            user.id for user in stream.realm.get_admin_users_and_bots()
+        }
+        peer_notify_user_ids = non_guest_user_ids - old_subscribers_access_user_ids
+        peer_add_event = dict(
+            type="subscription",
+            op="peer_add",
+            stream_ids=[stream.id],
+            user_ids=sorted(stream_subscriber_user_ids),
+        )
+        send_event(stream.realm, peer_add_event, peer_notify_user_ids)
+
     event = dict(
         op="update",
         type="stream",
@@ -954,7 +991,10 @@ def do_change_stream_permission(
         stream_id=stream.id,
         name=stream.name,
     )
-    send_event(stream.realm, event, can_access_stream_user_ids(stream))
+    # we do not need to send update events to the users who received creation event
+    # since they already have the updated stream info.
+    notify_stream_update_ids = can_access_stream_user_ids(stream) - notify_stream_creation_ids
+    send_event(stream.realm, event, notify_stream_update_ids)
 
     old_policy_name = get_stream_permission_policy_name(
         invite_only=old_invite_only_value,
diff --git a/zerver/lib/events.py b/zerver/lib/events.py
--- a/zerver/lib/events.py
+++ b/zerver/lib/events.py
@@ -923,17 +923,22 @@ def _draft_update_action(i: int) -> None:
         if event["op"] == "update":
             # For legacy reasons, we call stream data 'subscriptions' in
             # the state var here, for the benefit of the JS code.
-            for obj in state["subscriptions"]:
-                if obj["name"].lower() == event["name"].lower():
-                    obj[event["property"]] = event["value"]
-                    if event["property"] == "description":
-                        obj["rendered_description"] = event["rendered_description"]
-                    if event.get("history_public_to_subscribers") is not None:
-                        obj["history_public_to_subscribers"] = event[
-                            "history_public_to_subscribers"
-                        ]
-                    if event.get("is_web_public") is not None:
-                        obj["is_web_public"] = event["is_web_public"]
+            for sub_list in [
+                state["subscriptions"],
+                state["unsubscribed"],
+                state["never_subscribed"],
+            ]:
+                for obj in sub_list:
+                    if obj["name"].lower() == event["name"].lower():
+                        obj[event["property"]] = event["value"]
+                        if event["property"] == "description":
+                            obj["rendered_description"] = event["rendered_description"]
+                        if event.get("history_public_to_subscribers") is not None:
+                            obj["history_public_to_subscribers"] = event[
+                                "history_public_to_subscribers"
+                            ]
+                        if event.get("is_web_public") is not None:
+                            obj["is_web_public"] = event["is_web_public"]
             # Also update the pure streams data
             if "streams" in state:
                 for stream in state["streams"]:


Test patch:
diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py
--- a/zerver/tests/test_events.py
+++ b/zerver/tests/test_events.py
@@ -2744,6 +2744,40 @@ def do_test_subscribe_events(self, include_subscribers: bool) -> None:
         check_stream_update("events[0]", events[0])
         check_message("events[1]", events[1])
 
+        # Update stream privacy - make stream public
+        self.user_profile = self.example_user("cordelia")
+        action = lambda: do_change_stream_permission(
+            stream,
+            invite_only=False,
+            history_public_to_subscribers=True,
+            is_web_public=False,
+            acting_user=self.example_user("hamlet"),
+        )
+        events = self.verify_action(action, include_subscribers=include_subscribers, num_events=2)
+        check_stream_create("events[0]", events[0])
+        check_subscription_peer_add("events[1]", events[1])
+
+        do_change_stream_permission(
+            stream,
+            invite_only=True,
+            history_public_to_subscribers=True,
+            is_web_public=False,
+            acting_user=self.example_user("hamlet"),
+        )
+        self.subscribe(self.example_user("cordelia"), stream.name)
+        self.unsubscribe(self.example_user("cordelia"), stream.name)
+        action = lambda: do_change_stream_permission(
+            stream,
+            invite_only=False,
+            history_public_to_subscribers=True,
+            is_web_public=False,
+            acting_user=self.example_user("hamlet"),
+        )
+        events = self.verify_action(
+            action, include_subscribers=include_subscribers, num_events=2, include_streams=False
+        )
+
+        self.user_profile = self.example_user("hamlet")
         # Update stream stream_post_policy property
         action = lambda: do_change_stream_post_policy(
             stream, Stream.STREAM_POST_POLICY_ADMINS, acting_user=self.example_user("hamlet")