11// @flow
22import React from 'react' ;
3+ import { createPortal } from 'react-dom' ;
34import classnames from 'classnames' ;
45import Button from 'component/button' ;
6+ import ChannelThumbnail from 'component/channelThumbnail' ;
7+ import Icon from 'component/common/icon' ;
58import * as ICONS from 'constants/icons' ;
69import * as REACTION_TYPES from 'constants/reactions' ;
710import Skeleton from '@mui/material/Skeleton' ;
@@ -20,6 +23,11 @@ type Props = {
2023 autoPlayNextShort : boolean ,
2124 doToggleShortsAutoplay : ( ) => void ,
2225 isUnlisted : ?boolean ,
26+ channelUrl : ?string ,
27+ isSubscribed : boolean ,
28+ channelPermanentUrl : ?string ,
29+ doChannelSubscribe : ( sub : { } ) => void ,
30+ doChannelUnsubscribe : ( sub : { } ) => void ,
2331} ;
2432
2533const MobileActions = ( {
@@ -35,116 +43,207 @@ const MobileActions = ({
3543 autoPlayNextShort,
3644 doToggleShortsAutoplay,
3745 isUnlisted,
46+ channelUrl,
47+ isSubscribed,
48+ channelPermanentUrl,
49+ doChannelSubscribe,
50+ doChannelUnsubscribe,
3851} : Props ) => {
52+ const [ optimisticReaction , setOptimisticReaction ] = React . useState ( undefined ) ;
53+ const [ fireButtonGlow , setFireButtonGlow ] = React . useState ( false ) ;
54+ const fireButtonGlowTimeout = React . useRef ( null ) ;
55+ const [ slimeButtonGlow , setSlimeButtonGlow ] = React . useState ( false ) ;
56+ const slimeButtonGlowTimeout = React . useRef ( null ) ;
57+ const [ fireEffect , setFireEffect ] = React . useState ( false ) ;
58+ const fireEffectTimeout = React . useRef ( null ) ;
59+ const [ slimeEffect , setSlimeEffect ] = React . useState ( false ) ;
60+ const slimeEffectTimeout = React . useRef ( null ) ;
61+ const [ avatarHover , setAvatarHover ] = React . useState ( false ) ;
62+
63+ React . useEffect ( ( ) => {
64+ setOptimisticReaction ( undefined ) ;
65+ } , [ myReaction ] ) ;
66+
67+ const effectiveReaction = optimisticReaction !== undefined ? optimisticReaction : myReaction ;
68+ const isFireActive = effectiveReaction === REACTION_TYPES . LIKE ;
69+ const isSlimeActive = effectiveReaction === REACTION_TYPES . DISLIKE ;
70+
3971 const Placeholder = < Skeleton variant = "text" animation = "wave" className = "reaction-count-placeholder" /> ;
4072
4173 return (
42- < div className = "shorts-mobile-panel__actions" >
43- < div className = "shorts-mobile-panel__action-item" >
44- < Button
45- onClick = { ( ) => doReactionLike ( uri ) }
46- icon = { myReaction === REACTION_TYPES . LIKE ? ICONS . FIRE_ACTIVE : ICONS . FIRE }
47- iconSize = { 16 }
48- title = { __ ( 'I Like This' ) }
49- requiresAuth
50- authSrc = "filereaction_like"
51- className = { classnames ( 'shorts-mobile-panel__action-button button--file-action button-like' , {
52- 'button--fire' : myReaction === REACTION_TYPES . LIKE ,
53- } ) }
54- label = {
55- < >
56- { myReaction === REACTION_TYPES . LIKE && (
57- < >
58- < div className = "button__fire-glow" />
59- < div className = "button__fire-particle1" />
60- < div className = "button__fire-particle2" />
61- < div className = "button__fire-particle3" />
62- < div className = "button__fire-particle4" />
63- < div className = "button__fire-particle5" />
64- < div className = "button__fire-particle6" />
65- </ >
66- ) }
67- </ >
68- }
69- />
70- < span className = "shorts-mobile-panel__count" >
71- { Number . isInteger ( likeCount ) ? formatNumberWithCommas ( likeCount , 0 ) : Placeholder }
72- </ span >
73- </ div >
74+ < >
75+ { fireEffect &&
76+ createPortal (
77+ < div className = "shorts-mobile-flames" >
78+ { Array . from ( { length : 50 } , ( _ , i ) => (
79+ < div
80+ key = { i }
81+ className = "shorts-mobile-flames__particle"
82+ style = { {
83+ left : `calc(${ ( i / 50 ) * 100 } % - 35px)` ,
84+ animationDelay : `${ Math . random ( ) } s` ,
85+ } }
86+ />
87+ ) ) }
88+ </ div > ,
89+ // $FlowFixMe
90+ document . body
91+ ) }
7492
75- < div className = "shorts-mobile-panel__action-item" >
76- < Button
77- requiresAuth
78- authSrc = { 'filereaction_dislike' }
79- title = { __ ( 'I dislike this' ) }
80- className = { classnames ( 'shorts-mobile-panel__action-button button--file-action button-dislike' , {
81- 'button--slime' : myReaction === REACTION_TYPES . DISLIKE ,
82- } ) }
83- label = {
84- < >
85- { myReaction === REACTION_TYPES . DISLIKE && (
86- < >
87- < div className = "button__slime-stain" />
88- < div className = "button__slime-drop1" />
89- < div className = "button__slime-drop2" />
90- </ >
91- ) }
92- </ >
93- }
94- iconSize = { 16 }
95- icon = { myReaction === REACTION_TYPES . DISLIKE ? ICONS . SLIME_ACTIVE : ICONS . SLIME }
96- onClick = { ( ) => doReactionDislike ( uri ) }
97- />
98- < span className = "shorts-mobile-panel__count" >
99- { Number . isInteger ( dislikeCount ) ? formatNumberWithCommas ( dislikeCount , 0 ) : Placeholder }
100- </ span >
101- </ div >
93+ { /* $FlowFixMe */ }
94+ { slimeEffect && createPortal ( < div className = "shorts-mobile-slime" /> , document . body ) }
10295
103- < div className = "shorts-mobile-panel__action-item" >
104- < Button
105- className = "shorts-mobile-panel__action-button"
106- onClick = { onCommentsClick }
107- icon = { ICONS . COMMENTS_LIST }
108- iconSize = { 16 }
109- />
110- < span className = "shorts-mobile-panel__count" > { __ ( 'Comments' ) } </ span >
111- </ div >
96+ < div className = "shorts-mobile-panel__actions" >
97+ < div className = "shorts-mobile-panel__action-item" >
98+ < Button
99+ onClick = { ( ) => {
100+ setOptimisticReaction ( isFireActive ? null : REACTION_TYPES . LIKE ) ;
101+ if ( ! isFireActive ) {
102+ setFireButtonGlow ( false ) ;
103+ setFireEffect ( false ) ;
104+ clearTimeout ( fireButtonGlowTimeout . current ) ;
105+ clearTimeout ( fireEffectTimeout . current ) ;
106+ requestAnimationFrame ( ( ) => {
107+ setFireButtonGlow ( true ) ;
108+ setFireEffect ( true ) ;
109+ fireButtonGlowTimeout . current = setTimeout ( ( ) => setFireButtonGlow ( false ) , 2000 ) ;
110+ fireEffectTimeout . current = setTimeout ( ( ) => setFireEffect ( false ) , 2000 ) ;
111+ } ) ;
112+ }
113+ doReactionLike ( uri ) ;
114+ } }
115+ icon = { isFireActive ? ICONS . FIRE_ACTIVE : ICONS . FIRE }
116+ iconSize = { 16 }
117+ title = { __ ( 'I Like This' ) }
118+ requiresAuth
119+ authSrc = "filereaction_like"
120+ className = { classnames ( 'shorts-mobile-panel__action-button button--file-action button-like' , {
121+ 'button--fire' : isFireActive ,
122+ 'button--fire-glow-pulse' : fireButtonGlow ,
123+ } ) }
124+ />
125+ < span className = "shorts-mobile-panel__count" >
126+ { Number . isInteger ( likeCount ) ? formatNumberWithCommas ( likeCount , 0 ) : Placeholder }
127+ </ span >
128+ </ div >
112129
113- < div className = "shorts-mobile-panel__action-item" >
114- < Button
115- className = "shorts-mobile-panel__action-button"
116- onClick = { onShareClick }
117- icon = { ICONS . SHARE }
118- iconSize = { 16 }
119- title = { isUnlisted ? __ ( 'Get a sharable link for your unlisted content' ) : __ ( 'Share' ) }
120- />
121- < span className = "shorts-mobile-panel__count" > { __ ( 'Share' ) } </ span >
122- </ div >
130+ < div className = "shorts-mobile-panel__action-item" >
131+ < Button
132+ requiresAuth
133+ authSrc = { 'filereaction_dislike' }
134+ title = { __ ( 'I dislike this' ) }
135+ className = { classnames ( 'shorts-mobile-panel__action-button button--file-action button-dislike' , {
136+ 'button--slime' : isSlimeActive ,
137+ 'button--slime-glow-pulse' : slimeButtonGlow ,
138+ } ) }
139+ iconSize = { 16 }
140+ icon = { isSlimeActive ? ICONS . SLIME_ACTIVE : ICONS . SLIME }
141+ onClick = { ( ) => {
142+ setOptimisticReaction ( isSlimeActive ? null : REACTION_TYPES . DISLIKE ) ;
143+ if ( ! isSlimeActive ) {
144+ setSlimeButtonGlow ( false ) ;
145+ setSlimeEffect ( false ) ;
146+ clearTimeout ( slimeButtonGlowTimeout . current ) ;
147+ clearTimeout ( slimeEffectTimeout . current ) ;
148+ requestAnimationFrame ( ( ) => {
149+ setSlimeButtonGlow ( true ) ;
150+ setSlimeEffect ( true ) ;
151+ slimeButtonGlowTimeout . current = setTimeout ( ( ) => setSlimeButtonGlow ( false ) , 3000 ) ;
152+ slimeEffectTimeout . current = setTimeout ( ( ) => setSlimeEffect ( false ) , 3000 ) ;
153+ } ) ;
154+ }
155+ doReactionDislike ( uri ) ;
156+ } }
157+ />
158+ < span className = "shorts-mobile-panel__count" >
159+ { Number . isInteger ( dislikeCount ) ? formatNumberWithCommas ( dislikeCount , 0 ) : Placeholder }
160+ </ span >
161+ </ div >
123162
124- < div className = "shorts-mobile-panel__action-item" >
125- < Button
126- className = "shorts-mobile-panel__action-button"
127- onClick = { onInfoButtonClick }
128- icon = { ICONS . INFO }
129- iconSize = { 16 }
130- />
131- < span className = "shorts-mobile-panel__count" > { __ ( 'Details' ) } </ span >
132- </ div >
163+ { channelUrl && (
164+ < div
165+ className = "shorts-mobile-panel__action-item"
166+ onMouseEnter = { ( ) => setAvatarHover ( true ) }
167+ onMouseLeave = { ( ) => setAvatarHover ( false ) }
168+ onClick = { ( e ) => {
169+ e . stopPropagation ( ) ;
170+ const sub = { channelName : channelUrl . split ( '/' ) . pop ( ) , uri : channelPermanentUrl } ;
171+ if ( isSubscribed ) {
172+ doChannelUnsubscribe ( sub ) ;
173+ } else {
174+ doChannelSubscribe ( sub ) ;
175+ }
176+ } }
177+ >
178+ < div className = "shorts-mobile-panel__avatar-wrapper" >
179+ < ChannelThumbnail uri = { channelUrl } hideStakedIndicator />
180+ < div
181+ className = { classnames ( 'shorts-mobile-panel__subscribe-icon' , {
182+ 'shorts-mobile-panel__subscribe-icon--active' : isSubscribed ,
183+ } ) }
184+ >
185+ < Icon
186+ icon = {
187+ isSubscribed && avatarHover
188+ ? ICONS . UNSUBSCRIBE
189+ : isSubscribed || avatarHover
190+ ? ICONS . SUBSCRIBED
191+ : ICONS . SUBSCRIBE
192+ }
193+ size = { 10 }
194+ />
195+ </ div >
196+ </ div >
197+ < span className = "shorts-mobile-panel__count" > { isSubscribed ? __ ( 'Following' ) : __ ( 'Follow' ) } </ span >
198+ </ div >
199+ ) }
200+
201+ < div className = "shorts-mobile-panel__action-item" >
202+ < Button
203+ className = "shorts-mobile-panel__action-button"
204+ onClick = { onCommentsClick }
205+ icon = { ICONS . COMMENTS_LIST }
206+ iconSize = { 16 }
207+ />
208+ < span className = "shorts-mobile-panel__count" > { __ ( 'Comments' ) } </ span >
209+ </ div >
210+
211+ < div className = "shorts-mobile-panel__action-item" >
212+ < Button
213+ className = "shorts-mobile-panel__action-button"
214+ onClick = { onShareClick }
215+ icon = { ICONS . SHARE }
216+ iconSize = { 16 }
217+ title = { isUnlisted ? __ ( 'Get a sharable link for your unlisted content' ) : __ ( 'Share' ) }
218+ />
219+ < span className = "shorts-mobile-panel__count" > { __ ( 'Share' ) } </ span >
220+ </ div >
221+
222+ < div className = "shorts-mobile-panel__action-item" >
223+ < Button
224+ className = "shorts-mobile-panel__action-button"
225+ onClick = { onInfoButtonClick }
226+ icon = { ICONS . INFO }
227+ iconSize = { 16 }
228+ />
229+ < span className = "shorts-mobile-panel__count" > { __ ( 'Details' ) } </ span >
230+ </ div >
133231
134- < div className = "shorts-mobile-panel__action-item" >
135- < Button
136- className = { classnames ( 'shorts-mobile-panel__action-button button-bubble' , {
137- 'button-bubble--active' : autoPlayNextShort ,
138- } ) }
139- requiresAuth = { IS_WEB }
140- title = { __ ( 'Autoplay Next' ) }
141- onClick = { doToggleShortsAutoplay }
142- icon = { ICONS . AUTOPLAY_NEXT }
143- iconSize = { 24 }
144- />
145- < span className = "shorts-mobile-panel__count" > { __ ( 'Auto Next' ) } </ span >
232+ < div className = "shorts-mobile-panel__action-item" >
233+ < Button
234+ className = { classnames ( 'shorts-mobile-panel__action-button button-bubble' , {
235+ 'button-bubble--active' : autoPlayNextShort ,
236+ } ) }
237+ requiresAuth = { IS_WEB }
238+ title = { __ ( 'Autoplay Next' ) }
239+ onClick = { doToggleShortsAutoplay }
240+ icon = { ICONS . AUTOPLAY_NEXT }
241+ iconSize = { 24 }
242+ />
243+ < span className = "shorts-mobile-panel__count" > { __ ( 'Auto Next' ) } </ span >
244+ </ div >
146245 </ div >
147- </ div >
246+ </ >
148247 ) ;
149248} ;
150249
0 commit comments