22import React from 'react' ;
33import classnames from 'classnames' ;
44import Button from 'component/button' ;
5+ import ChannelThumbnail from 'component/channelThumbnail' ;
6+ import Icon from 'component/common/icon' ;
57import * as ICONS from 'constants/icons' ;
68import * as REACTION_TYPES from 'constants/reactions' ;
79import { formatNumberWithCommas } from 'util/number' ;
@@ -21,6 +23,13 @@ type Props = {
2123 autoPlayNextShort : boolean ,
2224 doToggleShortsAutoplay : ( ) => void ,
2325 doSetShortsSidePanel : ( isOpen : boolean ) => void ,
26+ channelUrl : ?string ,
27+ isSubscribed : boolean ,
28+ channelPermanentUrl : ?string ,
29+ doChannelSubscribe : ( sub : { } ) => void ,
30+ doChannelUnsubscribe : ( sub : { } ) => void ,
31+ onFireGlow ?: ( ) => void ,
32+ onSlimeEffect ?: ( ) => void ,
2433} ;
2534
2635const FloatingShortsActions = ( {
@@ -38,11 +47,33 @@ const FloatingShortsActions = ({
3847 autoPlayNextShort,
3948 doToggleShortsAutoplay,
4049 doSetShortsSidePanel,
50+ channelUrl,
51+ isSubscribed,
52+ channelPermanentUrl,
53+ doChannelSubscribe,
54+ doChannelUnsubscribe,
55+ onFireGlow,
56+ onSlimeEffect,
4157} : Props ) => {
58+ const [ optimisticReaction , setOptimisticReaction ] = React . useState ( undefined ) ;
59+ const [ fireButtonGlow , setFireButtonGlow ] = React . useState ( false ) ;
60+ const fireButtonGlowTimeout = React . useRef ( null ) ;
61+ const [ slimeButtonGlow , setSlimeButtonGlow ] = React . useState ( false ) ;
62+ const slimeButtonGlowTimeout = React . useRef ( null ) ;
63+ const [ avatarHover , setAvatarHover ] = React . useState ( false ) ;
64+
4265 React . useEffect ( ( ) => {
4366 if ( claimId ) doFetchReactions ( claimId ) ;
4467 } , [ claimId , doFetchReactions ] ) ;
4568
69+ React . useEffect ( ( ) => {
70+ setOptimisticReaction ( undefined ) ;
71+ } , [ myReaction ] ) ;
72+
73+ const effectiveReaction = optimisticReaction !== undefined ? optimisticReaction : myReaction ;
74+ const isFireActive = effectiveReaction === REACTION_TYPES . LIKE ;
75+ const isSlimeActive = effectiveReaction === REACTION_TYPES . DISLIKE ;
76+
4677 return (
4778 < >
4879 < div className = "content__shorts-floating-nav" >
@@ -64,16 +95,29 @@ const FloatingShortsActions = ({
6495 < div className = "content__shorts-floating-actions" >
6596 < div className = "shorts-floating-action" >
6697 < Button
67- onClick = { ( ) => doReactionLike ( uri ) }
68- icon = { myReaction === REACTION_TYPES . LIKE ? ICONS . FIRE_ACTIVE : ICONS . FIRE }
98+ onClick = { ( ) => {
99+ setOptimisticReaction ( isFireActive ? null : REACTION_TYPES . LIKE ) ;
100+ if ( ! isFireActive ) {
101+ if ( onFireGlow ) onFireGlow ( ) ;
102+ setFireButtonGlow ( false ) ;
103+ clearTimeout ( fireButtonGlowTimeout . current ) ;
104+ requestAnimationFrame ( ( ) => {
105+ setFireButtonGlow ( true ) ;
106+ fireButtonGlowTimeout . current = setTimeout ( ( ) => setFireButtonGlow ( false ) , 2000 ) ;
107+ } ) ;
108+ }
109+ doReactionLike ( uri ) ;
110+ } }
111+ icon = { isFireActive ? ICONS . FIRE_ACTIVE : ICONS . FIRE }
69112 iconSize = { 14 }
70113 requiresAuth
71114 authSrc = "filereaction_like"
72115 className = { classnames ( 'button--file-action button-like' , {
73- 'button--fire' : myReaction === REACTION_TYPES . LIKE ,
116+ 'button--fire' : isFireActive ,
117+ 'button--fire-glow-pulse' : fireButtonGlow ,
74118 } ) }
75119 label = {
76- myReaction === REACTION_TYPES . LIKE ? (
120+ isFireActive ? (
77121 < >
78122 < div className = "button__fire-glow" />
79123 < div className = "button__fire-particle1" />
@@ -93,16 +137,29 @@ const FloatingShortsActions = ({
93137
94138 < div className = "shorts-floating-action" >
95139 < Button
96- onClick = { ( ) => doReactionDislike ( uri ) }
97- icon = { myReaction === REACTION_TYPES . DISLIKE ? ICONS . SLIME_ACTIVE : ICONS . SLIME }
140+ onClick = { ( ) => {
141+ setOptimisticReaction ( isSlimeActive ? null : REACTION_TYPES . DISLIKE ) ;
142+ if ( ! isSlimeActive ) {
143+ if ( onSlimeEffect ) onSlimeEffect ( ) ;
144+ setSlimeButtonGlow ( false ) ;
145+ clearTimeout ( slimeButtonGlowTimeout . current ) ;
146+ requestAnimationFrame ( ( ) => {
147+ setSlimeButtonGlow ( true ) ;
148+ slimeButtonGlowTimeout . current = setTimeout ( ( ) => setSlimeButtonGlow ( false ) , 3000 ) ;
149+ } ) ;
150+ }
151+ doReactionDislike ( uri ) ;
152+ } }
153+ icon = { isSlimeActive ? ICONS . SLIME_ACTIVE : ICONS . SLIME }
98154 iconSize = { 14 }
99155 requiresAuth
100156 authSrc = "filereaction_dislike"
101157 className = { classnames ( 'button--file-action button-dislike' , {
102- 'button--slime' : myReaction === REACTION_TYPES . DISLIKE ,
158+ 'button--slime' : isSlimeActive ,
159+ 'button--slime-glow-pulse' : slimeButtonGlow ,
103160 } ) }
104161 label = {
105- myReaction === REACTION_TYPES . DISLIKE ? (
162+ isSlimeActive ? (
106163 < >
107164 < div className = "button__slime-stain" />
108165 < div className = "button__slime-drop1" />
@@ -116,6 +173,40 @@ const FloatingShortsActions = ({
116173 ) }
117174 </ div >
118175
176+ { channelUrl && (
177+ < div
178+ className = "shorts-floating-action shorts-floating-action--avatar"
179+ onMouseEnter = { ( ) => setAvatarHover ( true ) }
180+ onMouseLeave = { ( ) => setAvatarHover ( false ) }
181+ onClick = { ( ) => {
182+ const sub = { channelName : channelUrl . split ( '/' ) . pop ( ) , uri : channelPermanentUrl } ;
183+ if ( isSubscribed ) {
184+ doChannelUnsubscribe ( sub ) ;
185+ } else {
186+ doChannelSubscribe ( sub ) ;
187+ }
188+ } }
189+ >
190+ < ChannelThumbnail uri = { channelUrl } hideStakedIndicator className = "shorts-floating-action__avatar" />
191+ < div
192+ className = { classnames ( 'shorts-floating-action__subscribe' , {
193+ 'shorts-floating-action__subscribe--active' : isSubscribed ,
194+ } ) }
195+ >
196+ < Icon
197+ icon = {
198+ isSubscribed && avatarHover
199+ ? ICONS . UNSUBSCRIBE
200+ : isSubscribed || avatarHover
201+ ? ICONS . SUBSCRIBED
202+ : ICONS . SUBSCRIBE
203+ }
204+ size = { 10 }
205+ />
206+ </ div >
207+ </ div >
208+ ) }
209+
119210 < div className = "shorts-floating-action" >
120211 < Button navigate = { navigateUrl } onClick = { ( ) => doSetShortsSidePanel ( true ) } icon = { ICONS . INFO } iconSize = { 14 } />
121212 </ div >
0 commit comments