-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Fix "New" marker for the chat #1977
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
|
Mobile is being annoying. I'll update the screenshots when the emulators to behave again! |
|
It's a bit more complex, but from a very high level I think the ideal functionality for the unread indicator would be slightly different:
Curious what other people think. |
I'm not sure I fully understand this feature tbh, but I think having it disappear after 3 seconds should not happen. When you navigate to an unread chat then we should bring you to the "New" indicator and the chat itself would then be marked as "read" - but we wouldn't want to remove the context about which comment was read last (since that's useful to you, the person who is trying to catch up on a thread). If you want to mark the chat as unread again from a certain point in time / comment then we can move the indicator to that position. If you navigate away from the chat and navigate back to it then we'd mark the report as "read" again, but not necessarily remove the indicator since you still might want to know where you last left off with the chat. So, if you think about the problem this way "read" or "unread" becomes more of a boolean concept and the position of the indicator is more of a bookmark that tells you what you read last. |
😆 I am somewhere in between. I am for having the |
|
I'm going OOO and need to bow-out of this review. Sorry! |
No problem. Thanks, @tgolen! |
|
@roryabraham, @marcaaron bump! |
roryabraham
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.
marcaaron
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.
Left some questions and suggestions
| // received while the report is still active | ||
| this.shouldShowUnreadActionIndicator = true; | ||
| this.unreadTimer = null; | ||
| this.newMessageMarkerPosition = -1; |
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.
Why are we setting this to -1?
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.
Just initializing it to a non-valid position so we know is has yet to be set. I could initialize it to null, probably, if you like that better.
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.
Hmm... I'm not entirely sold we need this variable yet. But if we are making -1 have a special meaning then maybe we should use a constant like NON_VALID_POSITION.
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'm ok with that. If we keep the variable, we can do that :)
|
|
||
| this.setUpUnreadActionIndicator(); | ||
| if (this.newMessageMarkerPosition < 0) { | ||
| this.newMessageMarkerPosition = this.props.report.unreadActionCount === 0 |
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 wonder if we really need this variable, but correct me if this sounds wrong...
Since the newMessageMarkerPosition can always be inferred by looking at props we can instead make a method that calculates the marker position + tells us if a current sequenceNumber matches? I think it would be cleaner to call a method in renderItem that does this so that the logic isn't all over the place.
I think that will eliminate the need to "reset" this variable back to -1, but haven't tested this out.
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.
newMessageMarkerPosition can't really be always inferred by looking at props, right? Once we update the unreadActionCount (which gets updated when we update the last action), we'll lose that info.
I've been trying to get rid of this variable, but nothing I could think of really worked (for a "permanent" marker), so I'm down for any suggestions.
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.
It seems there are really only two things we need to know, yet we are expressing it in a single variable (and also updating it in a render method). This makes everything a little harder to understand IMO. Hopefully this helps think about the problem in a different way.
We need to know...
- Whether we should show a new message marker at all. something like
shouldShowNewMessageMarkerstored in state could tell us this. When would we need to hide and show this marker? - If we should show the new message marker what sequenceNumber should we show it at? The easier question, as this can always be known by looking at the props like we are already doing here.
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.
- Yep, we can totally have that piece of data in a different variable or in the state if we want.
- Nope. That's the issue I've been fighting with.
this.props.report.unreadActionCountgets updated to zero after the 3 seconds timer ends. At that point, if we haven't saved the sequence in a variable, we lose it. When a new message comes in and the component gets rerendered, we'll lose the marker.
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.
Sorry for all the back and forth here. I'm having a hard time understanding things, but I think turning a corner on a real suggestion. 😅
we can totally have that piece of data in a different variable or in the state if we want
Ok I think actually such a variable is not necessary, but it's an interesting exercise to think about when it might be true.
I think it'd be true when our unreadActionCount > 0 (you've got that part), but we need to use the unread count when the report view mounts (set in the constructor) or we switch to a new report.
This is where things get unclear for me...
this.props.report.unreadActionCount gets updated to zero after the 3 seconds timer ends. At that point, if we haven't saved the sequence in a variable, we lose it. When a new message comes in and the component gets rerendered, we'll lose the marker.
Right now we are setting this to -1 when the reportID changes and then updating it in the render() method when it's less than 0. This seems like kind of an anti-pattern because we're using -1 as a "signal" to the render() method that it needs to save this reference. That's not really the job of the render method and we should be able to save this reference when the component mounts or when the reportID changes.
So we should be able to end up with something like:
- Set
this.initialUnreadActionCountincomponentDidMountand when we switch to a new report (reset()) - Do the same checks we already have in
renderItem()
I think that would clean things up a bit. Does that sound like it might work?
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.
Haha, no worries! I also thought this would be simpler than it ended up being. This didn't work for a couple of reasons:
- I can't get rid of the
-1check (unless we use a different variable):
When we swap reports,componentDidUpdateis actually called twice. Once withprevProps.reportID !== newProps.reportID, which is when we call reset, and once more afterwards to actually update the report contents.
On the first call, we still don't have the correctunreadActionCount, so I can't set the variable at that point. We need to wait till the second round, so I still need to check that "set this only once" flag (the-1, in this case). - I still tried moving the logic to
componentDidUpdateso it wouldn't be inrender, but since this change does't trigger a rerender, even though the variable gets set correctly, the marker doesn't show up until something else happens. Maybe I could force a redraw, but we probably want to avoid that. 😢
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.
Ok, thanks for explaining. It seems clear what is happening here is that there is one update for the reportID then another update when withOnyx re-fetches anything with keys depending on props (so reportActions and report in this case).
Seems like we have two options...
- Implement this workaround for now (maybe try to add a lot of comments to explain why we need to do what we're doing)
- Fix Onyx so that we don't run into this situation again in the future
What would you prefer to do?
My expectation is that if a key depends on a prop and the prop it depends on changes we should check to see if we can also batch additional updates together instead of causing an extra re-render. I'm going to start looking into a solution on the Onyx side regardless and if we want to hold off for that, cool.
Otherwise, we can go ahead with the workaround, but here's what I'd suggest to make everything a little clearer...
- In
reset()set a flag on an instance variable calledthis.didReportIDChangetotrue - In
componentDidUpdate()check to see if this is equal totrue - Create a method called
this.getInitialUnreadCount()and use is to initialize a variable in statethis.state.initialUnreadActionCountin the constructor - In
componentDidUpdate()check forthis.didReportIDChangeand usegetInitialUnreadCount()to update the value ofthis.state.initialUnreadActionCountand setthis.didReportIDChangeback tofalse.
This will "force" an update. But I can't say whether that is a problem or not. I'd think maybe not.
|
All comments addressed! |
roryabraham
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.
| // received while the report is still active | ||
| this.shouldShowUnreadActionIndicator = true; | ||
| this.unreadTimer = null; | ||
| this.newMessageMarkerPosition = -1; |
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.
Hmm... I'm not entirely sold we need this variable yet. But if we are making -1 have a special meaning then maybe we should use a constant like NON_VALID_POSITION.
|
|
||
| this.setUpUnreadActionIndicator(); | ||
| if (this.newMessageMarkerPosition < 0) { | ||
| this.newMessageMarkerPosition = this.props.report.unreadActionCount === 0 |
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.
It seems there are really only two things we need to know, yet we are expressing it in a single variable (and also updating it in a render method). This makes everything a little harder to understand IMO. Hopefully this helps think about the problem in a different way.
We need to know...
- Whether we should show a new message marker at all. something like
shouldShowNewMessageMarkerstored in state could tell us this. When would we need to hide and show this marker? - If we should show the new message marker what sequenceNumber should we show it at? The easier question, as this can always be known by looking at the props like we are already doing here.
|
Comments addressed! |
| if (Visibility.isVisible()) { | ||
| this.timers.push(setTimeout(this.recordMaxAction, 3000)); | ||
| // If we are adding a new action while the report is open, record the max action immediately | ||
| if (Visibility.isVisible() && prevProps.reportID === this.props.reportID && !this.unreadTimer) { |
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.
It doesn't seem like it's possible to have anything else besides
prevProps.reportID === this.props.reportIDsince we return early above when these are not equal.
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.
Why only when !this.unreadTimer? Does it have some special significance?
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.
What if we record the last action anyway. Would something bad happen?
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.unreadTimer: If we have a non-selected chat with new messages and move to it, the timer triggers (3s). This prevents the last action from being recorded before that timer ends (so always wait those three seconds, even if we get a new message). Otherwise, since the last sequence is different when we swap, we'd set the lastAction immediately, without waiting for the timer.- Not sure what you mean
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 feels like a code smell to me. Probably if all the props updated at once we would not have this problem.
| }).start(); | ||
| }, 3000)); | ||
| scheduledRecordMaxAction() { | ||
| // Always cancel the existing timer |
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.
What happens if we do not cancel this timer ?
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.
Related to the answer above: I get rid of it so I could check for existence accurately (There is no "is there a timer running?" function)
|
|
||
| this.setUpUnreadActionIndicator(); | ||
| if (this.newMessageMarkerPosition < 0) { | ||
| this.newMessageMarkerPosition = this.props.report.unreadActionCount === 0 |
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.
Sorry for all the back and forth here. I'm having a hard time understanding things, but I think turning a corner on a real suggestion. 😅
we can totally have that piece of data in a different variable or in the state if we want
Ok I think actually such a variable is not necessary, but it's an interesting exercise to think about when it might be true.
I think it'd be true when our unreadActionCount > 0 (you've got that part), but we need to use the unread count when the report view mounts (set in the constructor) or we switch to a new report.
This is where things get unclear for me...
this.props.report.unreadActionCount gets updated to zero after the 3 seconds timer ends. At that point, if we haven't saved the sequence in a variable, we lose it. When a new message comes in and the component gets rerendered, we'll lose the marker.
Right now we are setting this to -1 when the reportID changes and then updating it in the render() method when it's less than 0. This seems like kind of an anti-pattern because we're using -1 as a "signal" to the render() method that it needs to save this reference. That's not really the job of the render method and we should be able to save this reference when the component mounts or when the reportID changes.
So we should be able to end up with something like:
- Set
this.initialUnreadActionCountincomponentDidMountand when we switch to a new report (reset()) - Do the same checks we already have in
renderItem()
I think that would clean things up a bit. Does that sound like it might work?
|
Comments answered! |
|
Heads up! I'll be OOO next week, so if we can't get this done today/tomorrow, it'll have to wait for a bit |
|
Hey @marcaaron, @roryabraham, I'll be OOO next week on vacation. If either of you wants to give this the final push, feel free to do so! Otherwise, I'll jump back into it when I'm back! |
|
Sorry @Gonals we stalled a bit on this one. There's an open PR that will make it a lot easier to tell when a report/chat has mounted (and therefore when we should record an initial |


@tgolen, @roryabraham, @marcaaron
Details
The "New" marker has been modified so that it no longer disappears after a few seconds. It will behave like Slack's marker, which remains until the user leaves the chat. It will also display in front of other elements, instead of half-hidden as it is now.
I tried to avoid relying on local variables and, instead, show/hide depending exclusively on
report.unreadActionCount, but that basically caused it to hide after three seconds (sincereport.unreadActionCountgets updated)Fixed Issues
Fixes https://github.com/Expensify/Expensify/issues/157517Tests
newmarker above the message. It should not disappear on its own:
4. After a few seconds, go back to chat 2 and then to chat 1 again. The marker should be gone. 5. If several messages come in while you are not in the chat, the marker should be above the oldest of them:Tested On
Screenshots
Web
Mobile Web
Desktop
iOS
Android