Add support for a 'user is typing' indicator #150

Open
3ruce opened this Issue Oct 6, 2015 · 19 comments

Projects

None yet

7 participants

@3ruce
3ruce commented Oct 6, 2015

Like this?

psychic-mode-in-action

@timabbott timabbott changed the title from Does Zulip have a 'user is typing' indicator to Add support for a 'user is typing' indicator Oct 6, 2015
@timabbott
Member

Nope but it'd be relatively straightforward to add...

@3ruce
3ruce commented Oct 7, 2015

If only I could code...

@timabbott
Member

Well, I think you're probably not the only person interested in this feature, so someone else may add it :)

@lfaraone lfaraone added the help wanted label Oct 7, 2015
@3ruce
3ruce commented Oct 7, 2015

Thanks Tim - fyi, what's the formal status of Zulip as a project re: governance and commited developers - have Dropbox assigned people formally to Zulip etc. etc. The project looks great but it would be cool to know how much 'backing' there is, so to speak... can you point me at anything?

@r0fls
Contributor
r0fls commented Oct 7, 2015

I was thinking about giving it a shot, but it might be a little out of my grasp. I looked at compose.js in static/js and saw there is a is_composing_message variable, and a compose.composing() method. Something like that needs to be paired with the specific chat room or private message they're composing in, and then sent to the server for relay to that chat room/person. I'm still wrapping my head around the code base though. Pointers appreciated.

@timabbott
Member

On thinking about this a bit more, this is perhaps technically straightforward but is nontrivial from a UI design perspective.

I think we'll want to start with just doing it for PMs since the UI questions about where to display it are simpler there.

@r0fls you should check out the new feature tutorial (see #93) to see how to make data flow through the backend system. For the frontend piece, I think we'll want to add a hook that runs whenever composing a message starts (which is probably starting typing, not opening a compose box, since it's easy to do the latter with no intent to write) and ends and calls a new endpoint on the backend to notify about changes to a user's typing state. The zerver/lib/actions.py function to action a state change can update the stored status (maybe we want to use something fast and short-term like redis or memcached?) and then send an event notifying the target user's clients about the typing.

The tricky things for this feature in particular that come in are:
(1) How do we manage the logic around state changes and timeouts? This needs to be on the backend or the person being typed at's browser, for the case where a user starts typing and then their computer drops off the network.
(2) Where in the UI do we display typing notifications in a way that is visible but not obtrusive?

(2) is probably the hardest problem so we might want to try doing a simple mockup for where we'd put them before putting a lot of time into an implementation.

@asmartin

+1

@asmartin
  1. I would say that the "user is typing" notification could fade out if they have stopped typing (or the computer dropped off the network) for more than 10 seconds

  2. I think a simple one-line message right above the "New stream message" and "New private message" buttons or above the name of the stream and topic if the compose input box is visible. This would be unintrusive but also directly where you have your focus at the bottom of the list of existing messages and above where you're composing your response. For streams, if multiple users are typing at once it could simply change to state "Users are typing..." instead of "John Smith is typing..."

@wdaher wdaher referenced this issue in timabbott/zulip Apr 10, 2016
@timabbott timabbott Add a draft roadmap doc. ec34f29
@timabbott timabbott added this to the 2016 roadmap milestone Apr 29, 2016
@umairwaheed
Contributor

@timabbott, can I take a shot at this?

@umairwaheed
Contributor

How about this design?

screen_shot_2016-09-07_at_5_08_53_pm
screen shot 2016-09-07 at 5 09 16 pm

@3ruce
3ruce commented Sep 7, 2016

Looks great to me!

@timabbott
Member

Sure, yeah, that seems like a great project. We can start with that design and iterate from there (most of the technical complexity is going to be in the backend implementation anyway). And is probably a good project for getting deeper familiarity with the Tornado event system.

@umairwaheed
Contributor

Sure.

@timabbott
Member
timabbott commented Oct 8, 2016 edited

A few notes on the data model for this:

  • Let's start with just showing typing notifications for PMs, since that's simpler from a UX perspective. I don't think we need any database storage for typing notifications since they're so transient; we can just send state transition notifications for typing/stopped typing (but still has compose open?)/no longer typing, on a per-user basis, essentially (user typing, recipient) pairs, and aggregate on the frontend.
  • We probably want to support multiple users typing at the same time, but that aggregation can be done on the frontend rather than making the backend data model pay attention to changes there.
  • We probably want to make it easy to change the precise state change algorithm, but we might want to start with something like "send a start notice as soon as a user puts something into the compose box, send a pause event after 5s of no typing, send a stop when compose box closes, either due to sending or closing compose box"
  • We should try to write the frontend code in a way where we can easily write node tests for this feature, since I think basically all the complexity that we'll want tests for will be the frontend state machine.
  • I think a technical design goal for this is to minimize the total event system traffic we generate from typing notifications, since this will be several times the traffic level due to actual sending messages and thus one of our more expensive things (right after heartbeat!). So we should be thoughtful about how to debounce things effectively, while still correctly handling the case that a browser loses network.
  • To handle browsers closing / losing network, we probably will want to mark a user as no longer typing if we haven't seen an update on that user's typing status in a fairly short time (e.g. 15s). An implication of this is that a user who is continuously typing needs to be sending "still typing" events that go out to other browsers with a frequency of e.g. <=10s.
@arpith
Contributor
arpith commented Oct 10, 2016 edited

A few thoughts - according to http://www.businessinsider.com/the-imessage-dots-explained-2016-1

  1. iMessage shows the indicator only after the first message is sent, and
  2. keeps the indicator for 60 seconds after the typing stops (including when the sender's phone is locked, etc)

These numbers can be changed, of course, in the following example they are:

  1. Thread activity: 10 minutes
  2. User activity (sent to server): 1 minute + 1 minute in case a message doesn't get through
  3. Typing indicator updates (requested from server): 30 seconds

Clients can make a GET request to (say) /users_typing/realm/thread_id once every thirty seconds based on whether a message was sent (either way) in this thread in the last ten minutes. The response will be a list of users that are currently typing.

This way clients can control how much traffic comes their way (one request every 30 seconds), and the typing indicators are not too incorrect (30 seconds seems reasonable, but this can be changed) or too unimportant (10 minutes is probably enough to say the conversation is not really active). Perhaps this could even be coupled with the heartbeat in some way.

On the side of the client that has a user typing, perhaps a dead man's switch of sorts could be implemented, with a POST request to /users_typing/realm/thread_id every 60 seconds (as long as there is text in the compose field). The server can drop the user if it does not receive a request in (let's say) two minutes. A DELETE request can be sent if the user clears the compose field (or changes the thread to which he is sending the message).

On the server, this can be implemented by having a Redis sorted set with the key namespaced by realm and thread_id whose members are user_ids scored by the timestamp of the last update from the user.

  1. When a POST request is made, the user (and timestamp) can be added to the relevant sorted set, and then the sorted set can be trimmed (either by score or number of members). If the user is already in this sorted set, the score will be updated to the new timestamp.
  2. When a GET request is made, ZREVRANGEBYSCORE can be used (with MIN set to two minutes before the current time) to obtain the relevant users.
  3. If a DELETE request is made, the user can be removed from the relevant sorted set.

The server will also have to check that the user making the request belongs to the realm and thread.

Disadvantages to this implementation:

  1. If the DELETE request doesn't reach the server but a subsequent POST request does, users in two different threads will see this particular user typing ☹️

Misc notes:

  • The additional minute (to account for missing POST requests) is to prevent the annoying possibility of a user seeming to type for a minute, and then not type for a bit, and then send a huge message 😆
@timabbott
Member

I think we can do this with far less work than your proposal if we take the basically stateless server approach I described, where clients subscribe to typing notifications the same way they subscribe to things like new message notifications (/api/v1/register), and when a client starts typing, that client sends a POST request to e.g. /api/v1/typing containing in the body the recipients (probably in the same format as we specify recipients when sending messages, so we can share the parsing logic), and the server uses our server -> client push system (i.e. send_event()) to send that information to all browser clients for the users who would receive the message. And we send another event if the user stops typing, probably with the same API call.

We should just make the various time threshholds configurable; I think a minute is way too long for out-of-date typing notification state (I'm thinking like 5-10s)), but the important part is to have it be easy to change.

@timabbott
Member

I think with that design, the business logic for this feature will be almost entirely in the JS code. It's likely we'll end up having several constants at the top of the JS code for this that control how chatty it is:

  • how long before we assume a client has gone away and expire its typing status (e.g. 15s)
  • how frequently "still typing" notifications to extend the expiry happen (needs to be less than the previous, e.g. 10s)
  • how long after someone stops editing in the compose box do we send a "stopped typing" notice (e.g. 10s; not directly related to the rest)

And we can tweak those to get the right balance between quick status updates vs. Tornado traffic. It may be that Tornado traffic is so dominated by the heartbeat traffic that typing notifications aren't actually a material performance nonissue.

@arpith
Contributor
arpith commented Oct 10, 2016

Sounds good 👍 I've put the checklist I'm using here

@arpith
Contributor
arpith commented Oct 21, 2016

The PR for the backend is here: #1985
and the WIP PR for the frontend: #2089

@timabbott timabbott modified the milestone: Zulip roadmap, Old roadmap Nov 18, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment