Skip to content

Commit 02f292a

Browse files
committed
Update shorts player
1 parent 5535834 commit 02f292a

File tree

9 files changed

+732
-166
lines changed

9 files changed

+732
-166
lines changed

ui/component/shortsActions/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@ import {
99
selectScheduledStateForUri,
1010
makeSelectTagInClaimOrChannelForUri,
1111
selectIsUriUnlisted,
12+
selectPermanentUrlForUri,
13+
selectChannelForClaimUri,
1214
} from 'redux/selectors/claims';
15+
import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
16+
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
1317
import { DISABLE_SLIMES_VIDEO_TAG, DISABLE_SLIMES_ALL_TAG } from 'constants/tags';
1418
import { doOpenModal } from 'redux/actions/app';
1519

1620
const select = (state, props) => {
1721
const { uri } = props;
1822

1923
const claim = selectClaimForUri(state, uri);
20-
2124
const { claim_id: claimId } = claim || {};
25+
const channelUrl = uri ? selectChannelForClaimUri(state, uri, true) : undefined;
2226

2327
return {
2428
myReaction: selectMyReactionForUri(state, uri),
@@ -34,6 +38,9 @@ const select = (state, props) => {
3438
isUnlisted: selectIsUriUnlisted(state, uri),
3539
webShareable: true,
3640
collectionId: props.collectionId,
41+
channelUrl,
42+
isSubscribed: channelUrl ? selectIsSubscribedForUri(state, channelUrl) : false,
43+
channelPermanentUrl: channelUrl ? selectPermanentUrlForUri(state, channelUrl) : undefined,
3744
};
3845
};
3946

@@ -42,6 +49,8 @@ const perform = {
4249
doReactionLike,
4350
doReactionDislike,
4451
doOpenModal,
52+
doChannelSubscribe,
53+
doChannelUnsubscribe,
4554
};
4655

4756
export default connect(select, perform)(ShortsActions);
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
1-
export { default } from './view';
1+
import { connect } from 'react-redux';
2+
import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
3+
import { selectPermanentUrlForUri, selectChannelForClaimUri } from 'redux/selectors/claims';
4+
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
5+
import MobileActions from './view';
6+
7+
const select = (state, props) => {
8+
const channelUrl = props.uri ? selectChannelForClaimUri(state, props.uri, true) : undefined;
9+
return {
10+
channelUrl,
11+
isSubscribed: channelUrl ? selectIsSubscribedForUri(state, channelUrl) : false,
12+
channelPermanentUrl: channelUrl ? selectPermanentUrlForUri(state, channelUrl) : undefined,
13+
};
14+
};
15+
16+
const perform = {
17+
doChannelSubscribe,
18+
doChannelUnsubscribe,
19+
};
20+
21+
export default connect(select, perform)(MobileActions);

ui/component/shortsActions/shortsMobileActions/view.jsx

Lines changed: 199 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// @flow
22
import React from 'react';
3+
import { createPortal } from 'react-dom';
34
import classnames from 'classnames';
45
import Button from 'component/button';
6+
import ChannelThumbnail from 'component/channelThumbnail';
7+
import Icon from 'component/common/icon';
58
import * as ICONS from 'constants/icons';
69
import * as REACTION_TYPES from 'constants/reactions';
710
import 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

2533
const 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

Comments
 (0)