Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

outbox: Fix some bugs from Narrow confusion; remove Narrow #4299

Merged
merged 14 commits into from
Nov 6, 2020

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Nov 3, 2020

This grew out of #3889. It follows #4294 by returning to the outbox code, and includes versions of all the changes made in #3889.

Along the way it fixes additional bugs, and the "Replace narrowContains" commit turns out to fix some bugs of its own. (That is, the old implementation turns out to be outright buggy. Meanwhile the new implementation here is from #4294 and well tested, and avoids the bugs in #3889's new implementation.)

Fixes: #4298
Fixes: #4301

@gnprice gnprice added the a-compose/send Compose box, autocomplete, camera/upload, outbox, sending label Nov 3, 2020
@gnprice gnprice requested a review from chrisbobbe November 3, 2020 21:53
Copy link
Contributor

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good; it's great news to have this tidied up! Just one small query, below.

@@ -34,6 +34,7 @@ describe('getMessagesForNarrow', () => {
},
messages,
outbox: [],
realm: eg.realmState({ email: eg.selfUser.email }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is setting realm.email in the state because that value gets passed directly as ownEmail to isMessageInNarrow, having been selected by getOwnEmail (that getOwnEmail use having been added in this same commit).

I do notice that getOwnEmail has this in its JSDoc:

 * Prefer using `getOwnUserId` or `getOwnUser`; see #3764.

(#3764)

It looks like getOwnUser itself uses getOwnEmail, so it's not obviously better, I guess.

But I could imagine that being changed someday; I could see getOwnUser instead using, e.g., getOwnUserId (there's even a comment on getOwnUserId suggesting to do so) and getUserForId.

If such a change were made, it would no longer be useful to just set state.realm.email; we'd want to set state.realm.user_id. Possibly that's totally fine, and a bridge we'll happily cross when we get to it. But I wondered if we might like to consider providing a simple way to grab state.realm from the example data such that it contains all the stuff that's specific to the self-user: there's email, user_id, probably more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I wondered if we might like to consider providing a simple way to grab state.realm from the example data such that it contains all the stuff that's specific to the self-user: there's email, user_id, probably more.

Yeah, I agree -- that would be useful, and we'll want to add something like that. A more "batteries included" example Redux state, one might call it.

The reason I haven't yet gone and done that is that there are some API choices to be made about it:

  • Probably it includes the self-user's email and related properties, as given by eg.selfUser, in state.realm.
  • Probably it also includes eg.selfUser in state.users, and a corresponding account (with eg.realm) in state.accounts.
  • Does it include eg.otherUser in state.users? Also eg.thirdUser? eg.crossRealmBot?
  • There's eg.stream and eg.subscription. Does it include those in state.streams and state.subscriptions?
  • Maybe there's even several different levels of "batteries included" that we want? In addition to probably still the initial state, the thing that's now eg.reduxState.
  • What about when we add new example values along those lines, like the recent thirdUser?
    • If they're included, that potentially changes the behavior of existing tests. If it makes them fail, that's one thing; more insidiously, it could potentially make them pass trivially, even if the code they're meant to test stops behaving the way they're meant to check that it does.
    • If they're left out, then the "batteries included" state comes to include only some batteries, and potentially a less and less predictable subset of them.
    • Probably the right answer is to include new example values, and make a practice of systematically writing tests so that they don't depend on any assumptions that the state doesn't have miscellaneous other data they weren't expecting. That is, if a test makes its own eg.makeStream() call, it can assume that stream isn't in the example state; but it can't make an assumption like "there's only one stream in the state".
      • This requires a bit of work finding a clear pattern to prescribe for how to systematically do that.

This diff (as you saw, there are a good handful of lines like this one) definitely made me think about adding this. 🙂 Not enough that I felt I wanted to do it before shipping this branch, but it moved that feeling closer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not enough that I felt I wanted to do it before shipping this branch, but it moved that feeling closer.

Makes sense, that's fine! So I'd say you can merge this branch at will.

This way the assumption about what `narrow[0].operand` is supposed
to look like is right next to the conditional that ensures it does
look like that.
This eliminates one more place we're mucking around with things like
`narrow[0].operand`, outside of the `narrow` module itself.
Also to deactivated users, if that's a possible thing to do (I'm
not sure whether it is.)

This appears to be only a latent bug, with no user-visible
symptoms.  But: if you send a PM with a cross-realm bot as
recipient, and use the debug console to look at the outbox message
in the state, you find that the display_recipient has an entry like
`{email: "[email protected]", id: -1, full_name: ""}`.

We're apparently relying on the email in the most relevant places
to interpret that value, avoiding visible symptoms... but this would
become a live bug when we transition any of that code to use numeric
IDs, as we've generally been doing.

That bogus data in the ID and name were taken from NULL_USER, in
that fallback over in mapEmailsToUsers.  And that fallback was hit
because we're using the limited `getUsersByEmail` rather than the
comprehensive `getAllUsersByEmail`.  Switch to the latter to fix it.
Now that we're working from the full dataset of all users -- including
deactivated users and cross-realm bots -- nothing good can come in any
case from trying to send a PM to a user who doesn't exist.
There's no way we should be able to get this far if we don't even
have user data about self.

Also make the types more normal.
This fixes a bug affecting sending self-PMs.  The bug's current
symptom is that when you hit send, the outbox message shows in the
message list... with its own sender header with name and avatar, even
when it's right after another message (which necessarily has the same
sender, namely yourself.)  If the send happens quickly, this shows up
mostly as a flicker glitch, where the other messages move briefly up
and then back down, because the message is taller with the sender.

The root issue is that when constructing the display_recipient of an
outbox PM, we were indiscriminately appending the self user after
taking the list of users given in the Narrow value.  Presumably this
was motivated by the fact that display_recipient values for PMs list
all participants including self, while for most PM narrows we leave
out the self user... but for self-PMs, we don't.  The result is that
for self-PMs we were producing a malformed display_recipient,
consisting of the self listed twice.

The observed symptom then comes because in `renderMessages`, we see
the outbox message has a different recipient from the previous one,
and treat it as if we expected to show a recipient bar for it.  (We
don't actually show a recipient bar, but that causes us to show the
sender header.)

Fix the issue by only adding the self user if it's not already in the
list.  While we're here, also sort the recipients in a canonical way,
by ID.

Fixes: zulip#4301
The actual narrow passed to `addToOutbox` comes from
`ComposeBox#getDestinationNarrow`, and is always something reasonable
to send to -- in particular never a whole-stream narrow.
There's no such thing as sending "to the home narrow"; the `narrow`
on an Outbox value is always a specific conversation.
This makes the mess the caller's responsibility -- which will
make it much more straightforward for us to let other callers
pass Outbox values, as we'd like to start doing.
For conversation narrows -- individual topics, and PM conversations
-- this relied completely on our various representations of the
narrow completely agreeing on the order of operators and operands.
For PM conversations it also relied on the set of users identified
(including or excluding self) and the order they're listed in.

This causes several bugs, and exacerbates possible others:

 * We have a number of different ways those users can be ordered in a
   group PM conversation, and each of those means a bug here.  One of
   those is zulip#4298.

 * Until quite recently, we also had longstanding bugs where the set
   of users would vary, and we may well still have more.  This meant
   an additional symptom for each of those bugs.

 * Because we rely on comparing two `JSON.stringify` results for
   equality, we're implicitly assuming that objects we want to
   consider equal always have their properties enumerate in the same
   order.  In particular, we regularly say `operator` first and then
   `operand`; if there's some way we can also end up with `operand`
   first and then `operator`, then this code wouldn't see the
   resulting narrow as equal to narrows it should be equal to.  That
   makes this at least a latent bug, as there's nothing else in the
   code expressing this assumption (even in comments) and it would
   have been easy to break.

This code also had a dumb bug where if the haystack was a whole-stream
narrow, we'd blindly assume the needle was a stream or topic narrow.
If you had an outbox message addressed to a PM conversation, and then
managed to visit a stream whose name looked like your correspondent's
email address (or their addresses, comma-separated), then you'd see
the outbox PM in that stream narrow.

This commit fixes all those issues by replacing the use of this
function with `isMessageInNarrow`, which avoids all the above
assumptions.

More broadly: the question we're actually asking at this function's
one caller is whether a specific *outbox message* belongs in a given
narrow.  It's sort of an accident that the inclusion relation on
*narrows* even mostly corresponds to that question.  Better to use
a function that's explicitly about our actual question.

Fixes: zulip#4298
As the TODO implicitly suggests, the condition that really says what
we mean here is `item.type === private`.

And indeed that is exactly equivalent to the existing code.  This
code gets the Outbox objects to act on from the Redux store at
`state.outbox`.  That's populated entirely by MESSAGE_SEND_START
actions, which are dispatched only from `addToOutbox`.  And those
get their `type` from `extractTypeToAndSubjectFromNarrow`, and store
as `narrow` the same `Narrow` value they passed there.  That will
produce `type: 'private'` just if `isPrivateOrGroupNarrow(narrow)`,
so the latter condition is equivalent to the former.
This was the last place we were using the `narrow` property on
outbox messages, so taking it out will mean we can remove that
property in the next commit.  Which is good because (a) the `Narrow`
type is a great big mess, and the fewer different parts of the code
are using it, the fewer need to be migrated to a cleaned-up version
of it; and (b) it was never an appropriate type to use to describe a
single message's destination.

I don't love the "display recipient" values either, but their
structure is at least less wild than Narrow, and their semantic
range is an appropriate match for "where this individual message
is sent".  And whenever we decide it's that type's turn to get
straightened out, this usage should fit in nicely to be swept up
with all the others, because it comes right after a check of the
display_recipient's friend the `type: 'stream' | 'private'` field.

This commit makes a small change in the data we send to the server
for sending a PM:
 * The old code would leave out the self user, except on self-PMs.
 * This new code will always include the self user.

Both conventions are found in different areas of the server and
webapp.  The new one here matches the display_recipient the server
sends on PMs, and is simpler.  In any event the server doesn't
care, and that's a deliberate API choice: we want to be sure to
make easy things easy when writing bots, and allowing a range of
natural ways to say who to send a message to is a key part of that.

Concretely in the server implementation, since earliest times it
drops dupes and does its own normalization whether to include the
sender, so it's essentially always lived up to that plan.  The
relevant logic lives today (as of zulip/zulip@620e9cbf7) in the
function get_recipient_from_user_profiles; and though it's moved
around and been refactored, its logic is essentially unchanged since
zulip/zulip@f851d9437, in 2012 a few months after Zulip began.
Made possible by the preceding series of changes.
These are only supported beginning with v2.0 of the server, so we can
only do this either conditioned on the server version, or after older
versions are no longer supported.

This isn't something where the old way is deprecated and we're eager
to remove it from a future server version -- to the contrary, we
intend to support stream names and user emails here forever, as part
of keeping easy things easy when writing bots.  So there isn't a lot
to be gained by eagerly switching; the main thing is it'd mitigate a
race where a user we're sending to happens to have changed their email
address right as we're trying to send.
@gnprice gnprice force-pushed the pr-outbox-no-narrow-take2 branch from e1df665 to e47aa00 Compare November 6, 2020 00:26
@gnprice gnprice merged commit e47aa00 into zulip:master Nov 6, 2020
@gnprice
Copy link
Member Author

gnprice commented Nov 6, 2020

Thanks @chrisbobbe for the review! Merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a-compose/send Compose box, autocomplete, camera/upload, outbox, sending
Projects
None yet
2 participants