Skip to content

Commit b0d7c76

Browse files
committed
Redesign follow button
1 parent 4a7521d commit b0d7c76

File tree

4 files changed

+211
-47
lines changed

4 files changed

+211
-47
lines changed

ui/component/subscribeButton/view.jsx

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// @flow
22
import * as ICONS from 'constants/icons';
33
import * as PAGES from 'constants/pages';
4-
import React, { useRef } from 'react';
4+
import React, { useRef, useLayoutEffect } from 'react';
55
import { parseURI } from 'util/lbryURI';
66
import Button from 'component/button';
7-
import useHover from 'effects/use-hover';
7+
import Icon from 'component/common/icon';
8+
89
import { useIsMobile } from 'effects/use-screensize';
910
import { ENABLE_UI_NOTIFICATIONS } from 'config';
1011
import { EmbedContext } from 'contexts/embed';
@@ -47,8 +48,9 @@ export default function SubscribeButton(props: Props) {
4748
const isEmbed = React.useContext(EmbedContext);
4849

4950
const buttonRef = useRef();
51+
const prevWidthRef = useRef(null);
5052
const isMobile = useIsMobile();
51-
let isHovering = useHover(buttonRef);
53+
5254
const uiNotificationsEnabled = (user && user.experimental_ui) || ENABLE_UI_NOTIFICATIONS;
5355

5456
const { channelName: rawChannelName } = parseURI(uri);
@@ -67,12 +69,32 @@ export default function SubscribeButton(props: Props) {
6769

6870
const { pushSupported, pushEnabled, pushRequest, pushErrorModal } = useBrowserNotifications();
6971

72+
useLayoutEffect(() => {
73+
const btn = buttonRef.current;
74+
const prev = prevWidthRef.current;
75+
if (btn && prev !== null) {
76+
const newWidth = Math.round(btn.getBoundingClientRect().width);
77+
const prevWidth = Math.round(prev);
78+
prevWidthRef.current = null;
79+
if (prevWidth !== newWidth) {
80+
// $FlowFixMe - WAAPI
81+
const anim = btn.animate([{ width: prevWidth + 'px' }, { width: newWidth + 'px' }], {
82+
duration: 300,
83+
easing: 'ease',
84+
});
85+
anim.onfinish = () => {
86+
if (buttonRef.current) buttonRef.current.style.width = '';
87+
};
88+
}
89+
}
90+
}, [isSubscribed]);
91+
7092
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
7193

7294
const subscriptionLabel = isSubscribed
7395
? __('Following --[button label indicating a channel has been followed]--')
7496
: __('Follow');
75-
const unfollowOverride = isSubscribed && isHovering && __('Unfollow');
97+
const unfollowOverride = false;
7698

7799
const label = isMobile && shrinkOnMobile ? '' : unfollowOverride || subscriptionLabel;
78100
const titlePrefix = isSubscribed ? __('Unfollow this channel') : __('Follow this channel');
@@ -116,9 +138,8 @@ export default function SubscribeButton(props: Props) {
116138
<Button
117139
ref={buttonRef}
118140
iconColor="red"
119-
className={isSubscribed ? 'button-following' : ''}
120-
largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel}
121-
icon={unfollowOverride ? ICONS.UNSUBSCRIBE : isSubscribed ? ICONS.SUBSCRIBED : ICONS.SUBSCRIBE}
141+
className={`button-following${isSubscribed ? ' button-following--active' : ''}`}
142+
icon={isSubscribed ? ICONS.SUBSCRIBED : ICONS.SUBSCRIBE}
122143
button={'alt'}
123144
requiresAuth={IS_WEB}
124145
label={label}
@@ -129,6 +150,47 @@ export default function SubscribeButton(props: Props) {
129150
? (e) => {
130151
e.stopPropagation();
131152

153+
if (buttonRef.current) {
154+
prevWidthRef.current = buttonRef.current.getBoundingClientRect().width;
155+
}
156+
157+
if (!isSubscribed && buttonRef.current) {
158+
// $FlowFixMe - WAAPI
159+
buttonRef.current.animate(
160+
[
161+
{ boxShadow: 'inset 0 0 0 999px transparent' },
162+
{ boxShadow: 'inset 0 0 0 999px #e9ea69', offset: 0.25 },
163+
{ boxShadow: 'inset 0 0 0 999px #db6a66', offset: 0.75 },
164+
{ boxShadow: 'inset 0 0 0 999px transparent' },
165+
],
166+
{ duration: 1000, easing: 'linear' }
167+
);
168+
169+
const btn = buttonRef.current;
170+
const group = btn ? btn.closest('.button-group') : null;
171+
if (btn && group) {
172+
group.style.position = 'relative';
173+
group.querySelectorAll('.button-following__heart-particle').forEach((el) => el.remove());
174+
const icon = btn.querySelector('.icon');
175+
if (icon) {
176+
const groupRect = group.getBoundingClientRect();
177+
const iconRect = icon.getBoundingClientRect();
178+
const cx = iconRect.left - groupRect.left + iconRect.width / 2;
179+
const cy = iconRect.top - groupRect.top;
180+
for (let i = 0; i < 5; i++) {
181+
const heart = document.createElement('span');
182+
heart.textContent = '\u2764';
183+
heart.className = 'button-following__heart-particle';
184+
heart.style.left = cx + (Math.random() * 20 - 10) + 'px';
185+
heart.style.top = cy + 'px';
186+
heart.style.animationDelay = Math.random() * 0.3 + 's';
187+
heart.style.fontSize = 12 + Math.random() * 10 + 'px';
188+
group.appendChild(heart);
189+
}
190+
}
191+
}
192+
}
193+
132194
subscriptionHandler(
133195
{
134196
channelName: claimName,
@@ -140,15 +202,20 @@ export default function SubscribeButton(props: Props) {
140202
}
141203
: undefined
142204
}
143-
/>
144-
{isSubscribed && uiNotificationsEnabled && (
145-
<>
146-
<Button
147-
button="alt"
148-
icon={notificationsDisabled ? ICONS.BELL : ICONS.BELL_ON}
149-
className={isSubscribed ? 'button-following' : ''}
205+
>
206+
{isSubscribed && uiNotificationsEnabled && (
207+
<span
208+
className="button-following__bell"
209+
role="button"
210+
tabIndex={0}
150211
aria-label={notificationsDisabled ? __('Turn on notifications') : __('Turn off notifications')}
151-
onClick={() => {
212+
onClick={(e) => {
213+
e.stopPropagation();
214+
const bell = e.currentTarget;
215+
bell.classList.add('button-following__bell--ringing');
216+
bell.addEventListener('animationend', () => bell.classList.remove('button-following__bell--ringing'), {
217+
once: true,
218+
});
152219
const newNotificationsDisabled = !notificationsDisabled;
153220

154221
doChannelSubscribe(
@@ -173,10 +240,12 @@ export default function SubscribeButton(props: Props) {
173240
pushRequest();
174241
}
175242
}}
176-
/>
177-
{pushErrorModal()}
178-
</>
179-
)}
243+
>
244+
<Icon icon={notificationsDisabled ? ICONS.BELL : ICONS.BELL_ON} size={16} />
245+
</span>
246+
)}
247+
</Button>
248+
{pushErrorModal()}
180249
</div>
181250
) : null;
182251
}

ui/scss/component/_button.scss

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,22 +1043,110 @@ svg + .button__label {
10431043
}
10441044
}
10451045

1046-
.button-following {
1047-
color: var(--color-text) !important;
1048-
background-color: var(--color-background) !important;
1049-
transition: width 1s;
1046+
.button--alt.button-following {
1047+
background-color: var(--color-primary);
1048+
color: var(--color-primary-contrast);
1049+
position: relative;
1050+
overflow: hidden;
1051+
clip-path: inset(0 round var(--border-radius));
1052+
1053+
&:focus {
1054+
z-index: unset;
1055+
}
1056+
10501057
.icon {
1051-
stroke: var(--color-text) !important;
1058+
stroke: var(--color-primary-contrast);
10521059
}
1053-
@media (min-width: $breakpoint-small) {
1054-
.button__label {
1055-
div:first-of-type {
1056-
min-width: 84px;
1057-
}
1060+
1061+
&.button-following--active {
1062+
background-color: hsla(0, 0%, 40%, 0.5) !important;
1063+
color: #fff !important;
1064+
1065+
.icon {
1066+
stroke: #fff !important;
10581067
}
10591068
}
10601069
}
10611070

1071+
.button-following__bell {
1072+
display: inline-flex;
1073+
align-items: center;
1074+
justify-content: center;
1075+
cursor: pointer;
1076+
position: relative;
1077+
z-index: 1;
1078+
margin: calc(-1 * var(--spacing-s));
1079+
margin-left: var(--spacing-m);
1080+
padding: var(--spacing-s);
1081+
padding-left: var(--spacing-m);
1082+
1083+
&::before {
1084+
content: '';
1085+
position: absolute;
1086+
left: 0;
1087+
top: var(--spacing-s);
1088+
bottom: var(--spacing-s);
1089+
width: 1px;
1090+
background-color: currentColor;
1091+
opacity: 0.7;
1092+
}
1093+
1094+
&:hover .icon {
1095+
fill: currentColor;
1096+
}
1097+
1098+
&.button-following__bell--ringing .icon {
1099+
animation: bell-ring 0.6s ease;
1100+
transform-origin: top center;
1101+
}
1102+
}
1103+
1104+
.button-following__heart-particle {
1105+
position: absolute;
1106+
pointer-events: none;
1107+
z-index: 1;
1108+
color: var(--color-primary);
1109+
animation: heart-float 1.4s ease-out forwards;
1110+
}
1111+
1112+
@keyframes heart-float {
1113+
0% {
1114+
opacity: 1;
1115+
transform: translateY(0) scale(1);
1116+
}
1117+
100% {
1118+
opacity: 0;
1119+
transform: translateY(-60px) scale(0.5);
1120+
}
1121+
}
1122+
1123+
@keyframes bell-ring {
1124+
0% {
1125+
transform: rotate(0deg);
1126+
}
1127+
15% {
1128+
transform: rotate(15deg);
1129+
}
1130+
30% {
1131+
transform: rotate(-12deg);
1132+
}
1133+
45% {
1134+
transform: rotate(9deg);
1135+
}
1136+
60% {
1137+
transform: rotate(-6deg);
1138+
}
1139+
75% {
1140+
transform: rotate(3deg);
1141+
}
1142+
90% {
1143+
transform: rotate(-1deg);
1144+
}
1145+
100% {
1146+
transform: rotate(0deg);
1147+
}
1148+
}
1149+
10621150
.recommended-content__bubble {
10631151
display: flex;
10641152

ui/scss/component/_channel.scss

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,23 @@ $actions-z-index: 2;
146146
left: 18px;
147147
}
148148
.button-group {
149-
.button__content {
150-
.icon {
151-
stroke: var(--color-text);
152-
}
153-
}
154149
.button-following {
155150
color: var(--color-text) !important;
156-
background-color: var(--color-button-alt-bg) !important;
157151
.icon {
158152
stroke: var(--color-text) !important;
159153
}
160-
&:hover {
154+
}
155+
.button__content {
156+
.icon {
157+
stroke: var(--color-text);
158+
}
159+
}
160+
.button-following--active {
161+
&:hover:not(:has(.button-following__bell:hover)) {
161162
color: var(--color-primary) !important;
163+
.button-following__bell {
164+
color: var(--color-text);
165+
}
162166
}
163167
@media (max-width: $breakpoint-small) {
164168
.button__label {
@@ -168,11 +172,8 @@ $actions-z-index: 2;
168172
}
169173
}
170174
}
171-
.button-following:last-of-type {
175+
.button-following--active:last-of-type {
172176
margin-left: 2px;
173-
&:hover {
174-
color: var(--color-text) !important;
175-
}
176177
}
177178
}
178179

@@ -727,6 +728,11 @@ $actions-z-index: 2;
727728
background-color: rgba(var(--color-background-base), 0.9) !important;
728729
}
729730

731+
.button-following__bell {
732+
margin-left: var(--spacing-s);
733+
padding-left: var(--spacing-s);
734+
}
735+
730736
> .button,
731737
> .button-group {
732738
&:not(:last-child) {

ui/scss/component/_claim-preview.scss

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -549,11 +549,11 @@
549549
.membership-button-wrapper {
550550
margin-top: 0;
551551
}
552-
.button-following {
553-
color: var(--color-primary-contrast) !important;
554-
background-color: rgba(125, 125, 125, 0.5) !important;
552+
.button-following--active {
553+
color: var(--color-primary-contrast);
554+
background-color: rgba(125, 125, 125, 0.5);
555555
.icon {
556-
stroke: var(--color-primary-contrast) !important;
556+
stroke: var(--color-primary-contrast);
557557
}
558558
}
559559
.button {
@@ -589,13 +589,14 @@
589589
margin-top: 0;
590590
}
591591

592-
a.button-following {
592+
a.button-following--active {
593593
.button__content {
594594
width: 100%;
595595
}
596596
}
597-
button.button-following:last-of-type {
598-
max-width: fit-content;
597+
598+
.button-following__bell {
599+
margin-left: auto;
599600
}
600601

601602
span {

0 commit comments

Comments
 (0)