hash_util: Extract hash_parser.ts.

This allows these functions to be used by modules that need this code
and cannot import people.ts without creating import cycles.
This commit is contained in:
Tim Abbott 2023-10-01 13:03:44 -07:00
parent 1aea88fac1
commit 9283da57f0
13 changed files with 170 additions and 165 deletions

View File

@ -1,7 +1,7 @@
// TODO: Rewrite this module to use history.pushState.
import * as blueslip from "./blueslip";
import * as hash_util from "./hash_util";
import * as hash_parser from "./hash_parser";
import * as ui_util from "./ui_util";
import {user_settings} from "./user_settings";
@ -20,7 +20,7 @@ export const state: {
// so that we can take user back to the allowed hash.
// TODO: Store #narrow old hashes. Currently they are not stored here since, the #narrow
// hashes are changed without calling `hashchanged` in many ways.
spectator_old_hash: hash_util.is_spectator_compatible(window.location.hash)
spectator_old_hash: hash_parser.is_spectator_compatible(window.location.hash)
? window.location.hash
: null,
};
@ -41,7 +41,7 @@ export function set_hash_before_overlay(hash: string): void {
export function update_web_public_hash(hash: string): boolean {
// Returns true if hash is web-public compatible.
if (hash_util.is_spectator_compatible(hash)) {
if (hash_parser.is_spectator_compatible(hash)) {
state.spectator_old_hash = hash;
return true;
}
@ -80,7 +80,7 @@ export function update(new_hash: string): void {
}
export function exit_overlay(): void {
if (hash_util.is_overlay_hash(window.location.hash) && !state.changing_hash) {
if (hash_parser.is_overlay_hash(window.location.hash) && !state.changing_hash) {
ui_util.blur_active_element();
const new_hash = state.hash_before_overlay || `#${user_settings.default_view}`;
update(new_hash);

View File

@ -3,7 +3,7 @@ import _ from "lodash";
import * as resolved_topic from "../shared/src/resolved_topic";
import render_search_description from "../templates/search_description.hbs";
import * as hash_util from "./hash_util";
import * as hash_parser from "./hash_parser";
import {$t} from "./i18n";
import * as message_parser from "./message_parser";
import * as message_store from "./message_store";
@ -1125,7 +1125,7 @@ export class Filter {
if (op.operand === undefined) {
return false;
}
if (!hash_util.allowed_web_public_narrows.includes(op.operator)) {
if (!hash_parser.allowed_web_public_narrows.includes(op.operator)) {
return false;
}
}

111
web/src/hash_parser.ts Normal file
View File

@ -0,0 +1,111 @@
export function get_hash_category(hash?: string): string {
// given "#streams/subscribed", returns "streams"
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"
if (!hash) {
return "";
}
const parts = hash.replace(/\/$/, "").split(/\//);
return parts[1] || "";
}
export function get_current_hash_category(): string {
return get_hash_category(window.location.hash);
}
export function get_current_hash_section(): string {
return get_hash_section(window.location.hash);
}
export function is_overlay_hash(hash: string): boolean {
// Hash changes within this list are overlays and should not unnarrow (etc.)
const overlay_list = [
"streams",
"drafts",
"groups",
"settings",
"organization",
"invite",
"keyboard-shortcuts",
"message-formatting",
"search-operators",
"about-zulip",
"scheduled",
];
const main_hash = get_hash_category(hash);
return overlay_list.includes(main_hash);
}
// this finds the stream that is actively open in the settings and focused in
// the left side.
export function is_editing_stream(desired_stream_id: number): boolean {
const hash_components = window.location.hash.slice(1).split(/\//);
if (hash_components[0] !== "streams") {
return false;
}
if (!hash_components[2]) {
return false;
}
// if the string casted to a number is valid, and another component
// after exists then it's a stream name/id pair.
const stream_id = Number.parseFloat(hash_components[1]);
return stream_id === desired_stream_id;
}
export function is_create_new_stream_narrow(): boolean {
return window.location.hash === "#streams/new";
}
export const allowed_web_public_narrows = [
"streams",
"stream",
"topic",
"sender",
"has",
"search",
"near",
"id",
];
export function is_spectator_compatible(hash: string): boolean {
// Defines which views are supported for spectators.
// This implementation should agree with the similar function in zerver/lib/narrow.py.
const web_public_allowed_hashes = [
"",
// full #narrow hash handled in filter.is_spectator_compatible
"narrow",
// TODO/compatibility: #recent_topics was renamed to #recent
// in 2022. We should support the old URL fragment at least
// until one cannot directly upgrade from Zulip 5.x.
"recent_topics",
"recent",
"keyboard-shortcuts",
"message-formatting",
"search-operators",
"all_messages",
"about-zulip",
];
const main_hash = get_hash_category(hash);
if (main_hash === "narrow") {
const hash_section = get_hash_section(hash);
if (!allowed_web_public_narrows.includes(hash_section)) {
return false;
}
return true;
}
return web_public_allowed_hashes.includes(main_hash);
}

View File

@ -9,31 +9,6 @@ import type {UserGroup} from "./user_groups";
type Operator = {operator: string; operand: string; negated?: boolean};
export function get_hash_category(hash?: string): string {
// given "#streams/subscribed", returns "streams"
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"
if (!hash) {
return "";
}
const parts = hash.replace(/\/$/, "").split(/\//);
return parts[1] || "";
}
export function get_current_hash_category(): string {
return get_hash_category(window.location.hash);
}
export function get_current_hash_section(): string {
return get_hash_section(window.location.hash);
}
export function build_reload_url(): string {
let hash = window.location.hash;
if (hash.length !== 0 && hash[0] === "#") {
@ -211,90 +186,3 @@ export function parse_narrow(hash: string): Operator[] | undefined {
}
return operators;
}
export function is_overlay_hash(hash: string): boolean {
// Hash changes within this list are overlays and should not unnarrow (etc.)
const overlay_list = [
"streams",
"drafts",
"groups",
"settings",
"organization",
"invite",
"keyboard-shortcuts",
"message-formatting",
"search-operators",
"about-zulip",
"scheduled",
];
const main_hash = get_hash_category(hash);
return overlay_list.includes(main_hash);
}
// this finds the stream that is actively open in the settings and focused in
// the left side.
export function is_editing_stream(desired_stream_id: number): boolean {
const hash_components = window.location.hash.slice(1).split(/\//);
if (hash_components[0] !== "streams") {
return false;
}
if (!hash_components[2]) {
return false;
}
// if the string casted to a number is valid, and another component
// after exists then it's a stream name/id pair.
const stream_id = Number.parseFloat(hash_components[1]);
return stream_id === desired_stream_id;
}
export function is_create_new_stream_narrow(): boolean {
return window.location.hash === "#streams/new";
}
export const allowed_web_public_narrows = [
"streams",
"stream",
"topic",
"sender",
"has",
"search",
"near",
"id",
];
export function is_spectator_compatible(hash: string): boolean {
// Defines which views are supported for spectators.
// This implementation should agree with the similar function in zerver/lib/narrow.py.
const web_public_allowed_hashes = [
"",
// full #narrow hash handled in filter.is_spectator_compatible
"narrow",
// TODO/compatibility: #recent_topics was renamed to #recent
// in 2022. We should support the old URL fragment at least
// until one cannot directly upgrade from Zulip 5.x.
"recent_topics",
"recent",
"keyboard-shortcuts",
"message-formatting",
"search-operators",
"all_messages",
"about-zulip",
];
const main_hash = get_hash_category(hash);
if (main_hash === "narrow") {
const hash_section = get_hash_section(hash);
if (!allowed_web_public_narrows.includes(hash_section)) {
return false;
}
return true;
}
return web_public_allowed_hashes.includes(main_hash);
}

View File

@ -5,6 +5,7 @@ import * as admin from "./admin";
import * as blueslip from "./blueslip";
import * as browser_history from "./browser_history";
import * as drafts from "./drafts";
import * as hash_parser from "./hash_parser";
import * as hash_util from "./hash_util";
import {$t_html} from "./i18n";
import * as inbox_ui from "./inbox_ui";
@ -286,9 +287,9 @@ function do_hashchange_overlay(old_hash) {
// show the user's default view behind it.
show_default_view();
}
const base = hash_util.get_current_hash_category();
const old_base = hash_util.get_hash_category(old_hash);
let section = hash_util.get_current_hash_section();
const 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();
if (base === "groups" && (!page_params.development_environment || page_params.is_guest)) {
// The #groups settings page is unfinished, and disabled in production.
@ -296,7 +297,7 @@ function do_hashchange_overlay(old_hash) {
return;
}
const coming_from_overlay = hash_util.is_overlay_hash(old_hash);
const coming_from_overlay = hash_parser.is_overlay_hash(old_hash);
if (section === "display-settings") {
// Since display-settings was deprecated and replaced with preferences
// #settings/display-settings is being redirected to #settings/preferences.
@ -464,7 +465,7 @@ function hashchanged(from_reload, e) {
return undefined;
}
if (hash_util.is_overlay_hash(current_hash)) {
if (hash_parser.is_overlay_hash(current_hash)) {
browser_history.state.changing_hash = true;
do_hashchange_overlay(old_hash);
browser_history.state.changing_hash = false;

View File

@ -13,7 +13,7 @@ import * as compose_recipient from "./compose_recipient";
import * as compose_state from "./compose_state";
import * as condense from "./condense";
import {Filter} from "./filter";
import * as hash_util from "./hash_util";
import * as hash_parser from "./hash_parser";
import * as hashchange from "./hashchange";
import {$t} from "./i18n";
import * as inbox_ui from "./inbox_ui";
@ -210,7 +210,8 @@ export function activate(raw_operators, opts) {
page_params.is_spectator &&
raw_operators.length &&
raw_operators.some(
(raw_operator) => !hash_util.allowed_web_public_narrows.includes(raw_operator.operator),
(raw_operator) =>
!hash_parser.allowed_web_public_narrows.includes(raw_operator.operator),
)
) {
spectators.login_to_access();

View File

@ -6,7 +6,7 @@ import render_dialog_default_language from "../templates/default_language_modal.
import * as channel from "./channel";
import * as dialog_widget from "./dialog_widget";
import * as emojisets from "./emojisets";
import * as hash_util from "./hash_util";
import * as hash_parser from "./hash_parser";
import {$t_html, get_language_list_columns, get_language_name} from "./i18n";
import * as loading from "./loading";
import * as overlays from "./overlays";
@ -121,7 +121,7 @@ function user_default_language_modal_post_render() {
function default_language_modal_post_render() {
if (page_params.is_spectator) {
spectator_default_language_modal_post_render();
} else if (hash_util.get_current_hash_category() === "organization") {
} else if (hash_parser.get_current_hash_category() === "organization") {
org_notification_default_language_modal_post_render();
} else {
user_default_language_modal_post_render();
@ -131,7 +131,7 @@ function default_language_modal_post_render() {
export function launch_default_language_setting_modal() {
let selected_language = user_settings.default_language;
if (hash_util.get_current_hash_category() === "organization") {
if (hash_parser.get_current_hash_category() === "organization") {
selected_language = page_params.realm_default_language;
}

View File

@ -7,7 +7,7 @@ import render_default_stream_choice from "../templates/settings/default_stream_c
import * as channel from "./channel";
import * as dialog_widget from "./dialog_widget";
import * as dropdown_widget from "./dropdown_widget";
import * as hash_util from "./hash_util";
import * as hash_parser from "./hash_parser";
import {$t_html} from "./i18n";
import * as ListWidget from "./list_widget";
import * as loading from "./loading";
@ -132,7 +132,7 @@ export function build_default_stream_table() {
}
export function update_default_streams_table() {
if (["organization", "settings"].includes(hash_util.get_current_hash_category())) {
if (["organization", "settings"].includes(hash_parser.get_current_hash_category())) {
$("#admin_default_streams_table").expectOne().find("tr.default_stream_row").remove();
build_default_stream_table();
}

View File

@ -9,7 +9,7 @@ import render_stream_subscription_request_result from "../templates/stream_setti
import * as add_subscribers_pill from "./add_subscribers_pill";
import * as blueslip from "./blueslip";
import * as confirm_dialog from "./confirm_dialog";
import * as hash_util from "./hash_util";
import * as hash_parser from "./hash_parser";
import {$t, $t_html} from "./i18n";
import * as ListWidget from "./list_widget";
import {page_params} from "./page_params";
@ -291,7 +291,7 @@ function remove_subscriber({stream_id, target_user_id, $list_entry}) {
export function update_subscribers_list(sub) {
// This is for the "Subscribers" tab of the right panel.
// Render subscriptions only if stream settings is open
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
blueslip.info("ignoring subscription for stream that is no longer being edited");
return;
}
@ -324,7 +324,7 @@ function update_subscribers_list_widget(subscriber_ids) {
}
export function rerender_subscribers_list(sub) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
blueslip.info("ignoring subscription for stream that is no longer being edited");
return;
}

View File

@ -17,6 +17,7 @@ import * as compose_recipient from "./compose_recipient";
import * as compose_state from "./compose_state";
import * as confirm_dialog from "./confirm_dialog";
import * as dropdown_widget from "./dropdown_widget";
import * as hash_parser from "./hash_parser";
import * as hash_util from "./hash_util";
import {$t, $t_html} from "./i18n";
import * as keydown_util from "./keydown_util";
@ -318,7 +319,7 @@ export function remove_stream(stream_id) {
const $row = row_for_stream_id(stream_id);
$row.remove();
update_empty_left_panel_message();
if (hash_util.is_editing_stream(stream_id)) {
if (hash_parser.is_editing_stream(stream_id)) {
stream_edit.open_edit_panel_empty();
}
}
@ -353,7 +354,7 @@ export function update_settings_for_subscribed(slim_sub) {
}
export function show_active_stream_in_left_panel() {
const selected_row = hash_util.get_current_hash_section();
const selected_row = hash_parser.get_current_hash_section();
if (Number.parseFloat(selected_row)) {
const $sub_row = row_for_stream_id(selected_row);
@ -877,7 +878,7 @@ export function launch(section) {
export function switch_rows(event) {
const active_data = get_active_data();
let $switch_row;
if (hash_util.is_create_new_stream_narrow()) {
if (hash_parser.is_create_new_stream_narrow()) {
// Prevent switching stream rows when creating a new stream
return false;
} else if (!active_data.id || active_data.$row.hasClass("notdisplayed")) {

View File

@ -5,7 +5,7 @@ import render_announce_stream_checkbox from "../templates/stream_settings/announ
import render_stream_privacy_icon from "../templates/stream_settings/stream_privacy_icon.hbs";
import render_stream_settings_tip from "../templates/stream_settings/stream_settings_tip.hbs";
import * as hash_util from "./hash_util";
import * as hash_parser from "./hash_parser";
import {$t} from "./i18n";
import {page_params} from "./page_params";
import * as settings_data from "./settings_data";
@ -33,7 +33,7 @@ export function initialize_cant_subscribe_popover() {
}
export function update_toggler_for_sub(sub) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
if (sub.subscribed) {
@ -54,7 +54,7 @@ export function update_toggler_for_sub(sub) {
}
export function enable_or_disable_subscribers_tab(sub) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
@ -70,7 +70,7 @@ export function enable_or_disable_subscribers_tab(sub) {
}
export function update_settings_button_for_sub(sub) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
@ -100,7 +100,7 @@ export function update_settings_button_for_sub(sub) {
export function update_regular_sub_settings(sub) {
// These are in the right panel.
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
const $settings = $(`.subscription_settings[data-stream-id='${CSS.escape(sub.stream_id)}']`);
@ -141,7 +141,7 @@ export function update_default_stream_and_stream_privacy_state($container) {
}
export function enable_or_disable_permission_settings_in_edit_panel(sub) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
@ -173,7 +173,7 @@ export function enable_or_disable_permission_settings_in_edit_panel(sub) {
}
export function update_announce_stream_option() {
if (!hash_util.is_create_new_stream_narrow()) {
if (!hash_parser.is_create_new_stream_narrow()) {
return;
}
if (stream_data.get_notifications_stream() === "") {
@ -191,7 +191,7 @@ export function update_announce_stream_option() {
}
export function update_stream_privacy_icon_in_settings(sub) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
@ -244,7 +244,7 @@ export function update_stream_row_in_settings_tab(sub) {
}
export function update_add_subscriptions_elements(sub) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}
@ -280,7 +280,7 @@ export function update_add_subscriptions_elements(sub) {
}
export function update_setting_element(sub, setting_name) {
if (!hash_util.is_editing_stream(sub.stream_id)) {
if (!hash_parser.is_editing_stream(sub.stream_id)) {
return;
}

View File

@ -5,6 +5,7 @@ const {strict: assert} = require("assert");
const {zrequire} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const hash_parser = zrequire("hash_parser");
const hash_util = zrequire("hash_util");
const stream_data = zrequire("stream_data");
const people = zrequire("people");
@ -52,26 +53,26 @@ run_test("hash_util", () => {
});
run_test("test_get_hash_category", () => {
assert.deepEqual(hash_util.get_hash_category("streams/subscribed"), "streams");
assert.deepEqual(hash_util.get_hash_category("#settings/preferences"), "settings");
assert.deepEqual(hash_util.get_hash_category("#drafts"), "drafts");
assert.deepEqual(hash_util.get_hash_category("invites"), "invites");
assert.deepEqual(hash_parser.get_hash_category("streams/subscribed"), "streams");
assert.deepEqual(hash_parser.get_hash_category("#settings/preferences"), "settings");
assert.deepEqual(hash_parser.get_hash_category("#drafts"), "drafts");
assert.deepEqual(hash_parser.get_hash_category("invites"), "invites");
window.location.hash = "#settings/profile";
assert.deepEqual(hash_util.get_current_hash_category(), "settings");
assert.deepEqual(hash_parser.get_current_hash_category(), "settings");
});
run_test("test_get_hash_section", () => {
assert.equal(hash_util.get_hash_section("streams/subscribed"), "subscribed");
assert.equal(hash_util.get_hash_section("#settings/profile"), "profile");
assert.equal(hash_parser.get_hash_section("streams/subscribed"), "subscribed");
assert.equal(hash_parser.get_hash_section("#settings/profile"), "profile");
assert.equal(hash_util.get_hash_section("settings/10/general/"), "10");
assert.equal(hash_parser.get_hash_section("settings/10/general/"), "10");
assert.equal(hash_util.get_hash_section("#drafts"), "");
assert.equal(hash_util.get_hash_section(""), "");
assert.equal(hash_parser.get_hash_section("#drafts"), "");
assert.equal(hash_parser.get_hash_section(""), "");
window.location.hash = "#settings/profile";
assert.deepEqual(hash_util.get_current_hash_section(), "profile");
assert.deepEqual(hash_parser.get_current_hash_section(), "profile");
});
run_test("build_reload_url", () => {
@ -90,26 +91,26 @@ run_test("build_reload_url", () => {
run_test("test is_editing_stream", () => {
window.location.hash = "#streams/1/announce";
assert.equal(hash_util.is_editing_stream(1), true);
assert.equal(hash_util.is_editing_stream(2), false);
assert.equal(hash_parser.is_editing_stream(1), true);
assert.equal(hash_parser.is_editing_stream(2), false);
// url is missing name at end
window.location.hash = "#streams/1";
assert.equal(hash_util.is_editing_stream(1), false);
assert.equal(hash_parser.is_editing_stream(1), false);
window.location.hash = "#streams/bogus/bogus";
assert.equal(hash_util.is_editing_stream(1), false);
assert.equal(hash_parser.is_editing_stream(1), false);
window.location.hash = "#test/narrow";
assert.equal(hash_util.is_editing_stream(1), false);
assert.equal(hash_parser.is_editing_stream(1), false);
});
run_test("test_is_create_new_stream_narrow", () => {
window.location.hash = "#streams/new";
assert.equal(hash_util.is_create_new_stream_narrow(), true);
assert.equal(hash_parser.is_create_new_stream_narrow(), true);
window.location.hash = "#some/random/hash";
assert.equal(hash_util.is_create_new_stream_narrow(), false);
assert.equal(hash_parser.is_create_new_stream_narrow(), false);
});
run_test("test_parse_narrow", () => {

View File

@ -14,9 +14,11 @@ const scroll_util = mock_esm("../src/scroll_util", {
mock_esm("../src/hash_util", {
by_stream_url() {},
get_current_hash_section: () => denmark_stream_id,
});
mock_esm("../src/hash_parser", {
get_current_hash_section: () => denmark_stream_id,
});
set_global("page_params", {});
const stream_data = zrequire("stream_data");