Page MenuHomePhabricator

[lib] introduced reactions field to chatMessageItems
ClosedPublic

Authored by ginsu on Dec 5 2022, 5:12 PM.
Tags
None
Referenced Files
Unknown Object (File)
Tue, Nov 12, 6:25 AM
Unknown Object (File)
Mon, Nov 11, 4:37 PM
Unknown Object (File)
Mon, Nov 11, 7:12 AM
Unknown Object (File)
Fri, Nov 8, 5:15 PM
Unknown Object (File)
Fri, Nov 8, 5:15 PM
Unknown Object (File)
Fri, Nov 8, 5:15 PM
Unknown Object (File)
Fri, Nov 8, 5:15 PM
Unknown Object (File)
Fri, Nov 8, 5:15 PM
Subscribers

Details

Summary

introduced reactions to chatMessageItems in the chat selectors. To handle and merge reactions into chatMessageItems, I created a map to hold all reactions messages, with the key being the targetMessageID and the values being another Map, and this map has the different types of reactions ('👍', '❤️', '😆') as the key, and some information about who used which reaction for that target message.

To populate this reaction map, I first filtered out all nonreaction messages

Then the first check is to see if the targetMessageID of the reaction message already exists in targetMessageReactionsMap . If it doesn't, then we then create a new reactsMap and a new usersSet (which holds the users who have reacted to the message), add the creator of the reaction message into the usersSet and package that along with viewerCreated to create messageReactionInfo. We then set the reaction string ('👍') as the key to reactsMap and set the value as the newly created messageReactionInfo. We lastly set the targetMessageID as a key of targetMessageReactionsMap and reactsMap as the value to the key.

The next check is to see if the target message has the reaction already. If it doesn't we do the same steps above minus the last step of setting a new key into targetMessageReactionsMap, since it already exists.

If the reaction message passes all of those check it then lastly checks if the reaction message action is add_reaction or remove_reaction. If it is add_reaction, then we add the creator of the reaction message to the users set (described above) and it also checks if the creator is a viewer and switches viewerReacted accordingly.

If the reaction message action is remove_reaction then we remove the creator of the reaction message from the users set, and checks if the creator is a viewer and switches viewerReacted accordingly.

Once we have all the reaction messages into targetMessageReactionsMap, we then want to merge it into ChatMessageInfoItem and RobotextChatMessageInfoItem. To make this merge happen we check to see if the message ID for a nonreaction message is a targetMessageID for a reaction message using targetMessageReactionsMap. If it isn't we return an empty Map of reactions, but if it is, we return the fetched map of reactions using the message id key.

We also want to make sure that each reaction in the map of reactions has at least one user reacting to it. If it doesn't we delete that reaction from the map of reactions.


Depends on D5782
Linear Task: ENG-2252

Test Plan

Will be doing more tests in subsequent diffs. Ran mobile/web app locally and nothing crashes. I also was able to get to the point where I could add and remove a reaction and it would render on my local stack (see demo video below)

Diff Detail

Repository
rCOMM Comm
Lint
No Lint Coverage
Unit
No Test Coverage

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
lib/selectors/chat-selectors.js
310 ↗(On Diff #19151)

Depending on the use case, it might be worth considering to further process this into something more efficient. On the client side we would probably need a count for each reaction and some indication if a viewer also reacted. But we can also do this optimization later. (one case where it might be really beneficial is when someone liked and unliked a message huge number of times. In the current approach, the resulting array will be long, but in the proposed one it will just be a number 0 or 1).

318–322 ↗(On Diff #19151)

It is a really minor thing, but you can avoid having to specify the inserted item twice

This revision now requires changes to proceed.Dec 6 2022, 6:33 AM
lib/selectors/chat-selectors.js
414 ↗(On Diff #19151)

Always reacting to the source message makes sense to me!

thanks for this feedback @tomek! this is really helpful, and I am working on improving the current solution, hoping to get it resubmitted later tonight/tomorrow morning

lib/selectors/chat-selectors.js
381 ↗(On Diff #19151)

Just put up D5853 and this should address these concerns!

changed reactions type from $ReadOnlyArray<ReactionMessageInfo> to Map<string, MessageReactionInfo>

ginsu edited the test plan for this revision. (Show Details)
lib/selectors/chat-selectors.js
414 ↗(On Diff #19151)

I took a stab at this, but I feel like is out of the scope of this diff. This is still a great point though, and I will create a follow up task to address this

tomek requested changes to this revision.Dec 13 2022, 4:24 AM

We're close - thank for making this extensive refactoring!

lib/selectors/chat-selectors.js
275 ↗(On Diff #19297)

Can we use $ReadOnlyMap?

291 ↗(On Diff #19297)

Can we use readonly?

292 ↗(On Diff #19297)

$ReadOnlySet

329 ↗(On Diff #19297)

Why do we store the whole object, as a string, instead of just the id of a user?

369–371 ↗(On Diff #19297)

Just an idea: would it be simpler if we completely avoided determining viewerReacted here and only check when rendering if users contains viewerId?

433 ↗(On Diff #19297)

If we're using an optional here, maybe it is safer to not compare to 0

This revision now requires changes to proceed.Dec 13 2022, 4:24 AM
lib/selectors/chat-selectors.js
420–436 ↗(On Diff #19297)

It might be cleaner to use an IIFE for this, eg.

const renderedReactions: $ReadOnlyMap<string, MessageReactionInfo> = (() => {
  let result;
  ...
  return result;
})();

That way we group all the code that sets up renderedReactions in one place, and the reader knows we aren't changing it later

lib/selectors/chat-selectors.js
329 ↗(On Diff #19297)

Right now, we are only showing how many users like a message, so we could just add the id of the user; however, I was trying to think a couple of steps ahead, and I know that we eventually will want to show some information about the user like the name for example. So it made sense to me to store the whole object into userSet so that when we are ready to work on showing that type of information, we have it readily available.

We also want to store the object as a string, because, from what I understand, it is not possible to remove the user object from the set, because they won't have the same reference, and converting the object into the string will bypass this issue. More context here.

369–371 ↗(On Diff #19297)

From what I understand, if we are just storing the user ids in the usersSet then using viewerId when rendering would be more simple, but since we have more data about the user than just the user id, I believe what we have right now would be more simple. However, if you think I am missing something please let me know!

tomek requested changes to this revision.Dec 14 2022, 6:43 AM
tomek added inline comments.
lib/selectors/chat-selectors.js
329 ↗(On Diff #19297)

Ok, thanks for the explanation! Overall I think it is a great idea to think ahead, but I don't think that this solution is the best. We can expect a lot of reactions and serializing user info in each step might become expensive. Also, to use it, we would need to deserialize it, so some invariants might be necessary.

I think we should find a solution where serialization / deserialization isn't required. Agree that storing objects doesn't work well with set, so using this approach directly won't work.

What might work is creating a map from user id to user info, storing a set with only ids here, and then later in this function replacing user ids with user infos by using the map. What do you think about it? A possible implementation might fill the map (userId -> creator) while iterating through the messages.

369–371 ↗(On Diff #19297)

Ok, up to you. Both approaches might work well.

This revision now requires changes to proceed.Dec 14 2022, 6:43 AM
lib/selectors/chat-selectors.js
329 ↗(On Diff #19297)

I like that idea a lot, definitely makes sense and avoids the serialization/deserialization concern. For now, since we are only showing the number of likes for the first version of message likes, I am just going to store the user id into the set, but will create a follow-up task for creating the (userID -> creator) map for when we are ready to show more user info on who reacted to a message

369–371 ↗(On Diff #19297)

Decided to go with your initial suggestion of using viewerID. Thanks for the suggestion, definitely thought that it made the overall solution more elegant!

tomek requested changes to this revision.Dec 15 2022, 4:49 AM

Looks great and we're really close! Just one suggestion inline that will reduce the duplication of logic significantly.

lib/selectors/chat-selectors.js
331–354 ↗(On Diff #19350)

You can simplify this code even further. Currently the approach can be summarized as:

  1. Check if there's a reaction - if there's not, handle adding collections and a new user to a set and then start new iteration
  2. Check if there's user set - if there's not, create it and add user, then start new iteration
  3. Add / remove user from an existing set

Which is more or less equal to:

  1. Check if condition A is matched - if not do A, B and C and continue
  2. Check if condition B is matched - if not do B and C and continue
  3. Do C

So C is repeated thrice and B is repeated twice.

In this case we can modify the code according to the pseudocode

if (targetMessageReactionsMap doesn't contain messageInfo.targetMessageID) {
  insert new map into targetMessageReactionsMap at messageInfo.targetMessageID
}
take messageInfo.targetMessageID from targetMessageReactionsMap
if the result doesn't have a reaction {
  insert new set for a reaction into it
}
take a set of users from the map
add or remove a user from it
This revision now requires changes to proceed.Dec 15 2022, 4:49 AM
atul requested changes to this revision.Dec 15 2022, 11:23 AM

couple nits

lib/selectors/chat-selectors.js
317 ↗(On Diff #19395)

Do we need to iterate backwards on this? It looks like the only time we're using i is to index into messages.

Could we instead do

for (const messageInfo of messages) {
...
}

?

412–419 ↗(On Diff #19395)

Can we reduce indentation here with an early continue

437 ↗(On Diff #19395)

Was this newline intentional? Feel free to leave in if it was.

This revision now requires changes to proceed.Dec 15 2022, 11:23 AM
lib/selectors/chat-selectors.js
317 ↗(On Diff #19395)

We need to iterate backwards here because if we do not, the order is from most recent to oldest, and we could run into the case where the most recent reaction message has the remove_reaction as its action. The code would then try to remove a user that hasn't been put into the usersSet already, essentially just skipping that unlike

lib/selectors/chat-selectors.js
317 ↗(On Diff #19395)

Could be good to add a code comment explaining that

add a comment explaining why we need to iterate backwards

lib/selectors/chat-selectors.js
317 ↗(On Diff #19395)

We need to iterate backwards here because if we do not, the order is from most recent to oldest, and we could run into the case where the most recent reaction message has the remove_reaction as its action. The code would then try to remove a user that hasn't been put into the usersSet already, essentially just skipping that unlike

ohhhh, yeah totally makes sense

lib/selectors/chat-selectors.js
414 ↗(On Diff #19151)

My feedback was addressed, would be good to clean up comment and get it signed off on by O6

lib/selectors/chat-selectors.js
317–321 ↗(On Diff #19437)

I think it'd be good to get the language cleared up here and have it signed off on by O6

This revision is now accepted and ready to land.Dec 18 2022, 9:07 PM
lib/selectors/chat-selectors.js
317–321 ↗(On Diff #19437)

O6 is not for code comments, it's for externally-facing documentation

lib/selectors/chat-selectors.js
317–321 ↗(On Diff #19437)

Just figured they're generally the experts on the English language and would be best suited to review code comments as well.

I don't think we need an extensive review process for code comments... both you and @ginsu are native speakers, I'm sure whatever you come up with is fine

// We need to iterate backwards to put the order of messages in chronological
// order, starting with the oldest. This helps to avoid an edge case where
// the most recent message with the remove_reaction action may try to remove
// a user that hasn't been added to the usersSet, causing it to be skipped.

Cut down the comment to this, let me know if this needs to be shorter/more clear

updated comment after sync with atul