snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
124
docs/subsystems/hashchange-system.md
Normal file
124
docs/subsystems/hashchange-system.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# URL hashes and deep linking
|
||||
|
||||
## Hashchange
|
||||
|
||||
The Zulip web application has a nice system of hash (#) URLs that can
|
||||
be used to deep-link into the application and allow the browser's
|
||||
"back" functionality to let the user navigate between parts of the UI.
|
||||
Some examples are:
|
||||
|
||||
- `/#settings/your-bots`: Bots section of the settings overlay.
|
||||
- `/#channels`: Channels overlay, where the user manages channels
|
||||
(subscription etc.)
|
||||
- `/#channels/11/announce`: Channels overlay with channel ID 11 (called
|
||||
"announce") selected.
|
||||
- `/#narrow/channel/42-android/topic/fun`: Message feed showing channel
|
||||
"android" and topic "fun". (The `42` represents the id of the
|
||||
channel.)
|
||||
|
||||
The main module in the frontend that manages this all is
|
||||
`web/src/hashchange.ts` (plus `hash_util.js` for all the parsing
|
||||
code), which is unfortunately one of our thorniest modules. Part of
|
||||
the reason that it's thorny is that it needs to support a lot of
|
||||
different flows:
|
||||
|
||||
- The user clicking on an in-app link, which in turn opens an overlay.
|
||||
For example the channels overlay opens when the user clicks the small
|
||||
cog symbol on the left sidebar, which is in fact a link to
|
||||
`/#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
|
||||
will be visited).
|
||||
- The user clicking some in-app click handler (e.g., "Channel settings"
|
||||
for an individual channel), that potentially does
|
||||
several UI-manipulating things including, for example, loading the
|
||||
channels overlay, and needs to update the hash without re-triggering
|
||||
the open animation (etc.).
|
||||
- Within an overlay like the channels overlay, the user clicks to
|
||||
another part of the overlay, which should update the hash but not
|
||||
re-trigger loading the overlay (which would result in a confusing
|
||||
animation experience).
|
||||
- The user is in a part of the web app, and reloads their browser window.
|
||||
Ideally the reloaded browser window should return them to their
|
||||
original state.
|
||||
- A server-initiated browser reload (done after a new version is
|
||||
deployed, or when a user comes back after being idle for a while,
|
||||
see [notes below][self-server-reloads]), where we try to preserve
|
||||
extra state (e.g., content of compose box, scroll position within a
|
||||
narrow) using the `/#reload` hash prefix.
|
||||
|
||||
When making changes to the hashchange system, it is **essential** to
|
||||
test all of these flows, since we don't have great automated tests for
|
||||
all of this (would be a good project to add them to the
|
||||
[Puppeteer suite][testing-with-puppeteer]) and there's enough complexity
|
||||
that it's easy to accidentally break something.
|
||||
|
||||
The main external API lives in `web/src/browser_history.js`:
|
||||
|
||||
- `browser_history.update` is used to update the browser
|
||||
history, and it should be called when the app code is taking care
|
||||
of updating the UI directly
|
||||
- `browser_history.go_to_location` is used when you want the `hashchange`
|
||||
module to actually dispatch building the next page
|
||||
|
||||
Internally you have these functions:
|
||||
|
||||
- `hashchange.hashchanged` is the function used to handle the hash,
|
||||
whether it's changed by the browser (e.g., by clicking on a link to
|
||||
a hash or using the back button) or triggered internally.
|
||||
- `hashchange.do_hashchange_normal` handles most cases, like loading the main
|
||||
page (but maybe with a specific URL if you are narrowed to a
|
||||
channel or topic or direct messages, etc.).
|
||||
- `hashchange.do_hashchange_overlay` handles overlay cases. Overlays have
|
||||
some minor complexity related to remembering the page from
|
||||
which the overlay was launched, as well as optimizing in-page
|
||||
transitions (i.e. don't close/re-open the overlay if you can
|
||||
easily avoid it).
|
||||
|
||||
## Server-initiated reloads
|
||||
|
||||
There are a few circumstances when the Zulip browser window needs to
|
||||
reload itself:
|
||||
|
||||
- If the browser has been offline for more than 10 minutes, the
|
||||
browser's [event queue][events-system] will have been
|
||||
garbage-collected by the server, meaning the browser can no longer
|
||||
get real-time updates altogether. In this case, the browser
|
||||
auto-reloads immediately in order to reconnect. We have coded an
|
||||
unsuspend callback (based on some clever time logic) that ensures we
|
||||
check immediately when a client unsuspends; grep for `watchdog` to
|
||||
see the code.
|
||||
- If a new version of the server has been deployed, we want to reload
|
||||
the browser so that it will start running the latest code. However,
|
||||
we don't want server deploys to be disruptive. So, the backend
|
||||
preserves user-side event queues (etc.) and just pushes a special
|
||||
`restart` event to all clients. That event causes the browser to
|
||||
start looking for a good time to reload, based on when the user is
|
||||
idle (ideally, we'd reload when they're not looking and restore
|
||||
state so that the user never knew it happened!). The logic for
|
||||
doing this is in `web/src/reload.ts`; but regardless we'll reload
|
||||
within 30 minutes unconditionally.
|
||||
|
||||
An important detail in server-initiated reloads is that we
|
||||
desynchronize when browsers start attempting them randomly, in
|
||||
order to avoid a thundering herd situation bringing down the server.
|
||||
|
||||
Here are some key functions in the reload system:
|
||||
|
||||
- `reload.preserve_state` is called when a server-initiated browser
|
||||
reload happens, and encodes a bunch of data like the current scroll
|
||||
position into the hash.
|
||||
- `reload_setup.initialize` handles restoring the preserved state after a
|
||||
reload where the hash starts with `/#reload`.
|
||||
|
||||
## All reloads
|
||||
|
||||
In addition to saving state as described above when reloading the
|
||||
browser, Zulip also does a few bookkeeping things on page reload (like
|
||||
cleaning up its event queue, and saving any text in an open compose
|
||||
box as a draft).
|
||||
|
||||
[testing-with-puppeteer]: ../testing/testing-with-puppeteer.md
|
||||
[self-server-reloads]: #server-initiated-reloads
|
||||
[events-system]: events-system.md
|
||||
Reference in New Issue
Block a user