2017-03-22 23:16:46 +01:00
|
|
|
# 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
|
2017-03-23 20:27:08 +01:00
|
|
|
"back" functionality to let the user navigate between parts of the UI.
|
2017-03-22 23:16:46 +01:00
|
|
|
Some examples are:
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- `/#settings/your-bots`: Bots section of the settings overlay.
|
2024-05-20 17:38:40 +02:00
|
|
|
- `/#channels`: Channels overlay, where the user manages channels
|
2017-03-23 20:27:08 +01:00
|
|
|
(subscription etc.)
|
2024-05-20 17:38:40 +02:00
|
|
|
- `/#channels/11/announce`: Channels overlay with channel ID 11 (called
|
2017-03-23 20:27:08 +01:00
|
|
|
"announce") selected.
|
2024-06-07 09:17:54 +02:00
|
|
|
- `/#narrow/channel/42-android/topic/fun`: Message feed showing channel
|
2021-08-20 21:53:28 +02:00
|
|
|
"android" and topic "fun". (The `42` represents the id of the
|
2024-05-20 17:38:40 +02:00
|
|
|
channel.)
|
2017-03-22 23:16:46 +01:00
|
|
|
|
|
|
|
The main module in the frontend that manages this all is
|
2023-02-22 23:03:47 +01:00
|
|
|
`web/src/hashchange.js` (plus `hash_util.js` for all the parsing
|
2021-08-20 21:53:28 +02:00
|
|
|
code), which is unfortunately one of our thorniest modules. Part of
|
2017-03-22 23:16:46 +01:00
|
|
|
the reason that it's thorny is that it needs to support a lot of
|
|
|
|
different flows:
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- The user clicking on an in-app link, which in turn opens an overlay.
|
2024-05-20 17:38:40 +02:00
|
|
|
For example the channels overlay opens when the user clicks the small
|
2017-03-23 20:27:08 +01:00
|
|
|
cog symbol on the left sidebar, which is in fact a link to
|
2024-04-30 13:30:24 +02:00
|
|
|
`/#channels`. This makes it easy to have simple links around the app
|
2017-03-22 23:16:46 +01:00
|
|
|
without custom click handlers for each one.
|
2021-08-20 21:45:39 +02:00
|
|
|
- The user uses the "back" button in their browser (basically
|
2021-08-20 22:54:08 +02:00
|
|
|
equivalent to the previous one, as a _link_ out of the browser history
|
2017-03-23 20:27:08 +01:00
|
|
|
will be visited).
|
2024-07-04 12:33:43 +02:00
|
|
|
- The user clicking some in-app click handler (e.g., "Channel settings"
|
2024-05-20 17:38:40 +02:00
|
|
|
for an individual channel), that potentially does
|
2024-07-09 17:45:31 +02:00
|
|
|
several UI-manipulating things including, for example, loading the
|
|
|
|
channels overlay, and needs to update the hash without re-triggering
|
|
|
|
the open animation (etc.).
|
2024-05-20 17:38:40 +02:00
|
|
|
- Within an overlay like the channels overlay, the user clicks to
|
2017-03-22 23:16:46 +01:00
|
|
|
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).
|
2021-08-20 21:45:39 +02:00
|
|
|
- The user is in a part of the web app, and reloads their browser window.
|
2017-03-22 23:16:46 +01:00
|
|
|
Ideally the reloaded browser window should return them to their
|
|
|
|
original state.
|
2021-08-20 21:45:39 +02:00
|
|
|
- A server-initiated browser reload (done after a new version is
|
2017-03-23 20:27:08 +01:00
|
|
|
deployed, or when a user comes back after being idle for a while,
|
|
|
|
see [notes below][self-server-reloads]), where we try to preserve
|
2024-07-04 12:33:43 +02:00
|
|
|
extra state (e.g., content of compose box, scroll position within a
|
2017-03-23 20:27:08 +01:00
|
|
|
narrow) using the `/#reload` hash prefix.
|
2017-03-22 23:16:46 +01:00
|
|
|
|
2017-03-23 20:27:08 +01:00
|
|
|
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
|
2020-08-31 03:39:34 +02:00
|
|
|
[Puppeteer suite][testing-with-puppeteer]) and there's enough complexity
|
2017-03-23 20:27:08 +01:00
|
|
|
that it's easy to accidentally break something.
|
2017-03-22 23:16:46 +01:00
|
|
|
|
2023-02-22 23:03:47 +01:00
|
|
|
The main external API lives in `web/src/browser_history.js`:
|
2018-12-04 19:00:01 +01:00
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- `browser_history.update` is used to update the browser
|
2018-12-04 19:00:01 +01:00
|
|
|
history, and it should be called when the app code is taking care
|
|
|
|
of updating the UI directly
|
2021-08-20 21:45:39 +02:00
|
|
|
- `browser_history.go_to_location` is used when you want the `hashchange`
|
2018-12-04 19:00:01 +01:00
|
|
|
module to actually dispatch building the next page
|
|
|
|
|
|
|
|
Internally you have these functions:
|
2017-03-22 23:16:46 +01:00
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- `hashchange.hashchanged` is the function used to handle the hash,
|
2024-07-04 12:33:43 +02:00
|
|
|
whether it's changed by the browser (e.g., by clicking on a link to
|
2018-12-04 19:00:01 +01:00
|
|
|
a hash or using the back button) or triggered internally.
|
2021-08-20 21:45:39 +02:00
|
|
|
- `hashchange.do_hashchange_normal` handles most cases, like loading the main
|
2018-12-04 19:00:01 +01:00
|
|
|
page (but maybe with a specific URL if you are narrowed to a
|
2024-05-20 17:38:40 +02:00
|
|
|
channel or topic or direct messages, etc.).
|
2021-08-20 21:53:28 +02:00
|
|
|
- `hashchange.do_hashchange_overlay` handles overlay cases. Overlays have
|
2018-12-04 19:00:01 +01:00
|
|
|
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).
|
2017-03-22 23:16:46 +01:00
|
|
|
|
|
|
|
## Server-initiated reloads
|
|
|
|
|
|
|
|
There are a few circumstances when the Zulip browser window needs to
|
|
|
|
reload itself:
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- If the browser has been offline for more than 10 minutes, the
|
2017-03-23 20:27:08 +01:00
|
|
|
browser's [event queue][events-system] will have been
|
2017-03-22 23:16:46 +01:00
|
|
|
garbage-collected by the server, meaning the browser can no longer
|
2021-08-20 21:53:28 +02:00
|
|
|
get real-time updates altogether. In this case, the browser
|
|
|
|
auto-reloads immediately in order to reconnect. We have coded an
|
2021-03-22 13:59:50 +01:00
|
|
|
unsuspend callback (based on some clever time logic) that ensures we
|
|
|
|
check immediately when a client unsuspends; grep for `watchdog` to
|
2017-03-23 20:27:08 +01:00
|
|
|
see the code.
|
2021-08-20 21:45:39 +02:00
|
|
|
- If a new version of the server has been deployed, we want to reload
|
2021-08-20 21:53:28 +02:00
|
|
|
the browser so that it will start running the latest code. However,
|
|
|
|
we don't want server deploys to be disruptive. So, the backend
|
2017-03-23 20:27:08 +01:00
|
|
|
preserves user-side event queues (etc.) and just pushes a special
|
2021-08-20 21:53:28 +02:00
|
|
|
`restart` event to all clients. That event causes the browser to
|
2017-03-22 23:16:46 +01:00
|
|
|
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
|
2021-08-20 21:53:28 +02:00
|
|
|
state so that the user never knew it happened!). The logic for
|
2024-09-18 21:40:01 +02:00
|
|
|
doing this is in `web/src/reload.ts`; but regardless we'll reload
|
2017-03-22 23:16:46 +01:00
|
|
|
within 30 minutes unconditionally.
|
|
|
|
|
|
|
|
An important detail in server-initiated reloads is that we
|
2017-03-23 20:27:08 +01:00
|
|
|
desynchronize when browsers start attempting them randomly, in
|
2017-03-22 23:16:46 +01:00
|
|
|
order to avoid a thundering herd situation bringing down the server.
|
|
|
|
|
2018-12-04 19:00:01 +01:00
|
|
|
Here are some key functions in the reload system:
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- `reload.preserve_state` is called when a server-initiated browser
|
2018-12-04 19:00:01 +01:00
|
|
|
reload happens, and encodes a bunch of data like the current scroll
|
|
|
|
position into the hash.
|
2023-10-06 09:04:22 +02:00
|
|
|
- `reload_setup.initialize` handles restoring the preserved state after a
|
2018-12-04 19:00:01 +01:00
|
|
|
reload where the hash starts with `/#reload`.
|
|
|
|
|
2017-03-22 23:16:46 +01:00
|
|
|
## 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).
|
2017-03-23 20:27:08 +01:00
|
|
|
|
2020-08-31 03:39:34 +02:00
|
|
|
[testing-with-puppeteer]: ../testing/testing-with-puppeteer.md
|
2017-03-23 20:27:08 +01:00
|
|
|
[self-server-reloads]: #server-initiated-reloads
|
2022-02-24 00:17:21 +01:00
|
|
|
[events-system]: events-system.md
|