mirror of https://github.com/zulip/zulip.git
106 lines
4.5 KiB
Markdown
106 lines
4.5 KiB
Markdown
# 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.
|