Start implementing muted users#4678
Conversation
afd652f to
de9515f
Compare
gnprice
left a comment
There was a problem hiding this comment.
Thanks @WesleyAC ! Will be great to have this feature in.
Comments below. I'm also going to go expand our testing style guide with some of this information, and some other parts will feed into the "how to add new data from the server" doc that #4655 (comment) was a draft of.
| describe('EVENT_MUTED_USERS', () => { | ||
| test('update `muted_users` when event comes in', () => { | ||
| const initialState = Immutable.Map([[makeUserId(42), 1618822632]]); | ||
|
|
||
| const action = deepFreeze({ | ||
| type: EVENT_MUTED_USERS, | ||
| muted_users: [ | ||
| { | ||
| id: 42, | ||
| timestamp: 1618822632, | ||
| }, | ||
| { | ||
| id: 1234, | ||
| timestamp: 1618822635, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| const expectedState = Immutable.Map([ | ||
| [makeUserId(42), 1618822632], | ||
| [makeUserId(1234), 1618822635], | ||
| ]); | ||
|
|
||
| const newState = mutedUsersReducer(initialState, action); | ||
|
|
||
| expect(newState).toEqual(expectedState); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
So this is a style that you'll see in a lot of our legacy tests, but I think the verbosity of it isn't helpful. It's possible to write all the same content with a lot less boilerplate; when we do, I think that (a) makes it easier to see what the test is really saying, and (b) makes it easier to scan through all the tests in the file and see what conditions are covered and think about what might not be.
For good examples to follow, you can look at tests written relatively recently by me or Chris. Scanning through git log --stat -p --author 'greg\|cbobbe' -- src/**/*-test.js and looking for newly-written tests (i.e. scanning past where we're just touching an existing test), there's 0a218e0 as a nice though short example. Another good example is src/session/__tests__/sessionReducer-test.js. And a longer example, covering more complexity than we need in this file, is src/unread/__tests__/unreadModel-test.js.
de9515f to
e36f43b
Compare
|
Ok, fixed all of those comments, and also went ahead and pushed the initial version of hiding messages from muted users in the MessageList. Should be ready for another look :) |
|
Ah, a few of my fixes here are in the wrong commit. I'll fix that in a moment. |
e36f43b to
90aa095
Compare
90aa095 to
a021f3c
Compare
| export type MuteState = MuteTuple[]; | ||
|
|
||
| /** A map from user IDs to the Unix timestamp in seconds when they were muted. */ | ||
| export type MutedUsersState = Immutable.Map<UserId, number>; |
There was a problem hiding this comment.
I'm glad to have another case of Immutable.Maps with numeric keys! 🎉 b54d68d should ensure that these get "revived" from storage with their keys as numbers, instead of strings. The data is persisted as JSON so we've had to be careful about that.
| messages: number[], | ||
| |}; | ||
|
|
||
| export type MutedUsersEvent = {| |
There was a problem hiding this comment.
nit: we've had the event types in alphabetical order in this file (same as in the EventTypes enum/class), so let's maintain that
There was a problem hiding this comment.
This file isn't quite in alphabetical order (since Submessage wants to be next to Message, it seems?), but I made it a bit closer :)
There was a problem hiding this comment.
Hmm true! And UserStatusEvent in what seems like a completely arbitrary place. Well, thanks for making it closer 🙂
gnprice
left a comment
There was a problem hiding this comment.
Thanks for the revisions! Comments below, mainly on the later commits.
| test('update `muted_users` when event comes in', () => { | ||
| const initialState = Immutable.Map([[makeUserId(42), 1618822632]]); | ||
|
|
||
| const action = deepFreeze({ | ||
| ...eg.eventMutedUsersActionBase, | ||
| muted_users: [ | ||
| { | ||
| id: makeUserId(42), | ||
| timestamp: 1618822632, | ||
| }, | ||
| { | ||
| id: makeUserId(1234), | ||
| timestamp: 1618822635, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| const expectedState = Immutable.Map([ | ||
| [makeUserId(42), 1618822632], | ||
| [makeUserId(1234), 1618822635], | ||
| ]); | ||
|
|
||
| const newState = mutedUsersReducer(initialState, action); | ||
|
|
||
| expect(newState).toEqual(expectedState); | ||
| }); |
There was a problem hiding this comment.
This is still in a pretty verbose style which is typical of some of our tests, but which I think we can do a lot better than. See the examples I linked here:
#4678 (comment)
There was a problem hiding this comment.
I changed it to look more like src/session/__tests__/sessionReducer-test.js, but that didn't make it much less verbose — this doesn't benefit too much from baseReduxData, since the state isn't a struct.
Let me know if there's a specific way that I could make this less verbose, though.
| .message-muted { | ||
| /* If a message is both brief and muted, we don't want to save the extra | ||
| * space for the avatar, since it's not shown anyways. */ | ||
| padding: 16px; |
There was a problem hiding this comment.
I don't think I follow what this comment is saying about "both brief and muted". Perhaps that'll end up being moot in a final version of this branch? I see this goes away in a later commit in this revision of the branch.
There was a problem hiding this comment.
Yeah, this goes away later.
A "brief" message is one that doesn't show the avatar or username (not a great name, but the one we use). This is so that we don't have the message about the fact that it's muted have a big margin on the left side, the way there normally is when there's no user avatar.
This goes away because I coalesce messages later, so brief messages never have any content in them.
| } | ||
| }); | ||
|
|
||
| const revealMutedMessages = (target: Element) => { |
There was a problem hiding this comment.
This should get a bit of jsdoc to describe what its behavior is supposed to be. It looks like it's fairly subtle.
There was a problem hiding this comment.
Done. Let me know if this needs any more elaboration.
| let messageNode = getMessageNode(target); | ||
| let first_run = true; | ||
| while ( | ||
| messageNode | ||
| && messageNode instanceof Element | ||
| && (messageNode.classList.contains('message-brief') || first_run) | ||
| ) { | ||
| messageNode.setAttribute('data-mute-state', 'shown'); | ||
| messageNode = nextMessage(messageNode); | ||
| first_run = false; | ||
| } |
There was a problem hiding this comment.
The logic controlling this loop is somewhat messy. For example nextMessage should always be returning an Element which is a message, or else null -- so the instanceof Element check and the classList check shouldn't be needed. (Unless there's an intended contrast between message-brief and message-full? If so, that calls for an explanatory comment.)
There was a problem hiding this comment.
Rewrote this as do { } while (), which is much nicer.
The reason for the instanceof Element was because of getMessageNode, which returns Node rather than Element, which I've now fixed.
Not sure what you mean about message-brief vs message-full, the classList check is definitely needed. Hopefully the jsdoc makes it clear what's going on?
There was a problem hiding this comment.
Definitely helpful, thanks!
I'll pick up this thread with a comment on the new version.
| }); | ||
|
|
||
| const revealMutedMessages = (target: Element) => { | ||
| let messageNode = getMessageNode(target); |
There was a problem hiding this comment.
As the jsdoc says, this function is deprecated 🙂 It looks like target.closest('.message-brief') will give you what you're looking for.
There was a problem hiding this comment.
Fixed, and added a note to use that instead in the jsdoc deprecation comment :)
| padding: 0 16px 16px 80px; | ||
| } | ||
|
|
||
| .message-muted { |
There was a problem hiding this comment.
I wonder if message-muted-user would be a clearer name for this class. I keep thinking message-muted sounds like it could apply somehow to a topic mute, which is a pretty different-feeling use case that gets treated differently in the UI.
There was a problem hiding this comment.
This goes away in a later commit, so I'm not too inclined to change it.
b61dd20 to
86440e8
Compare
|
@gnprice should be ready for another review :) |
2e6c799 to
e9298d3
Compare
gnprice
left a comment
There was a problem hiding this comment.
Thanks @WesleyAC for the revision! Comments below.
Because this PR thread is getting unwieldy, I haven't looked at the last few new commits:
f78745b PmConversationList [nfc]: Convert to functional component.
e9298d3 PmConversationList: Hide PMs from muted users.
0267617 contentHtmlFromPieceDescriptors: Switch to object for args.
13e530e messageAsHtml: Translate muted-user text.
I'm also going to go ahead and try merging some of these commits that are ready, in order to simplify things for the rest.
| /** If the given message is muted, show it and all following brief messages. */ | ||
| const revealMutedMessages = (target: Element) => { | ||
| let messageNode = target.closest('.message'); | ||
| if (!messageNode) { | ||
| throw new Error('messageNode is not defined'); | ||
| } | ||
| do { | ||
| messageNode.setAttribute('data-mute-state', 'shown'); | ||
| messageNode = nextMessage(messageNode); | ||
| } while (messageNode && messageNode.classList.contains('message-brief')); | ||
| }; |
There was a problem hiding this comment.
(continuing from #4678 (comment) )
This revision is definitely nicer, thanks!
There's still a similar messageNode instanceof Element at the call site. Can the call site switch to doing the .closest call? I think that would simplify the call site, and also let the if (!messageNode) here go away.
I think I see now what the intention is with the message-brief check. The point is that you want to reveal this message and all the ones after it that are part of the same block -- all the messages up to the next one that, well, that either isn't muted in the first place or has its own bit of UI to reveal it.
Ideally the jsdoc would say something in that direction. As is, the "brief messages" thing covers the "what" of the code's logic, but it leaves the "why" somewhat of a puzzle for the reader to piece together.
There was a problem hiding this comment.
(Also "all following brief messages" reads to me very much like it means all the brief messages that come after this point, in the whole rest of the message list.)
| >`; | ||
| const messageTime = shortTime(new Date(timestamp * 1000), backgroundData.twentyFourHourTime); | ||
|
|
||
| // TODO: i18n |
There was a problem hiding this comment.
Currently this is the same as the status quo as for widgets, which maybe isn't a good excuse, but does make more more comfortable merging it before this is done.
That is certainly fair reasoning! 🙂
I guess in that case, in general, the thing that'd be most helpful up front is to explain your intention when sending the PR, if it isn't made clear by a comment or the commit message. If I notice something missing like this I'll be curious about it; and if there's a TODO comment that's helpful, but I'll still be curious why not do it the correct way from the beginning.
Making these shorter this way makes it easier to scan through a test case and see what it's saying, and to scan through the whole test file to see what's being covered and think about if there's anything else to cover.
This further compacts the tests to make it easy to scan through them all. It also cuts out a lot of tokens like `newState` that didn't convey much meaning.
This further shortens the test cases -- the simplest ones become a single line. It also helps make it clear that there isn't meant to be anything significant in the details of this piece of test data.
This helps make it explicit that there's nothing special about these particular user IDs, other than the fact that one appears in certain places in these tests and the other appears in certain other places. It also means that if in the future we were to have any code that expects these user IDs to be the IDs of users that we actually know about -- which is an invariant that it'd be quite reasonable for code to rely on -- these tests will be readily compatible with that because there are already full User objects for these.
Also remove one property that was providing empty data, where any test case is going to want to explicitly specify some data in particular.
Also inline the EVENT_MUTED_USERS example to the one place it's used. Having the data right there explicitly is nice for seeing exactly what's happening, which makes it helpful to do that when it doesn't get repetitious.
We'll use this soon, for implementing the "muted users" feature in what we show in the message list.
We'll use this same styling soon for muted-user messages.
13e530e to
89b8f95
Compare
|
OK, so of these commits: 2d77cfe initialDataTypes: Add MutedUsers. I'm merging versions of these: 2d77cfe initialDataTypes: Add MutedUsers. The merged commits are: 10a07b9 api: Add types for muted users; add muted_users to requested data. Do take a look. The main changes are:
|
89b8f95 to
713f1c6
Compare
|
And merged those commits -- thanks again @WesleyAC! Because this thread has gotten unwieldy for GitHub (with parts of it hidden to keep their page-load numbers good, etc.), let's pick up the rest on a new PR. I've pushed a rebased version to my repo as a branch dev-muted-users -- the commits are: One thing I noticed on the - caughtUp = { older: action.foundOldest, newer: action.foundNewest };
+ caughtUp = {
+ // $FlowIssue[unnecessary-optional-chain]
+ older: state[key]?.older || action.foundOldest,
+ // $FlowIssue[unnecessary-optional-chain]
+ newer: state[key]?.newer || action.foundNewest,
+ };What happens if I believe we'll end up with the right result -- So it'd be good to make the fallback logic here more explicit. I think the logic at the end of const { older: prevOlder, newer: prevNewer } = prevCaughtUp || DEFAULT_CAUGHTUP;
return {
older: prevOlder || caughtUpOlder,
newer: prevNewer || caughtUpNewer,
};Where the jsdoc on /** The value implicitly represented by a missing entry in CaughtUpState. */ |
This event grew some documentation in zulip/zulip@65c400e06, and in that commit it also started sending the server version and feature level. Set up the boilerplate on the pattern Greg started in 979d283 and promoted just a week or two ago, in zulip#4678 (comment).
This event grew some documentation in zulip/zulip@65c400e06, and in that commit it also started sending the server version and feature level. Set up the boilerplate on the pattern Greg started in 979d283 and promoted just a week or two ago, in zulip#4678 (comment).
This event grew some documentation in zulip/zulip@65c400e06, and in that commit it also started sending the server version and feature level. Set up the boilerplate on the pattern Greg started in 979d283 and promoted just a week or two ago, in zulip#4678 (comment).



Related: #4655
Tested by opening the app, and running the following commands in my terminal:
And using the debug console to verify that the correct data got added to the redux state.