-
-
Couldn't load subscription status.
- Fork 677
unreadModel: Handle moving messages between topics/streams. #4980
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
unreadModel: Handle moving messages between topics/streams. #4980
Conversation
2106517 to
80401b9
Compare
There was a problem hiding this 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 🙂
src/actionTypes.js
Outdated
| message_ids: number[], | ||
| stream_id: number, | ||
| new_stream_id?: number, |
There was a problem hiding this comment.
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.
src/actionTypes.js
Outdated
| message_id: number, | ||
| message_ids: number[], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/actionTypes.js
Outdated
| edit_timestamp: number, | ||
| message_id: number, | ||
| message_ids: number[], | ||
| stream_id: number, |
There was a problem hiding this comment.
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.)
There was a problem hiding this 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 .
src/unread/unreadModel.js
Outdated
| ) | ||
| .updateIn( | ||
| [action.new_stream_id ?? action.stream_id, action.subject], | ||
| (messages = Immutable.List()) => messages.push(...updated_unreads), |
There was a problem hiding this comment.
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.
src/unread/unreadModel.js
Outdated
| const moved_ids = new Set(action.message_ids); | ||
| const updated_unreads = state |
There was a problem hiding this comment.
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.
src/unread/unreadModel.js
Outdated
| if (updated_unreads.size > 0) { | ||
| return state |
There was a problem hiding this comment.
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.
| 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); | ||
| }); |
There was a problem hiding this comment.
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]]])], |
There was a problem hiding this comment.
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.
|
FYI I've just sent #5155; it might be convenient for that to be reviewed/merged before this. |
80401b9 to
c9a38be
Compare
|
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! |
There was a problem hiding this 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, |
There was a problem hiding this comment.
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;
}There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| test('if topic updated, move unreads', () => { | |
| test('if topic updated (and not stream), move unreads', () => { |
| ])); | ||
| }); | ||
|
|
||
| test('if stream updated, move unreads', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| test('if stream updated, move unreads', () => { | |
| test('if stream updated (and not topic), move unreads', () => { |
| ])); | ||
| }); | ||
|
|
||
| test('if moved to existing topic, ids stay sorted', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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', () => { |
| if ( | ||
| (action.subject === action.orig_subject || action.orig_subject == null) | ||
| && (action.new_stream_id === stream_id || action.new_stream_id == null) | ||
| ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Fixes: zulip#4840 Co-authored-by: Greg Price <[email protected]>
|
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. |
c9a38be to
50ea26e
Compare
Fixes #4840.