-
-
Notifications
You must be signed in to change notification settings - Fork 677
Add UI to view read receipts #5489
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
Conversation
gnprice
left a comment
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 @chrisbobbe!
Generally this code looks good. I appreciate the automatic retry, the loading indicator (rather than false empty list) before data arrives, and the toast notices when things don't work. Small comments below.
The UI also looks fine to me, though perhaps @alya will have comments to add.
src/reactUtils.js
Outdated
| * True just when a given state/prop/etc. has been true continuously for the | ||
| * past `duration`. |
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 communicate something more specific than "just when value has been true" etc.? I.e., is there something that "a … state/prop/etc." is meant to say here?
If not, value is shorter, plus it ties it explicitly to the particular parameter it's meant to refer to.
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.
Oh, right! Makes sense; thanks.
src/reactUtils.js
Outdated
| * True just when a given state/prop/etc. has not changed for the past | ||
| * `duration`. |
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 one is where the reasoning in this commit message applies:
I don't expect this to give reliable results if `value` is something
mutable, like the current value of a ref. Mutating a value doesn't
cause the component to rerender. So, tighten up this bit of the
jsdoc, looking to usePrevious as an example:
Still, I think there's a clearer way to put this. The point is really about the meaning of "changed", more than about the nature of the value parameter; so we can leave "value" alone (for the same reasons as in the previous comment), and then clarify the specific meaning of "changed" in the paragraphs below.
This version already has such a clarification, but we can put it more in the foreground:
- Can make it the first paragraph right after the summary line -- seems appropriate because it's probably the most important caveat that someone might potentially run into as a pitfall if they didn't notice it.
- Could also add a few more words explicitly mentioning that if
valueitself is mutable, then this doesn't detect any mutations within it, if wanting to make it extra clear.
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.
Still, I think there's a clearer way to put this.
Yeah, I agree.
The point is really about the meaning of "changed", more than about the nature of the
valueparameter
I think there's a point about the meaning of "changed", and also a point about the nature of the value parameter. (I don't think my revision captured either point very well, and perhaps neither point needs to be explained in a code comment, I'm not sure. 🙂)
- As you've said, if
valueitself is mutable, then this doesn't detect any mutations within it. - If
valueis derived from something mutable (e.g., the current valuetruefrom a boolean ref), and that thing mutates such that the caller would pass something different (non-===) forvaluethan it did last time, then this might detect the change. In particular, it will detect the change if the Hook is actually called with the newvalue. That might happen on the same render as the change, or on a later render, or never (e.g., the component unmounts or the mutation is reversed before the next render). So passing avaluewith this nature is very risky, and if a caller passed auseRef(…).currentasvalue, I'd be skeptical even if they somehow got results they were satisfied with.
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 that thing mutates such that the caller would pass something different (non-
===) forvaluethan it did last time, then this might detect the change. In particular, it will detect the change if the Hook is actually called with the newvalue.
I see. The point you're making here is that this Hook only notices a change when the Hook is actually called, i.e. during a render -- it doesn't set up any kind of subscription to arrange to notice when something changes in between renders. (In contrast with useState, or react-redux's useSelector.)
Instead, the caller needs to arrange that. Which isn't hard -- it basically comes naturally if the caller follows normal React Hooks idioms. The value should come from something like a prop or useState state or useSelector result; and those should be immutable so that any updates deep inside them are reflected by the top-level object getting replaced with a different object, one that's !== from the old one. The main risk is probably from overusing useRef.
I think that caveat is worth mentioning in the jsdoc here, particularly because this Hook does arrange to notice (and cause a rerender on its own initiative) in a different case, namely when the given time elapses. I'll make a go at it and push another commit to the end of the PR branch.
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'll make a go at it and push another commit to the end of the PR branch.
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.
Yeah exactly; thanks!
| * "Changed" means last render's and this render's `value`s aren't ===. | ||
| */ | ||
| export const useHasStayedTrueForMs = (value: boolean, duration: number): boolean => { | ||
| export function useHasNotChangedForMs(value: mixed, duration: number): boolean { |
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.
Neat!
src/reactUtils.js
Outdated
| // Trigger the effect just if React sees a change in `value`. In other | ||
| // words, just when last render's and this render's `value`s aren't ===. | ||
| value, |
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 the logic here is good, but this comment isn't quite accurate: "just if X" means "if X, and only if X". IOW it means that the thing always happens when X, and also never happens when not X. Here, that would be true if this were the whole dependency array… but there's also duration.
Now, we have that debug assert that duration never actually changes. So it's sensible to take duration changing as a pathological case that we mostly don't worry about. But it does mean the wording here should be a bit different in order for the reader to not say "wait, but that isn't quite right -- so now I wonder what other bugs might be hiding here".
A sufficient fix would be to add a prefix: "Provided duration is constant, trigger the effect just if [… and the rest]".
Or: move this to appear after duration in the list, and then just add the prefix "Otherwise,".
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! Thanks.
| type SuccessResult<TData> = {| | ||
| +data: TData, | ||
| +error: null, | ||
| |}; | ||
| type FailResult = {| | ||
| +data: null, | ||
| +error: mixed, | ||
| |}; | ||
| type Result<TData> = SuccessResult<TData> | FailResult; |
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: blank line between top-level declarations
(Or compact these onto one line each, and then they can go without the blank lines)
| 'z-link': chunks => ( | ||
| <WebLink url={new URL('/help/read-receipts', auth.realm)}> | ||
| {chunks.map(chunk => ( | ||
| <ZulipText>{chunk}</ZulipText> |
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.
Huh, I wonder what the scenario is where this callback gets passed more than one item in chunks.
I guess perhaps if these are nested? So if you have e.g. a <foo>red <bar>thing</bar></foo>, then the foo callback gets (in English) two chunks, one of them a string "red " and one the node returned by the bar callback.
(And in a translation there might be a string after the node instead of the one before, or neither or both.)
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.
Huh, yeah that would make sense.
| getUserProfile, | ||
| updateUserStatus, | ||
| getFileTemporaryUrl, | ||
| getReadReceipts, |
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: this should go in the commit that adds this endpoint-binding
| if (!user) { | ||
| // E.g., data out of sync because we're working outside the event | ||
| // system. Shrug, drop this one. | ||
| return; |
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
| // And it looks like Hermes, which we hope to switch to soon, supports it: | ||
| // https://github.com/facebook/hermes/issues/23#issuecomment-1156832485 | ||
| const userSorter = useCallback( | ||
| (a, b) => Intl.Collator(language).compare(a.full_name, b.full_name), |
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.
Fancy!
src/message/ReadReceiptsScreen.js
Outdated
| const { latestResult, latestSuccessResult } = useFetchedDataWithRefresh(callApiMethod, 15_000); | ||
|
|
||
| // TODO: These vanishing toasts are easy to miss. Instead, show | ||
| // latestResultIsError, isFirstLoadLate, haveStaleData, and onPressUser |
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.
Is onPressUser in this list unintentionally? Or, what would it mean to "show [it] with something more persistent"?
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.
Ah, haha—yeah, that was accidental. I grabbed names from four code stanzas just below this, when I should've grabbed from just the first three.
185e421 to
dcce0e8
Compare
|
Thanks for the prompt review! Revision pushed (which of course I don't expect you to review before you're back from PTO). 🙂 |
Based on this discussion thread: zulip#5489 (comment)
|
Thanks @chrisbobbe for the revisions! The code all looks good. Two ongoing threads above which are now just about comments: @alya, any thoughts to add on the UI? See screenshots in Chris's first comment above: #5489 (comment) |
|
The screenshots look good to me, thanks! Oh, sorry I didn't think of this earlier -- does zulip/zulip#22905 affect the mobile implementation? |
Ah, probably does, yeah. Yep, looking at the web implementation: |
…ipts) We'll use this for zulip#5367, "Make it possible to view read receipts".
…nging Based on this discussion thread: zulip#5489 (comment)
…ForMs We'll use this new, more generic Hook useHasNotChangedForMs later in this series. Keep useHasStayedTrueForMs, but have it call this new Hook. For that change to its implementation, it's helpful that useHasStayedTrueForMs has pretty thorough test coverage.
The docs are spotty on this feature, but it seems powerful and worth a try. We'll use it to put a web link on a single word in a translated message, in the style chosen by the web app.
86ea850 to
adc04bf
Compare
|
Thanks for the review! Revision pushed. |
This makes it possible to view read receipts. Fixes: zulip#5367
|
Thanks! Looks good -- merging. Added a small comment-commit on top, and tweaked a summary line: to have the simpler, more recurring prefix |
adc04bf to
868ba9c
Compare
|
Thanks! |




This lets us view read receipts in the mobile app! 🙂
While working on this, I noticed a few nice-to-haves and orthogonal cleanups, but I've rebased those atop this feature branch to send in a followup PR, to help us concentrate on implementing the feature.
Anyway, in this PR, we:
InitialDataRealmandRealmDataForUpdatehave already been updated for this feature)useFetchedDataWithRefresh, for logic the new view calls for: a somewhat live-updating snapshot of data we fetch outside the event system, and information about staleness/failure to fetchreact-intlfeature that should let us follow the web app in making a translated string where one word is a linkReadReceiptsScreenand add the action-sheet button to open it 🎉Screenshots coming soon.
Fixes: #5367