Skip to content

Conversation

@WesleyAC
Copy link
Contributor

Fixes #4840.

@WesleyAC WesleyAC requested review from chrisbobbe and gnprice August 30, 2021 23:32
@WesleyAC WesleyAC force-pushed the unreads-reducer-handle-topic-moves branch from 2106517 to 80401b9 Compare August 30, 2021 23:33
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks! Comments below on the first commit. The second commit is more interesting, and I'll return to it after looking at some other things, but wanted to get these out without waiting 🙂

Comment on lines 347 to 349
message_ids: number[],
stream_id: number,
new_stream_id?: number,
Copy link
Member

Choose a reason for hiding this comment

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

Let's accompany this kind of change by having a comment at the top of the event link to the relevant bit of docs:
https://zulip.com/api/get-events#update_message
That helps a lot in reviewing a change here, or in comparing in the future to the possibly-by-then-further-changed API. It also makes it easy to look up what each of these properties is documented as being supposed to mean.

We mostly don't have those now (in fact it looks like there's just one such example, RestartEvent in eventTypes.js -- which not coincidentally looks like the latest event we've added to our types), mainly because the bulk of these type definitions were written down in the reverse-engineering era, when there were no API docs to link to.

Comment on lines 346 to 347
message_id: number,
message_ids: number[],
Copy link
Member

Choose a reason for hiding this comment

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

I just mentioned linking to the API docs, and I had the thought that that substitutes for writing down any documentation here about what the properties mean... but as I read this event's docs, I see that this message_id vs. message_ids thing is odd enough, and enough of a potential gotcha, that it probably merits making an exception and writing something down here.

In particular, any content changes apply to just message_id, and any stream or topic changes apply to all of message_ids, which is guaranteed to include message_id.

Copy link
Member

Choose a reason for hiding this comment

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

The docs also kind of make it sound like message_ids isn't always present -- emphasis added:

If the stream or topic was changed, the set of moved messages is encoded in the separate message_ids field, which is guaranteed to include message_id.

even though the message_ids bit doesn't say it's optional. That might be good to clarify in chat on #api-design.

Copy link
Contributor

Choose a reason for hiding this comment

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

Confirmed in chat that we do expect it to be always present.

edit_timestamp: number,
message_id: number,
message_ids: number[],
stream_id: number,
Copy link
Member

Choose a reason for hiding this comment

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

This should be optional, according to the docs. (It's there only when the message was sent to a stream.)

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

And read the main commit. The logic generally looks good! Small comments below.

One commit-message nit:

unreadModel: Handle moving messages between topics/streams.

Fixes #4840.

See https://github.com/zulip/zulip-mobile/blob/master/docs/style.md#fixes-format .

)
.updateIn(
[action.new_stream_id ?? action.stream_id, action.subject],
(messages = Immutable.List()) => messages.push(...updated_unreads),
Copy link
Member

Choose a reason for hiding this comment

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

This data structure has an invariant that these lists are sorted (see jsdoc at the type definition, in unreadModelTypes.js), so this should sort.

Comment on lines 209 to 210
const moved_ids = new Set(action.message_ids);
const updated_unreads = state
Copy link
Member

Choose a reason for hiding this comment

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

nit: we use camelCase for internal names (when not C_MACRO_CASE), following the usual convention for JS.

The exceptions are in names that come straight from the server API, like properties on objects that are taken straight from, or modified only lightly from, some JSON the server sent us.

Comment on lines 214 to 215
if (updated_unreads.size > 0) {
return state
Copy link
Member

Choose a reason for hiding this comment

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

These conditionals (this, and the if above) have the feeling that the main line of the code's logic gets nested deeper and deeper as the code goes on. This pattern also has the effect that what happens in the alternative case (here, when updated_unreads is empty) is left in suspense for the reader until after all the other code.

I tend to prefer the "early return" pattern for arranging code flow: exceptional cases like errors and fast-path shortcuts go off to the side and then return, so that the main narrative of the code gets to continue vertically all in the same column. This also means that the condition for each exceptional case, and what we do in that case, get to stay right next to each other.

I can try demonstrating that rearrangement for this code at merge time, or after merge.

Comment on lines 118 to 136
test('if topic updated, but no unreads, return original state', () => {
const state = reducer(baseState, mkAction({ message_id: 100 }), eg.plusReduxState);
expect(state.streams).toBe(baseState.streams);
});
Copy link
Member

Choose a reason for hiding this comment

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

Does this test match its description? It doesn't look like a topic is updated.

// prettier-ignore
expect(summary(state)).toEqual(Immutable.Map([
[123, Immutable.Map([['foo', [1, 2]]])],
[456, Immutable.Map([['zzz', [6, 7]], ['foo', [3, 4, 5]]])],
Copy link
Member

Choose a reason for hiding this comment

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

Let's also have a test case where they get moved to an existing topic (like 456 > 'zzz'), showing that the list gets properly combined with the existing one.

@chrisbobbe
Copy link
Contributor

FYI I've just sent #5155; it might be convenient for that to be reviewed/merged before this.

@gnprice
Copy link
Member

gnprice commented Dec 13, 2021

I've just pushed a revision of this PR, rebasing atop the API types from #5155 and applying my comments above. @chrisbobbe please take a look!

@gnprice gnprice added a-data-sync Zulip's event system, event queues, staleness/liveness a-redux P1 high-priority labels Dec 13, 2021
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.

Thanks! See a few small comments/suggestions below. Let me know if I can be clearer on any of them.

const mkAction = args => {
const {
message_ids,
stream_id = 123,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this default parameter means that mkAction's callers can't request an action that omits stream_id: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters#passing_undefined_vs._other_falsy_values

Whether those callers will end up wanting to do that will depend on whether servers want to convey something relevant by omitting stream_id, I guess. You've written down the state of that question in unreadModel:

      if (stream_id == null) {
        // Not stream messages, or else a pure content edit (no stream/topic change.)
        //
        // As of this writing, the docs actually promise this field for all
        // updates to stream messages.  Empirically it's absent on pure
        // content edits, but present on edits affecting either the stream
        // or topic:
        //   https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60update_message.60.20event/near/1296823
        return state;
      }

Copy link
Member

Choose a reason for hiding this comment

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

True. At present this test file is just for the "streams" part of the unreads data, because that's the part that has been modernized. In the future when we similarly modernize the rest and have its tests in the same place, we'll want to either add a different helper for those tests or modify this one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense. Also, I see from your other comment that we've settled that servers will start always including stream_id for stream messages, so there's no reason for this modernized streams-focused part of the model to be tested with an event that's missing stream_id.

]));
});

test('if topic not updated, return original state', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
test('if topic not updated, return original state', () => {
test('if topic/stream not updated, return original state', () => {

expect(state.streams).toBe(baseState.streams);
});

test('if topic updated, but no unreads, return original state', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
test('if topic updated, but no unreads, return original state', () => {
test('if topic updated (and not stream), but no unreads, return original state', () => {

expect(state.streams).toBe(baseState.streams);
});

test('if topic updated, move unreads', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
test('if topic updated, move unreads', () => {
test('if topic updated (and not stream), move unreads', () => {

]));
});

test('if stream updated, move unreads', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
test('if stream updated, move unreads', () => {
test('if stream updated (and not topic), move unreads', () => {

]));
});

test('if moved to existing topic, ids stay sorted', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
test('if moved to existing topic, ids stay sorted', () => {
test('if stream/topic both updated, and stream/topic thread has existing unreads, ids stay sorted', () => {

Comment on lines +220 to +224
if (
(action.subject === action.orig_subject || action.orig_subject == null)
&& (action.new_stream_id === stream_id || action.new_stream_id == null)
) {
Copy link
Contributor

@chrisbobbe chrisbobbe Dec 13, 2021

Choose a reason for hiding this comment

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

I think a check like this makes a lot of sense, given that we don't have confirmation on what we've observed empirically; see above:

        // As of this writing, the docs actually promise this field for all
        // updates to stream messages.  Empirically it's absent on pure
        // content edits, but present on edits affecting either the stream
        // or topic:
        //   https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60update_message.60.20event/near/1296823

And even if we did, I still like it because it's nice and intuitive, even though I guess it would be kind of belt-and-suspenders, right. We would've already returned before this, on pure content edits, because of the absent stream_id.

Copy link
Member

Choose a reason for hiding this comment

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

Updated that above comment to reflect the further discussion in that thread: we plan to make stream_id present on all updates to stream messages. Then this check will be directly needed in order to skip pure content edits.

@gnprice
Copy link
Member

gnprice commented Dec 16, 2021

Thanks for the review!

I applied some of the test-name suggestions, but left out the "(and not stream)" ones; I think it's generally fine for the test names to highlight the thing that does happen and leave implicit that other things that would be interesting don't happen.

The revision is only in comments and test names, so going ahead and merging.

@gnprice gnprice force-pushed the unreads-reducer-handle-topic-moves branch from c9a38be to 50ea26e Compare December 16, 2021 04:51
@gnprice gnprice merged commit 50ea26e into zulip:main Dec 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a-data-sync Zulip's event system, event queues, staleness/liveness a-redux P1 high-priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unreadModel: respond to message topic/stream edits

3 participants