Thread reactions #3370

Merged
merged 41 commits into from Jun 27, 2018

Conversation

Projects
None yet
3 participants
@uberbryn
Contributor

uberbryn commented Jun 20, 2018

Status

  • WIP
  • Ready for review
  • Needs testing

Deploy after merge (delete what needn't be deployed)

  • api
  • hyperion (frontend)
  • desktop
  • mobile

Run database migrations (delete if no migration was added)
YES

Release notes for users (delete if codebase-only change)

  • Allows users to "like" threads

Related issues (delete if you don't know of any)
Closes #

@spectrum-bot

This comment has been minimized.

Show comment
Hide comment
@spectrum-bot

spectrum-bot bot Jun 20, 2018

Warnings
⚠️

These modified files do not have Flow enabled:

  • mercury/queues/processReputationEvent.js
  • src/views/dashboard/components/threadCommunityInfo.js
  • src/views/notifications/components/notificationDropdownList.js
  • src/views/notifications/utils.js
  • src/views/titlebar/index.js

Generated by 🚫 dangerJS

spectrum-bot bot commented Jun 20, 2018

Warnings
⚠️

These modified files do not have Flow enabled:

  • mercury/queues/processReputationEvent.js
  • src/views/dashboard/components/threadCommunityInfo.js
  • src/views/notifications/components/notificationDropdownList.js
  • src/views/notifications/utils.js
  • src/views/titlebar/index.js

Generated by 🚫 dangerJS

@mxstbr

Overall LGTM, only concern is the Thread.reactions schema as noted below. Let's get something better in there that's not like the Message.reactions one, then I'm good with the backend side of things here apart from the nits!

api/models/threadReaction.js
+import { events } from 'shared/analytics';
+import { trackQueue } from 'shared/bull/queues';
+
+type ReactionType = 'like';

This comment has been minimized.

@mxstbr

mxstbr Jun 26, 2018

Member

nit: this should probably be ThreadReactionType?

@mxstbr

mxstbr Jun 26, 2018

Member

nit: this should probably be ThreadReactionType?

+ },
+ });
+
+ sendThreadReactionNotificationQueue.add({ threadReaction: thisReaction, userId });

This comment has been minimized.

@mxstbr

mxstbr Jun 26, 2018

Member

This is probably not ideal? If I'm reading this correctly, it would mean that if I spam-click the thread reaction button you'll be sent millions of notifications, one for each click. We should only ever send one of 'em?

@mxstbr

mxstbr Jun 26, 2018

Member

This is probably not ideal? If I'm reading this correctly, it would mean that if I spam-click the thread reaction button you'll be sent millions of notifications, one for each click. We should only ever send one of 'em?

This comment has been minimized.

@brianlovin

brianlovin Jun 26, 2018

Member

This is handled in athena. It works by looking up a previous notification of the same type on the same entity in the previous 30mins, then will de-duplicate actors. So the result is that it will mark the notification as "unread" for the thread author, but will not actually insert multiple notifications unless there is a 30 min gap in between.

@brianlovin

brianlovin Jun 26, 2018

Member

This is handled in athena. It works by looking up a previous notification of the same type on the same entity in the previous 30mins, then will de-duplicate actors. So the result is that it will mark the notification as "unread" for the thread author, but will not actually insert multiple notifications unless there is a 30 min gap in between.

api/types/Thread.js
@@ -97,13 +98,24 @@ const Thread = /* GraphQL */ `
filesToUpload: [Upload]
}
+ input AddThreadReactionInput {
+ threadId: ID!
+ type: ReactionTypes

This comment has been minimized.

@mxstbr

mxstbr Jun 26, 2018

Member

nit: should be ThreadReactionTypes

@mxstbr

mxstbr Jun 26, 2018

Member

nit: should be ThreadReactionTypes

+ debug('get thread');
+
+ // make sure that the person who left the original message still has notification permissions in this thread. We have to check the threadtype to determine if the reaction was left in a story thread or a direct message thread
+ // TODO: In the future we'll want reactions in direct message threads to trigger push notifications, but for now it introduces too much complexity so we just say false

This comment has been minimized.

@mxstbr

mxstbr Jun 26, 2018

Member

this todo doesn't make any sense, you can't react to a dm thread?

@mxstbr

mxstbr Jun 26, 2018

Member

this todo doesn't make any sense, you can't react to a dm thread?

+ const thread = await getThreadById(updatedNotification.context.id);
+
+ // make sure that the person who left the original message still has notification permissions in this thread. We have to check the threadtype to determine if the reaction was left in a story thread or a direct message thread
+ // TODO: In the future we'll want reactions in direct message threads to trigger push notifications, but for now it introduces too much complexity so we just say false

This comment has been minimized.

@mxstbr

mxstbr Jun 26, 2018

Member

this todo doesn't make any sense, you can't react to a dm thread?

@mxstbr

mxstbr Jun 26, 2018

Member

this todo doesn't make any sense, you can't react to a dm thread?

api/types/Thread.js
@@ -54,6 +54,7 @@ const Thread = /* GraphQL */ `
attachments: [Attachment]
watercooler: Boolean
currentUserLastSeen: Date @cost(complexity: 1)
+ reactions: ReactionData @cost(complexity: 1)

This comment has been minimized.

@mxstbr

mxstbr Jun 26, 2018

Member

I'm going to veto using the same schema as the message reactions because it doesn't let us show which users liked the thing. (you only see it in the notifications, but nobody else can see/check) Let's change the return type here to include that information somehow, (open to ideas) so we can in the future show who liked a thread or not in the UI.

@mxstbr

mxstbr Jun 26, 2018

Member

I'm going to veto using the same schema as the message reactions because it doesn't let us show which users liked the thing. (you only see it in the notifications, but nobody else can see/check) Let's change the return type here to include that information somehow, (open to ideas) so we can in the future show who liked a thread or not in the UI.

This comment has been minimized.

@brianlovin

brianlovin Jun 26, 2018

Member

Going to change this to be a separate type ThreadReactions which will expose:

count: Int
hasReacted: Boolean
reactors: [User]

Or something like that - we can actually extend the message reactions to have the same data model, but let's keep message reactions and thread reactions as separately defined types in case they diverge in the future.

@brianlovin

brianlovin Jun 26, 2018

Member

Going to change this to be a separate type ThreadReactions which will expose:

count: Int
hasReacted: Boolean
reactors: [User]

Or something like that - we can actually extend the message reactions to have the same data model, but let's keep message reactions and thread reactions as separately defined types in case they diverge in the future.

@brianlovin

This comment has been minimized.

Show comment
Hide comment
@brianlovin

brianlovin Jun 27, 2018

Member

Took a while, but finally got this working. @mxstbr the one trouble I ran into was debouncing the handleScroll method in that last commit - ideally we would debounce or throttle it so we aren't putting a ton of load on the browser just to determine whether or not to show the header (I found a 100ms debounce is probably a good threshold). But I ran into some issues with React removing synthetic events from the event pool. I tried using event.persist() from the docs (https://reactjs.org/docs/events.html) but couldn't get it to work - React kept throwing the error:

This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the method `target` on a released/nullified synthetic event. This is a no-op function. If you must keep the original synthetic event around, use event.persist(). See https://facebook.github.io/react/docs/events.html#event-pooling for more information.

Any chance you could take a look? Without the debounce people's machines are gonna get real real hot.

Member

brianlovin commented Jun 27, 2018

Took a while, but finally got this working. @mxstbr the one trouble I ran into was debouncing the handleScroll method in that last commit - ideally we would debounce or throttle it so we aren't putting a ton of load on the browser just to determine whether or not to show the header (I found a 100ms debounce is probably a good threshold). But I ran into some issues with React removing synthetic events from the event pool. I tried using event.persist() from the docs (https://reactjs.org/docs/events.html) but couldn't get it to work - React kept throwing the error:

This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the method `target` on a released/nullified synthetic event. This is a no-op function. If you must keep the original synthetic event around, use event.persist(). See https://facebook.github.io/react/docs/events.html#event-pooling for more information.

Any chance you could take a look? Without the debounce people's machines are gonna get real real hot.

@brianlovin

This comment has been minimized.

Show comment
Hide comment
@brianlovin

brianlovin Jun 27, 2018

Member

Also @uberbryn @mxstbr - if you want to pull this and try it out, let me know what you think. In my opinion it's very very distracting to have this header jump in and out every time you change the thread in the inbox view. But it seems to be quite a challenge to work around that since we're doing so much scroll position handling every time a thread is changed.

Member

brianlovin commented Jun 27, 2018

Also @uberbryn @mxstbr - if you want to pull this and try it out, let me know what you think. In my opinion it's very very distracting to have this header jump in and out every time you change the thread in the inbox view. But it seems to be quite a challenge to work around that since we're doing so much scroll position handling every time a thread is changed.

@uberbryn

This comment has been minimized.

Show comment
Hide comment
@uberbryn

uberbryn Jun 27, 2018

Contributor

Would be cool for it to be more chill when we jump your scroll position as you mentioned, but I also think it's fine as is - it really wouldn't have bothered me other than that you mentioned it.

Other solution I could think of would be transitioning it from 0 opacity to 1 so the motion isn't jumpy, but I really don't think it's that big of a deal. Would prefer shipping and refining later.

Great work, bud! Thanks for grabbing that!

Contributor

uberbryn commented Jun 27, 2018

Would be cool for it to be more chill when we jump your scroll position as you mentioned, but I also think it's fine as is - it really wouldn't have bothered me other than that you mentioned it.

Other solution I could think of would be transitioning it from 0 opacity to 1 so the motion isn't jumpy, but I really don't think it's that big of a deal. Would prefer shipping and refining later.

Great work, bud! Thanks for grabbing that!

@mxstbr

This comment has been minimized.

Show comment
Hide comment
@mxstbr

mxstbr Jun 27, 2018

Member

Will take a look!

Member

mxstbr commented Jun 27, 2018

Will take a look!

src/views/thread/container.js
+ // enable the `bannerIsVisible` state to slide the thread context banner
+ // in from the top of the screen
+ const scrollOffset = e.target.scrollTop;
+ this.setState({ scrollOffset }, () => {

This comment has been minimized.

@mxstbr

mxstbr Jun 27, 2018

Member

We shouldn't be putting the scroll offset (which changes every single frame you scroll) into the state, this is what's causing the issues. This isn't even necessary, just don't put it into the state, compare scrollOffset against threadDetailHeight and only set the state if it changes. That way you don't even need to debounce which is kinda annoying for this behavior

I'm taking a look at refactoring this!

@mxstbr

mxstbr Jun 27, 2018

Member

We shouldn't be putting the scroll offset (which changes every single frame you scroll) into the state, this is what's causing the issues. This isn't even necessary, just don't put it into the state, compare scrollOffset against threadDetailHeight and only set the state if it changes. That way you don't even need to debounce which is kinda annoying for this behavior

I'm taking a look at refactoring this!

@mxstbr

Needs an optimistic update on the like click—feels bad rn on slow networks/with the remote server overhead. Will also tackle.

Stop storing scroll offset in state unnecessarily, causing excessive …
…re-renders


@brianlovin see how I made it so we only `setState` once the state
actually changes? This means we barely re-render the React side of
things, which makes this much much much more performant! 👌
+ type ThreadReactions {
+ count: Int!
+ hasReacted: Boolean
+ }

This comment has been minimized.

@mxstbr

mxstbr Jun 27, 2018

Member

Even though you split 'em up, this is still the same schema as the message reactions? I thought we agreed that we should do this one right from the start (to one that let's us show who liked a thread) so we wouldn't have to migrate to a different one in the future?

@mxstbr

mxstbr Jun 27, 2018

Member

Even though you split 'em up, this is still the same schema as the message reactions? I thought we agreed that we should do this one right from the start (to one that let's us show who liked a thread) so we wouldn't have to migrate to a different one in the future?

This comment has been minimized.

@brianlovin

brianlovin Jun 27, 2018

Member

Right, see comment below.

@brianlovin

brianlovin Jun 27, 2018

Member

Right, see comment below.

@mxstbr

This comment has been minimized.

Show comment
Hide comment
@mxstbr

mxstbr Jun 27, 2018

Member

Needs an optimistic update on the like click—feels bad rn on slow networks/with the remote server overhead. Will also tackle.

Nevermind, played around with this a bit and it's a pain in the butt. Let's not do this for now 😅 So the last bit here is coming up with a nicer schema that let's us show who liked a thread, right?

Member

mxstbr commented Jun 27, 2018

Needs an optimistic update on the like click—feels bad rn on slow networks/with the remote server overhead. Will also tackle.

Nevermind, played around with this a bit and it's a pain in the butt. Let's not do this for now 😅 So the last bit here is coming up with a nicer schema that let's us show who liked a thread, right?

@brianlovin

This comment has been minimized.

Show comment
Hide comment
@brianlovin

brianlovin Jun 27, 2018

Member

Hm, the schema will be:

type ThreadReactions {
  count: Int!
  hasReacted: Boolean
  users: [User]
}

Right? We just add a field inside that type. users could follow the connection spec, or just be an array of user data (obviously will be problematic when we get to hundreds of reactions on a post). So then it would be:

type ReactionUsersConnection {
  pageInfo: PageInfo
  edges: [ReactionUserEdge]
}

type ReactionUserEdge {
  node: User
  cursor: String
}

type ThreadReactions {
  count: Int!
  hasReacted: Boolean
  users: ReactionUsersConnection
}

But again, all of this can be added later. Querying would not change except adding the field to the graphql query and extracting it on the frontend with thread.reactions.users.edges.map(({ node: user }) => ...)

Member

brianlovin commented Jun 27, 2018

Hm, the schema will be:

type ThreadReactions {
  count: Int!
  hasReacted: Boolean
  users: [User]
}

Right? We just add a field inside that type. users could follow the connection spec, or just be an array of user data (obviously will be problematic when we get to hundreds of reactions on a post). So then it would be:

type ReactionUsersConnection {
  pageInfo: PageInfo
  edges: [ReactionUserEdge]
}

type ReactionUserEdge {
  node: User
  cursor: String
}

type ThreadReactions {
  count: Int!
  hasReacted: Boolean
  users: ReactionUsersConnection
}

But again, all of this can be added later. Querying would not change except adding the field to the graphql query and extracting it on the frontend with thread.reactions.users.edges.map(({ node: user }) => ...)

@brianlovin

This comment has been minimized.

Show comment
Hide comment
@brianlovin

brianlovin Jun 27, 2018

Member

Let me see if I can handle optimistic update real quick

Member

brianlovin commented Jun 27, 2018

Let me see if I can handle optimistic update real quick

@brianlovin

This comment has been minimized.

Show comment
Hide comment
@brianlovin

brianlovin Jun 27, 2018

Member

Optimistic updating is pushed, @mxstbr this is ready for a final review!

Member

brianlovin commented Jun 27, 2018

Optimistic updating is pushed, @mxstbr this is ready for a final review!

@mxstbr

mxstbr approved these changes Jun 27, 2018

That schema actually makes a lot of sense. This LGTM, let's ship it?!

@brianlovin

This comment has been minimized.

Show comment
Hide comment
@brianlovin

brianlovin Jun 27, 2018

Member

LET'S SHIP IT! Gonna put it on alpha, run the migration, test, then do a prod cut.

Member

brianlovin commented Jun 27, 2018

LET'S SHIP IT! Gonna put it on alpha, run the migration, test, then do a prod cut.

@brianlovin brianlovin merged commit 54128d4 into alpha Jun 27, 2018

5 checks passed

Peril ⚠️ Danger found some issues. Don't worry, everything is fixable.
Details
ci/circleci: checkout_environment Your tests passed on CircleCI!
Details
ci/circleci: test_mobile_js Your tests passed on CircleCI!
Details
ci/circleci: test_static_js Your tests passed on CircleCI!
Details
ci/circleci: test_web Your tests passed on CircleCI!
Details

@brianlovin brianlovin deleted the thread-likes branch Jun 27, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment