In the very early days of Zulip, we didn't have unread counts; just the pointer, and the correct behavior when opening a new tab was to place you near the pointer. That doesn't make any sense now that we do have unread counts, and this corner case has been a wart for a long time. This commit does the main behavior change here. However, there's a bug we need to fix, where we might end up trying to pre-render a view of the narrow based on the `all_msg_list` data before `all_msg_list` is caught up). We need to fix that bug before we can merge this; it should be possible to determine that using `FetchStatus` on `all_msg_list`, or with better performance by using the `unread_msgs` structure to determine whether the message we should be selecting is present locally. Fixes #789. Fixes #9070.
		
			
				
	
	
	
		
			4.8 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	Unread counts and the pointer
When you're using Zulip and you reload, or narrow to a stream, 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 (stream/topic or private 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 narrowed tab: unread/recent matching narrow
If you instead narrow by clicking on something in the left sidebar or typing some terms into the search box, 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'ev 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 streams or in muted topics within non-muted streams.)
Unnarrow: previous sequence
When you unnarrow using e.g. the escape key, you will automatically be taken to the same message that was selected in the All messages 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 All messages view (or the bottom of the feed if there is none). This makes for a nice experience reading threads via the All messages view in sequence.
New All messages view: "high watermark"
When you open a new browser window or tab to the All messages view (a.k.a. the
interleaved view you get if you visit /), Zulip will select the
furthest down that your cursor has ever reached in the All messages
view. Because of the logic around unnarrowing in the last bullet, this
is usually just before the first unread message in the All messages view, but
if you never go to the All messages view, or you leave messages unread on some
streams in your All messages view, this can lag.
We plan to change this to automatically advance the pointer in a way similar to the unnarrow logic.
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.
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).
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.