diff --git a/docs/subsystems/hashchange-system.md b/docs/subsystems/hashchange-system.md
index cd8137a2d8..376054b749 100644
--- a/docs/subsystems/hashchange-system.md
+++ b/docs/subsystems/hashchange-system.md
@@ -8,9 +8,9 @@ be used to deep-link into the application and allow the browser's
Some examples are:
- `/#settings/your-bots`: Bots section of the settings overlay.
-- `/#streams`: Streams overlay, where the user manages streams
+- `/#channels`: Streams overlay, where the user manages streams
(subscription etc.)
-- `/#streams/11/announce`: Streams overlay with stream ID 11 (called
+- `/#channels/11/announce`: Streams overlay with stream ID 11 (called
"announce") selected.
- `/#narrow/stream/42-android/topic/fun`: Message feed showing stream
"android" and topic "fun". (The `42` represents the id of the
@@ -25,7 +25,7 @@ different flows:
- The user clicking on an in-app link, which in turn opens an overlay.
For example the streams overlay opens when the user clicks the small
cog symbol on the left sidebar, which is in fact a link to
- `/#streams`. This makes it easy to have simple links around the app
+ `/#channels`. This makes it easy to have simple links around the app
without custom click handlers for each one.
- The user uses the "back" button in their browser (basically
equivalent to the previous one, as a _link_ out of the browser history
diff --git a/web/e2e-tests/lib/common.ts b/web/e2e-tests/lib/common.ts
index 42a9c93ffa..bf0fc59178 100644
--- a/web/e2e-tests/lib/common.ts
+++ b/web/e2e-tests/lib/common.ts
@@ -544,7 +544,7 @@ export async function open_streams_modal(page: Page): Promise {
await page.waitForSelector("#subscription_overlay.new-style", {visible: true});
const url = await page_url_with_fragment(page);
- assert.ok(url.includes("#streams/all"));
+ assert.ok(url.includes("#channels/all"));
}
export async function open_personal_menu(page: Page): Promise {
diff --git a/web/e2e-tests/message-basics.test.ts b/web/e2e-tests/message-basics.test.ts
index 2e28aca932..674a92c01f 100644
--- a/web/e2e-tests/message-basics.test.ts
+++ b/web/e2e-tests/message-basics.test.ts
@@ -488,7 +488,7 @@ async function test_users_search(page: Page): Promise {
async function test_narrow_public_streams(page: Page): Promise {
const stream_id = await common.get_stream_id(page, "Denmark");
- await page.goto(`http://zulip.zulipdev.com:9981/#streams/${stream_id}/Denmark`);
+ await page.goto(`http://zulip.zulipdev.com:9981/#channels/${stream_id}/Denmark`);
await page.waitForSelector("button.sub_unsub_button", {visible: true});
await page.click("button.sub_unsub_button");
await page.waitForSelector(
diff --git a/web/e2e-tests/navigation.test.ts b/web/e2e-tests/navigation.test.ts
index 3625d8b5df..ae5208d36e 100644
--- a/web/e2e-tests/navigation.test.ts
+++ b/web/e2e-tests/navigation.test.ts
@@ -42,7 +42,7 @@ async function navigate_to_subscriptions(page: Page): Promise {
await open_menu(page);
- const manage_streams_selector = '.link-item a[href^="#streams"]';
+ const manage_streams_selector = '.link-item a[href^="#channels"]';
await page.waitForSelector(manage_streams_selector, {visible: true});
await page.click(manage_streams_selector);
diff --git a/web/src/add_stream_options_popover.ts b/web/src/add_stream_options_popover.ts
index 11f7c4496d..77a223b8ad 100644
--- a/web/src/add_stream_options_popover.ts
+++ b/web/src/add_stream_options_popover.ts
@@ -19,7 +19,7 @@ export function initialize(): void {
if (!can_create_streams) {
// If the user can't create streams, we directly
// navigate them to the Stream settings subscribe UI.
- window.location.assign("#streams/all");
+ window.location.assign("#channels/all");
// Returning false from an onShow handler cancels the show.
return false;
}
diff --git a/web/src/gear_menu.js b/web/src/gear_menu.js
index 0f1aecd0af..7ca7b1d281 100644
--- a/web/src/gear_menu.js
+++ b/web/src/gear_menu.js
@@ -59,7 +59,7 @@ The menu itself has the selector
The items with the prefix of "hash:" are in-page
links:
- #streams
+ #channels
#settings
#organization
#about-zulip
diff --git a/web/src/hash_parser.ts b/web/src/hash_parser.ts
index 5bc832190e..16e8d7032b 100644
--- a/web/src/hash_parser.ts
+++ b/web/src/hash_parser.ts
@@ -1,11 +1,11 @@
export function get_hash_category(hash?: string): string {
- // given "#streams/subscribed", returns "streams"
+ // given "#channels/subscribed", returns "channels"
return hash ? hash.replace(/^#/, "").split(/\//)[0] : "";
}
export function get_hash_section(hash?: string): string {
// given "#settings/profile", returns "profile"
- // given '#streams/5/social", returns "5"
+ // given '#channels/5/social", returns "5"
if (!hash) {
return "";
}
@@ -17,7 +17,7 @@ export function get_hash_section(hash?: string): string {
function get_nth_hash_section(hash: string, n: number): string {
// given "#settings/profile" and n=1, returns "profile"
- // given '#streams/5/social" and n=2, returns "social"
+ // given '#channels/5/social" and n=2, returns "social"
const parts = hash.replace(/\/$/, "").split(/\//);
return parts.at(n) ?? "";
}
@@ -49,7 +49,13 @@ export function is_same_server_message_link(url: string): boolean {
export function is_overlay_hash(hash: string): boolean {
// Hash changes within this list are overlays and should not unnarrow (etc.)
const overlay_list = [
+ // In 2024, stream was renamed to channel in the Zulip API and UI.
+ // Because pre-change Welcome Bot and Notification Bot messages
+ // included links to "/#streams/all" and "/#streams/new", we'll
+ // need to support "streams" as an overlay hash as an alias for
+ // "channels" permanently.
"streams",
+ "channels",
"drafts",
"groups",
"settings",
@@ -72,7 +78,7 @@ export function is_overlay_hash(hash: string): boolean {
export function is_editing_stream(desired_stream_id: number): boolean {
const hash_components = window.location.hash.slice(1).split(/\//);
- if (hash_components[0] !== "streams") {
+ if (hash_components[0] !== "channels") {
return false;
}
@@ -88,7 +94,7 @@ export function is_editing_stream(desired_stream_id: number): boolean {
}
export function is_create_new_stream_narrow(): boolean {
- return window.location.hash === "#streams/new";
+ return window.location.hash === "#channels/new";
}
// This checks whether the user is in the stream settings menu
@@ -96,7 +102,7 @@ export function is_create_new_stream_narrow(): boolean {
export function is_subscribers_section_opened_for_stream(): boolean {
const hash_components = window.location.hash.slice(1).split(/\//);
- if (hash_components[0] !== "streams") {
+ if (hash_components[0] !== "channels") {
return false;
}
if (!hash_components[3]) {
diff --git a/web/src/hash_util.ts b/web/src/hash_util.ts
index a5897cf731..af259373a3 100644
--- a/web/src/hash_util.ts
+++ b/web/src/hash_util.ts
@@ -145,7 +145,7 @@ export function by_conversation_and_time_url(message: Message): string {
}
export function stream_edit_url(sub: StreamSubscription, right_side_tab: string): string {
- return `#streams/${sub.stream_id}/${internal_url.encodeHashComponent(
+ return `#channels/${sub.stream_id}/${internal_url.encodeHashComponent(
sub.name,
)}/${right_side_tab}`;
}
@@ -192,7 +192,16 @@ export function parse_narrow(hash: string): NarrowTerm[] | undefined {
return terms;
}
-export function validate_stream_settings_hash(hash: string): string {
+export function channels_settings_section_url(section = "subscribed"): string {
+ const valid_section_values = new Set(["new", "subscribed", "all"]);
+ if (!valid_section_values.has(section)) {
+ blueslip.warn("invalid section for channels settings: " + section);
+ return "#channels/subscribed";
+ }
+ return `#channels/${section}`;
+}
+
+export function validate_channels_settings_hash(hash: string): string {
const hash_components = hash.slice(1).split(/\//);
const section = hash_components[1];
@@ -201,7 +210,7 @@ export function validate_stream_settings_hash(hash: string): string {
settings_data.user_can_create_web_public_streams() ||
settings_data.user_can_create_private_streams();
if (section === "new" && !can_create_streams) {
- return "#streams/subscribed";
+ return channels_settings_section_url();
}
if (/\d+/.test(section)) {
@@ -216,27 +225,18 @@ export function validate_stream_settings_hash(hash: string): string {
//
// In all these cases we redirect the user to 'subscribed' tab.
if (sub === undefined || (page_params.is_guest && !stream_data.is_subscribed(stream_id))) {
- return "#streams/subscribed";
+ return channels_settings_section_url();
}
- const stream_name = hash_components[2];
let right_side_tab = hash_components[3];
const valid_right_side_tab_values = new Set(["general", "personal", "subscribers"]);
- if (sub.name === stream_name && valid_right_side_tab_values.has(right_side_tab)) {
- return hash;
- }
if (!valid_right_side_tab_values.has(right_side_tab)) {
right_side_tab = "general";
}
return stream_edit_url(sub, right_side_tab);
}
- const valid_section_values = ["new", "subscribed", "all"];
- if (!valid_section_values.includes(section)) {
- blueslip.warn("invalid section for streams: " + section);
- return "#streams/subscribed";
- }
- return hash;
+ return channels_settings_section_url(section);
}
export function validate_group_settings_hash(hash: string): string {
diff --git a/web/src/hashchange.js b/web/src/hashchange.js
index 4895cb0684..0dd4f48aa7 100644
--- a/web/src/hashchange.js
+++ b/web/src/hashchange.js
@@ -216,6 +216,7 @@ function do_hashchange_normal(from_reload) {
case "#search-operators":
case "#drafts":
case "#invite":
+ case "#channels":
case "#streams":
case "#organization":
case "#settings":
@@ -235,7 +236,7 @@ function do_hashchange_overlay(old_hash) {
// show the user's home view behind it.
show_home_view();
}
- const base = hash_parser.get_current_hash_category();
+ let base = hash_parser.get_current_hash_category();
const old_base = hash_parser.get_hash_category(old_hash);
let section = hash_parser.get_current_hash_section();
@@ -263,11 +264,21 @@ function do_hashchange_overlay(old_hash) {
);
}
- if (base === "streams") {
- const valid_hash = hash_util.validate_stream_settings_hash(window.location.hash);
+ // In 2024, stream was renamed to channel in the Zulip API and UI.
+ // Because pre-change Welcome Bot and Notification Bot messages
+ // included links to "/#streams/all" and "/#streams/new", we'll
+ // need to support "streams" as an overlay hash as an alias for
+ // "channels" permanently.
+ if (base === "streams" || base === "channels") {
+ const valid_hash = hash_util.validate_channels_settings_hash(window.location.hash);
+ // Here valid_hash will always return "channels" as the base.
+ // So, if we update the history because the valid hash does
+ // not match the window.location.hash, then we also reset the
+ // base string we're tracking for the hash.
if (valid_hash !== window.location.hash) {
history.replaceState(null, "", browser_history.get_full_url(valid_hash));
section = hash_parser.get_current_hash_section();
+ base = hash_parser.get_current_hash_category();
}
}
@@ -279,14 +290,14 @@ function do_hashchange_overlay(old_hash) {
}
}
- // Start by handling the specific case of going
- // from something like streams/all to streams_subscribed.
+ // Start by handling the specific case of going from
+ // something like "#channels/all" to "#channels/subscribed".
//
// In most situations we skip by this logic and load
// the new overlay.
if (coming_from_overlay && base === old_base) {
- if (base === "streams") {
- // e.g. #streams/29/social/subscribers
+ if (base === "channels") {
+ // e.g. #channels/29/social/subscribers
const right_side_tab = hash_parser.get_current_nth_hash_section(3);
stream_settings_ui.change_state(section, undefined, right_side_tab);
return;
@@ -353,8 +364,8 @@ function do_hashchange_overlay(old_hash) {
browser_history.set_hash_before_overlay(old_hash);
}
- if (base === "streams") {
- // e.g. #streams/29/social/subscribers
+ if (base === "channels") {
+ // e.g. #channels/29/social/subscribers
const right_side_tab = hash_parser.get_current_nth_hash_section(3);
if (is_somebody_else_profile_open()) {
diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js
index 520e6a6b60..b63fc756fc 100644
--- a/web/src/stream_edit.js
+++ b/web/src/stream_edit.js
@@ -44,9 +44,9 @@ export function setup_subscriptions_tab_hash(tab_key_value) {
return;
}
if (tab_key_value === "all-streams") {
- browser_history.update("#streams/all");
+ browser_history.update("#channels/all");
} else if (tab_key_value === "subscribed") {
- browser_history.update("#streams/subscribed");
+ browser_history.update("#channels/subscribed");
} else {
blueslip.debug("Unknown tab_key_value: " + tab_key_value);
}
diff --git a/web/src/stream_settings_ui.js b/web/src/stream_settings_ui.js
index 405e6dc11b..20a28bea62 100644
--- a/web/src/stream_settings_ui.js
+++ b/web/src/stream_settings_ui.js
@@ -731,7 +731,7 @@ function show_right_section() {
}
export function change_state(section, left_side_tab, right_side_tab) {
- // if in #streams/new form.
+ // if in #channels/new form.
if (section === "new") {
do_open_create_stream();
show_right_section();
@@ -880,7 +880,7 @@ export function do_open_create_stream() {
export function open_create_stream() {
do_open_create_stream();
- browser_history.update("#streams/new");
+ browser_history.update("#channels/new");
}
export function update_stream_privacy_choices(policy) {
diff --git a/web/templates/compose_banner/stream_does_not_exist_error.hbs b/web/templates/compose_banner/stream_does_not_exist_error.hbs
index 07aa2c4825..0a5052dd49 100644
--- a/web/templates/compose_banner/stream_does_not_exist_error.hbs
+++ b/web/templates/compose_banner/stream_does_not_exist_error.hbs
@@ -3,7 +3,7 @@
{{#tr}}
The channel #{channel_name} does not exist. Manage your subscriptions
on your Channels page.
- {{#*inline "z-link"}}{{> @partial-block}}{{/inline}}
+ {{#*inline "z-link"}}{{> @partial-block}}{{/inline}}
{{/tr}}
{{/compose_banner}}
diff --git a/web/templates/gear_menu_popover.hbs b/web/templates/gear_menu_popover.hbs
index 2cda23b9f1..971733a376 100644
--- a/web/templates/gear_menu_popover.hbs
+++ b/web/templates/gear_menu_popover.hbs
@@ -55,7 +55,7 @@