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:
105
docs/subsystems/pointer.md
Normal file
105
docs/subsystems/pointer.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Unread counts and the pointer
|
||||
|
||||
When you're using Zulip and you reload, or narrow to a channel, how
|
||||
does Zulip decide where to place you?
|
||||
|
||||
Conceptually, Zulip takes you to the place where you left off
|
||||
(e.g., the first unread message), not the most recent messages, to
|
||||
facilitate reviewing all the discussions that happened while you were
|
||||
away from your computer. The scroll position is then set to keep that
|
||||
message in view and away from both the top and bottom of the visible
|
||||
section of messages.
|
||||
|
||||
But there a lot of details around doing this right, and around
|
||||
counting unread messages. Here's how Zulip currently decides which
|
||||
message to select, along with some notes on improvements we'd like to
|
||||
make to the model.
|
||||
|
||||
First a bit of terminology:
|
||||
|
||||
- "Narrowing" is the process of filtering to a particular subset of
|
||||
the messages the user has access to.
|
||||
|
||||
- The blue cursor box (the "pointer") is around is called the
|
||||
"selected" message. Zulip ensures that the currently selected
|
||||
message is always in-view.
|
||||
|
||||
## Pointer logic
|
||||
|
||||
### Recipient bar: message you clicked
|
||||
|
||||
If you enter a narrow by clicking on a message group's _recipient bar_
|
||||
(channel/topic or direct message recipient list at the top of a group
|
||||
of messages), Zulip will select the message you clicked on. This
|
||||
provides a nice user experience where you get to see the stuff near
|
||||
what you clicked on, and in fact the message you clicked on stays at
|
||||
exactly the same scroll position in the window after the narrowing as
|
||||
it was at before.
|
||||
|
||||
### Search, sidebar click, or new tab: unread/recent matching narrow
|
||||
|
||||
If you instead narrow by clicking on something in the left sidebar,
|
||||
typing some terms into the search box, reloading the browser, or any
|
||||
other method that doesn't encode a specific message to visit, Zulip
|
||||
will instead select the first unread message matching that narrow, or
|
||||
if there are none, the most recent messages matching that narrow.
|
||||
|
||||
This provides the nice user experience of taking you to the start of
|
||||
the new stuff (with enough messages you've seen before still in view
|
||||
at the top to provide you with context), which is usually what you
|
||||
want. (When finding the "first unread message", Zulip ignores unread
|
||||
messages in muted channels or in muted topics within non-muted
|
||||
channels.)
|
||||
|
||||
### Unnarrow: previous sequence
|
||||
|
||||
When you unnarrow using, for example, the `a` key, you will automatically be
|
||||
taken to the same message that was selected in the Combined feed view before
|
||||
you narrowed, unless in the narrow you read new messages, in which
|
||||
case you will be jumped forward to the first unread and non-muted
|
||||
message in the Combined feed view (or the bottom of the feed if there is
|
||||
none). This makes for a nice experience reading threads via the Combined feed
|
||||
view in sequence.
|
||||
|
||||
### Forced reload: state preservation
|
||||
|
||||
When the server forces a reload of a browser that's otherwise caught
|
||||
up (which happens within 30 minutes when a new version of the server
|
||||
is deployed, usually at a type when the user isn't looking at the
|
||||
browser), Zulip will preserve the state -- what (if any) narrow the
|
||||
user was in, the selected message, and even exact scroll position!
|
||||
|
||||
For more on the user experience philosophy guiding these decisions,
|
||||
see [the architectural overview](../overview/architecture-overview.md).
|
||||
|
||||
## Unread count logic
|
||||
|
||||
How does Zulip decide whether a message has been read by the user?
|
||||
The algorithm needs to correctly handle a range of ways people might
|
||||
use the product. The algorithm is as follows:
|
||||
|
||||
- Any message which is selected or above a message which is selected
|
||||
is marked as read. So messages are marked as read as you scroll
|
||||
down the keyboard when the pointer passes over them.
|
||||
|
||||
- If the whitespace at the very bottom of the feed is in view, all
|
||||
messages in view are marked as read.
|
||||
|
||||
These two simple rules, combined with the pointer logic above, end up
|
||||
matching user expectations well for whether the product should treat
|
||||
them as having read a set of messages (or not).
|
||||
|
||||
One key detail to highlight is that we only mark messages as read
|
||||
through these processes in views that contain all messages in a
|
||||
thread; search views will never mark messages as read.
|
||||
|
||||
## Testing and development
|
||||
|
||||
In a Zulip development environment, you can use
|
||||
`manage.py mark_all_messages_unread` to set every user's pointer to 0
|
||||
and all messages as unread, for convenience in testing unread count
|
||||
related logic.
|
||||
|
||||
It can be useful to combine this with `manage.py populate_db -n 3000`
|
||||
(which rebuilds the database with 3000 initial messages) to ensure a
|
||||
large number of messages are present.
|
||||
Reference in New Issue
Block a user