mute: Request and consume new user_topic format in API; adapt our model#5382
mute: Request and consume new user_topic format in API; adapt our model#5382gnprice merged 5 commits intozulip:mainfrom
Conversation
Following Greg's example of defining an enum in zulip#5382.
Following Greg's example of defining an enum in zulip#5382.
ae93f12 to
056ea85
Compare
There was a problem hiding this comment.
Thanks! This looks good; see a few comments below, including one where I'm not sure what the intended server API quite is.
I'll force-push a rebase, resolving the conflicts, just after posting this review. (I tried doing so before posting, but then went back because I'd written the comments on the old commits.)
I've also posted a review on the server PR, at zulip/zulip#21251 (review).
| // FlowIssue: sad that we end up having to write numeric literals here :-/ | ||
| // But the most important thing to get from the type-checker here is | ||
| // that the ensureUnreachable works -- that ensures that when we add a | ||
| // new possible value, we'll add a case for it here. Couldn't find a | ||
| // cleaner way to write this that still accomplished that. |
There was a problem hiding this comment.
Is this the same issue as #5384 (comment) ?
Should we leave a note about trying Flow enums with the flowlint rule require-explicit-enum-switch-cases, new in Flow 153?
There was a problem hiding this comment.
Is this the same issue as #5384 (comment) ?
Ah, yep. I knew I'd run into that issue recently, went looking and found that other commit 04c5a03, but clearly didn't find the right search because this is what I was actually thinking of 🙂
Should we leave a note about trying Flow enums with the flowlint rule
require-explicit-enum-switch-cases, new in Flow 153?
I think the main thing is just Flow enums themselves -- I believe that's already enough that Flow will point out an error if a switch isn't exhaustive.
Yep, here's a demo:
enum E { A, B };
(e: E) => {
switch (e) {
case E.A: break;
// case E.B: break;
// default: break;
}
}That gives an error at the switch (e):
Incomplete exhaustive check: the member
Bof enumE[1] has not been considered in check ofe. [invalid-exhaustive-check]
If you uncomment either the other case, or the default, the error goes away because now it's exhaustive.
I believe what that flowlint does is just to say also you can't have a default case (without explicitly suppressing the lint rule): you have to name all the enum members, so that when someone adds another member to that enum they definitely get notified of each of the switches on it.
| // new possible value, we'll add a case for it here. Couldn't find a | ||
| // cleaner way to write this that still accomplished that. | ||
| case 0: // UserTopicVisibilityPolicy.None | ||
| return false; |
There was a problem hiding this comment.
Does UserTopicVisibilityPolicy.None quite mean "the topic is not muted", i.e., isTopicMuted(…) should be false?
From the doc in the current revision of zulip/zulip#21251:
0 = None. Used in events to indicate that this topic now follows its default user-topic configuration.
I don't know what that default is or where it's defined. Do you think the doc should do more to help me find that out?
There was a problem hiding this comment.
Yeah, probably that doc can be clearer.
It does indeed mean that the topic is not muted -- or rather that it's not individually muted. If the stream it's part of is muted, then the messages in the topic will get much the same behavior as if the topic were itself muted: they don't show up in unreads or in all-messages.
Rereading, I think the main issue is that "its default user-topic configuration" is not an accurate description. Rather, what that's trying to say is that the topic now follows the default visibility policy that it inherits from the stream. Which currently just means that it behaves as muted or unmuted according to how the user has the stream.
There was a problem hiding this comment.
| @@ -0,0 +1,56 @@ | |||
| /** General helpers to augment Immutable.js. */ | |||
There was a problem hiding this comment.
Just to point it out in case it's helpful (I don't have a strong preference): would you prefer src/immutableUtils.js as a name for this? See discussion: #4683 (comment)
There was a problem hiding this comment.
Sure, thanks for the reminder of that thought.
| export type InitialDataUserTopic = {| | ||
| /** When absent (on older servers), read `muted_topics` instead. */ | ||
| // TODO(server-6.0): Introduced in FL 125 or so. | ||
| +user_topics?: UserTopic[], |
There was a problem hiding this comment.
Could use $ReadOnlyArray here, right?
|
Thanks for the review! Pushed a revision with the two small changes discussed above. |
Following Greg's example of defining an enum in zulip#5382.
Following Greg's example of defining an enum in zulip#5382.
|
And added unit tests. |
|
Also tested manually against the draft backend. Left a comment at zulip/zulip#21251 (review) , but all's well as far as the client code is concerned. |
|
(Just resolved rebase conflicts.) |
chrisbobbe
left a comment
There was a problem hiding this comment.
Thanks! LGTM (with a few small things below); now I guess we wait till the server PR is merged.
|
|
||
| export type UserTopicEvent = {| | ||
| ...EventCommon, | ||
| type: typeof EventTypes.user_topic, |
| user_topics: [makeUserTopic(eg.stream, 'topic', UserTopicVisibilityPolicy.Muted)], | ||
| }); | ||
| const newState = tryGetActiveAccountState(fullReducer(eg.baseReduxState, action)); | ||
| expect(newState).toBeTruthy(); |
There was a problem hiding this comment.
Can the mute-model code (what this file is responsible for testing) cause this newState to be falsy? I wonder if this could just be an invariant, as here: https://github.com/zulip/zulip-mobile/blob/41e50a740/src/__tests__/permissionSelectors-test.js#L41
Here, and in another case below.
There was a problem hiding this comment.
It could. I don't totally love this toBeTruthy solution, because it doesn't narrow the type and so the next line still has to account for the falsy case:
expect(newState).toBeTruthy();
expect(newState && getMute(newState)).toEqual(makeMuteState([[eg.stream, 'topic']]));But I don't love the invariant solution either, here in a test. The Jest expect API is nice because if the assertion is ever wrong, it gives good informative error output that's helpful for debugging. Also invariant requires a message argument, which is a good thing for error cases in application code -- helpful for sorting out what's happening from a Sentry event or whatever, and for clarifying why the author thought it was an invariant -- but feels redundant here.
I'd probably go for invariant if the narrowing is more necessary, like if the subsequent logic that relied on the condition was more complex so that repeatedly accounting for it was more of a pain. If so, I might go for something like invariant(newState, '');, leaving the message empty.
|
I think zulip/zulip#21251 is waiting for your re-review: zulip/zulip#21251 (comment) |
ab0b2d9 to
1d461c7
Compare
|
The new server API having been test-deployed to CZO, I've just rebased, resolving several conflicts, and pushed to your PR branch; please take a look. 🙂 |
chrisbobbe
left a comment
There was a problem hiding this comment.
While rebasing (see my comment above), I found a few comments to make; see below. And we'll still want to sync the feature-level numbers, etc., after the server work is merged.
| // InitialDataMessage | ||
| max_message_id: 100, | ||
|
|
||
| // InitialDataMutedTopics |
There was a problem hiding this comment.
wip mute: Request and consume new user_topic format in API
Commit-message nit:
In this commit, we start requesting the new `user_topic` format for
the data describing the "muted topics" feature.
On new servers, that causes the new format to appear, and the old to
disappear. Describe the API changes, and start consuming the new
format.
This seems to be true of the data as we receive it in the /register response, but we can still get both the old and new shape of event. I don't yet understand why; that doesn't seem to match the doc test-deployed on CZO right now, does it:
Changes: Deprecated in Zulip 6.0 (feature level 134). Starting with this version, clients that explicitly requested the replacement
user_topicevent type when registering their event queue will not receive this legacy event type.
So, could just update with an explicit mention of the /register response, if the API settles on the current behavior re: the events.
There was a problem hiding this comment.
I don't yet understand why
Oh, I think I see: event_types is for explicitly requesting events, and fetch_event_types is for explicitly requesting initial data. We do the initial-data part but leave event_types unspecified, which the server takes as not "explicitly request[ing] the replacement user_topic event type".
| export type InitialDataUserTopic = {| | ||
| /** When absent (on older servers), read `muted_topics` instead. */ | ||
| // TODO(server-6.0): Introduced in FL 125 or so. | ||
| +user_topics?: $ReadOnlyArray<UserTopic>, |
There was a problem hiding this comment.
Here, and in UserTopicEvent, I believe we're asking UserTopic to depart slightly from its jsdoc—
/**
* A user's relationship to a topic; in particular, muting.
*
* Found in the initial data at `user_topics`, and in `user_topic` events:
* https://zulip.com/api/get-events#user_topic
*/
export type UserTopic = {|
+stream_id: number,
+topic_name: string,
+last_updated: number,
+visibility_policy: UserTopicVisibilityPolicyT,
|};—to represent a change to a user's relationship to a topic.
Should we make a UserTopicUpdate type, on the pattern of UserStatusUpdate, for use here and in the event?
It seems that the server data we're modeling with InitialDataUserTopic['user_topics'] is a change from the UserTopic "zero value," which has visibility_policy: UserTopicVisibilityPolicy.None. The API doc doesn't make this change-from-zero semantics explicit, but a clue is that it only lists one of the two values for visibility_policy: the non-"zero" value. From the doc as test-deployed on CZO now (formatting/links lost):
visibility_policy: integer
An integer indicating the user's visibility configuration for the topic.
1 = Muted. Used to record muted topics.
So that could be good to say in the jsdoc.
Currently, the substance of what we'd want as a UserTopicUpdate type would coincide with the current UserTopic type, I think. But if the server wants to send richer data, then they might stop coinciding. E.g., a "user–topic relationship" gains a property "foo" besides "visibility_policy", and the "change to user–topic relationship" payload starts dropping "foo" or "visibility_policy" if the other property changed but it didn't. We see this happening with user statuses.
There was a problem hiding this comment.
Here, and in
UserTopicEvent, I believe we're askingUserTopicto depart slightly from its jsdoc […] to represent a change to a user's relationship to a topic.
Here in the initial data, I don't see it as representing a change in anything: it's telling us what the relationship is now, as of the snapshot represented by this /register response.
In the user_topic event, it is representing a change, but the way it does so is by providing the new state of that relationship.
It seems that the server data we're modeling with
InitialDataUserTopic['user_topics']is a change from theUserTopic"zero value," which hasvisibility_policy: UserTopicVisibilityPolicy.None. The API doc doesn't make this change-from-zero semantics explicit, but a clue is that it only lists one of the two values forvisibility_policy: the non-"zero" value.
I see. Yeah, I still don't think of this as a change, because there's no particular previous period when the value was something else. The value we're being told is the value that it has now, and that it's had since, as far as we know, time immemorial.
But it's true that it could be thought of as a diff, the result of comparing two values. Still, for each individual user-topic relationship, the diff is represented as simply the entire actual state, if it's present at all. So I think this jsdoc is still accurate, and a good succinct description of what a UserTopic should be thought of as.
I'll add a few words at the two places it's used in the API types, to clarify how those respectively use the UserTopic type.
There was a problem hiding this comment.
Currently, the substance of what we'd want as a
UserTopicUpdatetype would coincide with the currentUserTopictype, I think. But if the server wants to send richer data, then they might stop coinciding. E.g., a "user–topic relationship" gains a property "foo" besides "visibility_policy", and the "change to user–topic relationship" payload starts dropping "foo" or "visibility_policy" if the other property changed but it didn't. We see this happening with user statuses.
True. I think we can cross that bridge if/when we come to it. I don't think there are currently any planned features that would add something to a user-topic value other than the visibility policy.
There was a problem hiding this comment.
But it's true that it could be thought of as a diff, the result of comparing two values.
Ah, yeah, I think my message would have been less confusing if I'd used the word "diff" instead of "change". 🙂
787cbab to
e118f33
Compare
|
New revision pushed; now ready for review. |
chrisbobbe
left a comment
There was a problem hiding this comment.
Thanks! LGTM with one tiny comment below; please merge at will.
| /** | ||
| * The "visibility policy" our user has chosen for each topic. | ||
| * | ||
| * See jsdoc of UserTopicVisibilityPolicyT and related items for background. |
There was a problem hiding this comment.
nit: UserTopicVisibilityPolicy (without the T, happily, thanks to Flow enums!)
This just adds type definitions, without changing the types we expect from the server. After this, as the next steps: * we'll migrate the data structure we maintain in our own store to one that can smoothly handle the new API format; * then as a separate step after that, we'll switch to actually consuming the new API format. It's convenient to use this UserTopicVisibilityPolicy type in describing that new data structure's type. Hence we add it early, in this commit, before we actually start requesting the new format (or update our event and initial-data types to reflect doing so).
Ideally we'd rename this to something like `state.userTopic` too. But it's currently a pain to actually successfully rename a state subtree (and not have its ghost return at the old name): zulip#5381. So just keep calling it "mute" for now. Also add tests for the model. Note that we don't have to update any of the existing tests! They use `makeMuteState`, which uses the reducer; so they get the new data structure automatically.
This is a better pattern for reducing import cycles. The explicitness also seems helpful to the reader, though there isn't a big difference.
We'll reuse some of these in other models as we use Immutable more.
In this commit, we start requesting the new `user_topic` format for the data describing the "muted topics" feature. On new servers, that causes the new format to appear, and the old to disappear. Describe the API changes, and start consuming the new format. This should have no user-visible effect -- currently the new format describes exactly the same information as the old. Taking the client plus (a properly behaving) server as one system, this change is therefore NFC. We don't call the change NFC, though, because it does change the client's behavior as witnessed by the server. (This change does switch from stream names to stream IDs in the API... but only for data that we get from the server through the event system, not for data we send or receive in any request outside that system. That means that (unless affected by some server bug) it's always working with a stream name-to-ID mapping that exactly matches what the server had at the time it sent that data. So even if a stream gets renamed concurrently with getting some data here, there's no race and no bug. OTOH when we *change* this at the server on the user's behalf, that does suffer from such a race. But that part of the API was fixed long ago to accept stream IDs; we already have a TODO-server comment, at `api.setTopicMute`, to start relying on that update.) Fixes: zulip#5380
e118f33 to
deab4ad
Compare
|
Thanks for the review! Merging with that fix. |
This implements for mobile the client side of zulip/zulip#21251 / zulip/zulip#21015.
This is a draft PR for two reasons:
I should write some unit tests, and also test these changes manually against the draft backend.But I believe the code is good, and ready for review. The "wip" markers on some commits correspond to TODO items in the commit messages, basically for the two bullet points mentioned above.
Fixes: #5380