Skip to content

Conversation

@Gonals
Copy link
Contributor

@Gonals Gonals commented Mar 22, 2021

@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 (since report.unreadActionCount gets updated)

Fixed Issues

Fixes https://github.com/Expensify/Expensify/issues/157517

Tests

  1. Open a chat (chat 1) in the app. Send a message there. No new marker should show up.
  2. Receive a message in the chat. No new marker should show up
  3. Move to a different chat (chat 2) and receive a message in chat 1. Move back to chat 1. You should see the new marker above the message. It should not disappear on its own:

Screen Shot 2021-03-22 at 4 30 14 PM

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:

Screen Shot 2021-03-22 at 4 36 21 PM

Tested On

  • Web
  • Mobile Web
  • Desktop
  • iOS
  • Android

Screenshots

Web

Screen Shot 2021-03-22 at 4 30 14 PM

Mobile Web

Desktop

Screen Shot 2021-03-22 at 4 55 04 PM

iOS

Screen Shot 2021-03-30 at 1 16 28 PM

Android

Screen Shot 2021-03-31 at 12 06 39 PM

@Gonals Gonals requested a review from a team as a code owner March 22, 2021 16:08
@Gonals Gonals self-assigned this Mar 22, 2021
@botify botify requested review from NikkiWines and removed request for a team March 22, 2021 16:08
@Gonals
Copy link
Contributor Author

Gonals commented Mar 22, 2021

Mobile is being annoying. I'll update the screenshots when the emulators to behave again!

@roryabraham
Copy link
Contributor

roryabraham commented Mar 22, 2021

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:

  1. By default, the unread indicator only appears for new chats for 3 seconds, then disappears.
  2. In the case then the Mark Unread action has been used on a report, then (and only then) does the unread indicator persist until the user leaves the report and comes back.

Curious what other people think.

@marcaaron
Copy link
Contributor

By default, the unread indicator only appears for new chats for 3 seconds, then disappears.

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.

@Gonals
Copy link
Contributor Author

Gonals commented Mar 23, 2021

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 new marker be persistent as long as the chat is open. If you leave the chat and then come back to no new messages, no marker should be shown (I believe this is what Slack does right now), but your option would simplify this a lot. I'd just have to store a sequence number in Report.NewMarkerPosition and we'd be good to go 😆

@tgolen tgolen removed their request for review March 24, 2021 21:13
@tgolen
Copy link
Contributor

tgolen commented Mar 24, 2021

I'm going OOO and need to bow-out of this review. Sorry!

@Gonals
Copy link
Contributor Author

Gonals commented Mar 25, 2021

I'm going OOO and need to bow-out of this review. Sorry!

No problem. Thanks, @tgolen!

@Gonals
Copy link
Contributor Author

Gonals commented Mar 26, 2021

@roryabraham, @marcaaron bump!

@roryabraham roryabraham self-requested a review March 26, 2021 18:10
Copy link
Contributor

@roryabraham roryabraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NEW is appearing as cut-off on iOS... maybe it's just not properly centered vertically?

image

Copy link
Contributor

@marcaaron marcaaron left a 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;
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

@Gonals Gonals Mar 29, 2021

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.

Copy link
Contributor

@marcaaron marcaaron Mar 31, 2021

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...

  1. Whether we should show a new message marker at all. something like shouldShowNewMessageMarker stored in state could tell us this. When would we need to hide and show this marker?
  2. 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Yep, we can totally have that piece of data in a different variable or in the state if we want.
  2. Nope. That's the issue I've been fighting with. 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.

Copy link
Contributor

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.initialUnreadActionCount in componentDidMount and 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?

Copy link
Contributor Author

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 -1 check (unless we use a different variable):
    When we swap reports, componentDidUpdate is actually called twice. Once with prevProps.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 correct unreadActionCount, 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 componentDidUpdate so it wouldn't be in render, 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. 😢

Copy link
Contributor

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...

  1. Implement this workaround for now (maybe try to add a lot of comments to explain why we need to do what we're doing)
  2. 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...

  1. In reset() set a flag on an instance variable called this.didReportIDChange to true
  2. In componentDidUpdate() check to see if this is equal to true
  3. Create a method called this.getInitialUnreadCount() and use is to initialize a variable in state this.state.initialUnreadActionCount in the constructor
  4. In componentDidUpdate() check for this.didReportIDChange and use getInitialUnreadCount() to update the value of this.state.initialUnreadActionCount and set this.didReportIDChange back to false.

This will "force" an update. But I can't say whether that is a problem or not. I'd think maybe not.

@Gonals
Copy link
Contributor Author

Gonals commented Mar 29, 2021

The NEW is appearing as cut-off on iOS... maybe it's just not properly centered vertically?

image

I'm fighting with the simulator today. Hopefully I'll get it to work and I'll fix this 😆

@Gonals Gonals requested a review from a team March 30, 2021 09:32
@MelvinBot MelvinBot requested review from Luke9389 and removed request for a team March 30, 2021 09:32
@Gonals Gonals removed the request for review from Luke9389 March 30, 2021 09:33
@Gonals
Copy link
Contributor Author

Gonals commented Mar 30, 2021

The NEW is appearing as cut-off on iOS... maybe it's just not properly centered vertically?
image

I'm fighting with the simulator today. Hopefully I'll get it to work and I'll fix this 😆

Fixed!

@Gonals
Copy link
Contributor Author

Gonals commented Mar 30, 2021

All comments addressed!

Copy link
Contributor

@roryabraham roryabraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New messages received are not appearing as bolded in LHN on iOS. They are bold for like 1 render cycle, then go back to the read state:

image

// received while the report is still active
this.shouldShowUnreadActionIndicator = true;
this.unreadTimer = null;
this.newMessageMarkerPosition = -1;
Copy link
Contributor

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
Copy link
Contributor

@marcaaron marcaaron Mar 31, 2021

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...

  1. Whether we should show a new message marker at all. something like shouldShowNewMessageMarker stored in state could tell us this. When would we need to hide and show this marker?
  2. 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.

@Gonals
Copy link
Contributor Author

Gonals commented Mar 31, 2021

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) {
Copy link
Contributor

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.reportID

since we return early above when these are not equal.

Copy link
Contributor

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?

Copy link
Contributor

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?

Copy link
Contributor Author

@Gonals Gonals Apr 1, 2021

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

Copy link
Contributor

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
Copy link
Contributor

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 ?

Copy link
Contributor Author

@Gonals Gonals Apr 1, 2021

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
Copy link
Contributor

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.initialUnreadActionCount in componentDidMount and 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?

@Gonals
Copy link
Contributor Author

Gonals commented Apr 1, 2021

Comments answered!

@Gonals
Copy link
Contributor Author

Gonals commented Apr 1, 2021

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

@Gonals
Copy link
Contributor Author

Gonals commented Apr 2, 2021

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!

@marcaaron
Copy link
Contributor

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 unreadActionCount. I think it will simplify this logic a lot so maybe we should just hold. cc @roryabraham for his thoughts.

#2221

@marcaaron
Copy link
Contributor

Hey @Gonals I'm gonna close this in favor of #2338 so we can get the "New" indicator working correctly again.

@marcaaron marcaaron closed this Apr 10, 2021
@marcaaron marcaaron deleted the alberto-fixNewMarker branch April 10, 2021 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants