Stickers/emojis fall out / improvements (#220)

* Fix error logs

* Improve LBC sticker flow/clarity

* Show inline error if custom sticker amount below min

* Sort emojis alphabetically

* Improve loading of Images

* Improve quality and display of emojis and fix CSS

* Display both USD and LBC prices

* Default to LBC tip if creator can't receive USD

* Don't clear text-field after sticker is sent

* Refactor notification component

* Handle notifications

* Don't show profile pic on sticker livestream comments

* Change Sticker icon

* Fix wording and number rounding

* Fix blurring emojis

* Disable non functional emote buttons
This commit is contained in:
saltrafael 2021-11-05 16:31:51 -03:00 committed by GitHub
parent 7cae754867
commit fc2e2d2cfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 389 additions and 275 deletions

View file

@ -343,7 +343,7 @@ function Comment(props: Props) {
</div> </div>
) : stickerFromMessage ? ( ) : stickerFromMessage ? (
<div className="sticker__comment"> <div className="sticker__comment">
<OptimizedImage src={stickerFromMessage.url} waitLoad /> <OptimizedImage src={stickerFromMessage.url} waitLoad loading="lazy" />
</div> </div>
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? ( ) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
<Expandable> <Expandable>

View file

@ -1,6 +1,6 @@
// @flow // @flow
import 'scss/component/_emote-selector.scss'; import 'scss/component/_emote-selector.scss';
import { EMOTES_24px as EMOTES } from 'constants/emotes'; import { EMOTES_48px as EMOTES } from 'constants/emotes';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import Button from 'component/button'; import Button from 'component/button';
import EMOJIS from 'emoji-dictionary'; import EMOJIS from 'emoji-dictionary';
@ -32,8 +32,6 @@ export default function EmoteSelector(props: Props) {
<div className="emoteSelector__list"> <div className="emoteSelector__list">
<div className="emoteSelector__listRow"> <div className="emoteSelector__listRow">
<div className="emoteSelector__listRowTitle">{__('Global Emotes')}</div>
<div className="emoteSelector__listRowItems"> <div className="emoteSelector__listRowItems">
{OLD_QUICK_EMOJIS.map((emoji) => ( {OLD_QUICK_EMOJIS.map((emoji) => (
<Button <Button
@ -50,7 +48,7 @@ export default function EmoteSelector(props: Props) {
return ( return (
<Button <Button
key={String(emote)} key={emoteName}
title={emoteName} title={emoteName}
button="alt" button="alt"
className="button--file-action" className="button--file-action"

View file

@ -47,7 +47,7 @@ export default function StickerSelector(props: Props) {
className="button--file-action" className="button--file-action"
onClick={() => onSelect(sticker)} onClick={() => onSelect(sticker)}
> >
<OptimizedImage src={sticker.url} waitLoad /> <OptimizedImage src={sticker.url} waitLoad loading="lazy" />
{sticker.price && sticker.price > 0 && ( {sticker.price && sticker.price > 0 && (
<CreditAmount superChatLight amount={sticker.price} size={2} isFiat /> <CreditAmount superChatLight amount={sticker.price} size={2} isFiat />
)} )}

View file

@ -4,6 +4,7 @@ import { buildValidSticker } from 'util/comments';
import { FF_MAX_CHARS_IN_COMMENT, FF_MAX_CHARS_IN_LIVESTREAM_COMMENT } from 'constants/form-field'; import { FF_MAX_CHARS_IN_COMMENT, FF_MAX_CHARS_IN_LIVESTREAM_COMMENT } from 'constants/form-field';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
import { Lbryio } from 'lbryinc';
import { SIMPLE_SITE } from 'config'; import { SIMPLE_SITE } from 'config';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
@ -123,6 +124,8 @@ export function CommentCreate(props: Props) {
const [pauseQuickSend, setPauseQuickSend] = React.useState(false); const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
const [showEmotes, setShowEmotes] = React.useState(false); const [showEmotes, setShowEmotes] = React.useState(false);
const [disableReviewButton, setDisableReviewButton] = React.useState(); const [disableReviewButton, setDisableReviewButton] = React.useState();
const [exchangeRate, setExchangeRate] = React.useState();
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
const selectedMentionIndex = const selectedMentionIndex =
commentValue.indexOf('@', selectionIndex) === selectionIndex commentValue.indexOf('@', selectionIndex) === selectionIndex
@ -152,6 +155,7 @@ export function CommentCreate(props: Props) {
const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0; const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0;
const minAmount = minTip || minSuper || 0; const minAmount = minTip || minSuper || 0;
const minAmountMet = minAmount === 0 || tipAmount >= minAmount; const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
const stickerPrice = selectedSticker && selectedSticker.price;
const minAmountRef = React.useRef(minAmount); const minAmountRef = React.useRef(minAmount);
minAmountRef.current = minAmount; minAmountRef.current = minAmount;
@ -168,7 +172,7 @@ export function CommentCreate(props: Props) {
setStickerSelector(false); setStickerSelector(false);
if (sticker.price && sticker.price > 0) { if (sticker.price && sticker.price > 0) {
setActiveTab(TAB_FIAT); setActiveTab(canReceiveFiatTip ? TAB_FIAT : TAB_LBC);
setIsSupportComment(true); setIsSupportComment(true);
} }
} }
@ -316,7 +320,7 @@ export function CommentCreate(props: Props) {
if (setQuickReply) setQuickReply(res); if (setQuickReply) setQuickReply(res);
if (res && res.signature) { if (res && res.signature) {
setCommentValue(''); if (!stickerValue) setCommentValue('');
setReviewingSupportComment(false); setReviewingSupportComment(false);
setIsSupportComment(false); setIsSupportComment(false);
setCommentFailure(false); setCommentFailure(false);
@ -370,12 +374,45 @@ export function CommentCreate(props: Props) {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [pauseQuickSend]); }, [pauseQuickSend]);
// Stickers: Get LBC-USD exchange rate if hasn't yet and selected a paid sticker
React.useEffect(() => {
if (stickerPrice && !exchangeRate) Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD));
}, [exchangeRate, stickerPrice]);
// Stickers: Check if creator has a tip account saved (on selector so that if a paid sticker is selected,
// it defaults to LBC tip instead of USD)
React.useEffect(() => {
if (!stripeEnvironment || !stickerSelector || canReceiveFiatTip !== undefined) return;
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
Lbryio.call(
'account',
'check',
{
channel_claim_id: channelClaimId,
channel_name: tipChannelName,
environment: stripeEnvironment,
},
'post'
)
.then((accountCheckResponse) => {
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
setCanReceiveFiatTip(true);
} else {
setCanReceiveFiatTip(false);
}
})
.catch(() => {});
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
// ************************************************************************** // **************************************************************************
// Render // Render
// ************************************************************************** // **************************************************************************
const getActionButton = (title: string, icon: string, handleClick: () => void) => ( const getActionButton = (title: string, label?: string, icon: string, handleClick: () => void) => (
<Button title={title} button="alt" icon={icon} onClick={handleClick} /> <Button title={title} label={label} button="alt" icon={icon} onClick={handleClick} />
); );
if (channelSettings && !channelSettings.comments_enabled) { if (channelSettings && !channelSettings.comments_enabled) {
@ -410,6 +447,7 @@ export function CommentCreate(props: Props) {
return ( return (
<Form <Form
onSubmit={() => {}}
className={classnames('commentCreate', { className={classnames('commentCreate', {
'commentCreate--reply': isReply, 'commentCreate--reply': isReply,
'commentCreate--nestedReply': isNested, 'commentCreate--nestedReply': isNested,
@ -427,10 +465,15 @@ export function CommentCreate(props: Props) {
</div> </div>
<div className="commentCreate__stickerPreviewImage"> <div className="commentCreate__stickerPreviewImage">
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad /> <OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
</div> </div>
{selectedSticker.price && <FilePrice customPrice={selectedSticker.price} isFiat />} {selectedSticker.price && exchangeRate && (
<FilePrice
customPrices={{ priceFiat: selectedSticker.price, priceLBC: selectedSticker.price / exchangeRate }}
isFiat
/>
)}
</div> </div>
) : isReviewingSupportComment && activeChannelClaim ? ( ) : isReviewingSupportComment && activeChannelClaim ? (
<div className="commentCreate__supportCommentPreview"> <div className="commentCreate__supportCommentPreview">
@ -499,13 +542,14 @@ export function CommentCreate(props: Props) {
</> </>
)} )}
{(isSupportComment || (isReviewingStickerComment && selectedSticker && selectedSticker.price)) && ( {(isSupportComment || (isReviewingStickerComment && stickerPrice)) && (
<WalletTipAmountSelector <WalletTipAmountSelector
activeTab={activeTab} activeTab={activeTab}
amount={tipAmount} amount={tipAmount}
claim={claim} claim={claim}
convertedAmount={convertedAmount} convertedAmount={convertedAmount}
customTipAmount={selectedSticker && selectedSticker.price} customTipAmount={stickerPrice}
exchangeRate={exchangeRate}
fiatConversion={selectedSticker && !!selectedSticker.price} fiatConversion={selectedSticker && !!selectedSticker.price}
onChange={(amount) => setTipAmount(amount)} onChange={(amount) => setTipAmount(amount)}
setConvertedAmount={setConvertedAmount} setConvertedAmount={setConvertedAmount}
@ -536,14 +580,7 @@ export function CommentCreate(props: Props) {
<Button <Button
button="primary" button="primary"
label={__('Send')} label={__('Send')}
disabled={ disabled={isSupportComment && (tipError || disableReviewButton)}
(isSupportComment && (tipError || disableReviewButton)) ||
(selectedSticker &&
selectedSticker.price &&
(activeTab === TAB_FIAT
? tipAmount < selectedSticker.price
: convertedAmount && convertedAmount < selectedSticker.price))
}
onClick={() => { onClick={() => {
if (isSupportComment) { if (isSupportComment) {
handleSupportComment(); handleSupportComment();
@ -591,33 +628,38 @@ export function CommentCreate(props: Props) {
{/** Stickers/Support Buttons **/} {/** Stickers/Support Buttons **/}
{!supportDisabled && !stickerSelector && ( {!supportDisabled && !stickerSelector && (
<> <>
{isReviewingStickerComment ? ( {getActionButton(
<Button __('Stickers'),
button="alt" isReviewingStickerComment ? __('Different Sticker') : undefined,
label={__('Different Sticker')} ICONS.STICKER,
onClick={() => { () => {
setReviewingStickerComment(false); if (isReviewingStickerComment) setReviewingStickerComment(false);
setIsSupportComment(false);
setStickerSelector(true);
}}
/>
) : (
getActionButton(__('Stickers'), ICONS.TAG, () => {
setIsSupportComment(false); setIsSupportComment(false);
setStickerSelector(true); setStickerSelector(true);
}) }
)}
{!claimIsMine && (
<>
{(!isSupportComment || activeTab !== TAB_LBC) &&
getActionButton(__('LBC'), isSupportComment ? __('Switch to LBC') : undefined, ICONS.LBC, () => {
setIsSupportComment(true);
setActiveTab(TAB_LBC);
})}
{stripeEnvironment &&
(!isSupportComment || activeTab !== TAB_FIAT) &&
getActionButton(
__('Cash'),
isSupportComment ? __('Switch to Cash') : undefined,
ICONS.FINANCE,
() => {
setIsSupportComment(true);
setActiveTab(TAB_FIAT);
}
)}
</>
)} )}
{!claimIsMine &&
getActionButton(__('LBC'), ICONS.LBC, () => {
setIsSupportComment(true);
setActiveTab(TAB_LBC);
})}
{!claimIsMine &&
stripeEnvironment &&
getActionButton(__('Cash'), ICONS.FINANCE, () => {
setIsSupportComment(true);
setActiveTab(TAB_FIAT);
})}
</> </>
)} )}
@ -635,7 +677,7 @@ export function CommentCreate(props: Props) {
if (isSupportComment || isReviewingSupportComment) { if (isSupportComment || isReviewingSupportComment) {
if (!isReviewingSupportComment) setIsSupportComment(false); if (!isReviewingSupportComment) setIsSupportComment(false);
setReviewingSupportComment(false); setReviewingSupportComment(false);
if (selectedSticker && selectedSticker.price) { if (stickerPrice) {
setReviewingStickerComment(false); setReviewingStickerComment(false);
setStickerSelector(false); setStickerSelector(false);
setSelectedSticker(null); setSelectedSticker(null);

View file

@ -1,102 +1,112 @@
// @flow // @flow
import React from 'react'; import { formatCredits, formatFullPrice } from 'util/format-credits';
import classnames from 'classnames'; import classnames from 'classnames';
import LbcSymbol from 'component/common/lbc-symbol'; import LbcSymbol from 'component/common/lbc-symbol';
import { formatCredits, formatFullPrice } from 'util/format-credits'; import React from 'react';
type Props = { type Props = {
amount: number, amount?: number,
className?: string,
customAmounts?: { amountFiat: number, amountLBC: number },
fee?: boolean,
isEstimate?: boolean,
isFiat?: boolean,
noFormat?: boolean,
precision: number, precision: number,
showFree: boolean, showFree: boolean,
showFullPrice: boolean, showFullPrice: boolean,
showPlus: boolean,
isEstimate?: boolean,
showLBC?: boolean, showLBC?: boolean,
fee?: boolean, showPlus: boolean,
className?: string,
noFormat?: boolean,
size?: number, size?: number,
superChat?: boolean, superChat?: boolean,
superChatLight?: boolean, superChatLight?: boolean,
isFiat?: boolean,
}; };
class CreditAmount extends React.PureComponent<Props> { class CreditAmount extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
noFormat: false,
precision: 2, precision: 2,
showFree: false, showFree: false,
showFullPrice: false, showFullPrice: false,
showPlus: false,
showLBC: true, showLBC: true,
noFormat: false, showPlus: false,
}; };
render() { render() {
const { const {
amount, amount,
precision,
showFullPrice,
showFree,
showPlus,
isEstimate,
fee,
showLBC,
className, className,
customAmounts,
fee,
isEstimate,
isFiat,
noFormat, noFormat,
precision,
showFree,
showFullPrice,
showLBC,
showPlus,
size, size,
superChat, superChat,
superChatLight, superChatLight,
isFiat,
} = this.props; } = this.props;
const minimumRenderableAmount = 10 ** (-1 * precision); const minimumRenderableAmount = 10 ** (-1 * precision);
// return null, otherwise it will try and convert undefined to a string // return null, otherwise it will try and convert undefined to a string
if (amount === undefined) { if (amount === undefined && customAmounts === undefined) return null;
return null;
}
const fullPrice = formatFullPrice(amount, 2);
const isFree = parseFloat(amount) === 0;
let formattedAmount; function getAmountText(amount: number, isFiat?: boolean) {
if (showFullPrice) { const fullPrice = formatFullPrice(amount, 2);
formattedAmount = fullPrice; const isFree = parseFloat(amount) === 0;
} else { let formattedAmount;
formattedAmount =
amount > 0 && amount < minimumRenderableAmount
? `<${minimumRenderableAmount}`
: formatCredits(amount, precision, true);
}
let amountText; if (showFullPrice) {
if (showFree && isFree) { formattedAmount = fullPrice;
amountText = __('Free'); } else {
} else { formattedAmount =
amountText = noFormat ? amount : formattedAmount; amount > 0 && amount < minimumRenderableAmount
? `<${minimumRenderableAmount}`
if (showPlus && amount > 0) { : formatCredits(amount, precision, true);
amountText = `+${amountText}`;
} }
if (showLBC && !isFiat) { if (showFree && isFree) {
amountText = <LbcSymbol postfix={amountText} size={size} />; return __('Free');
} else if (showLBC && isFiat) { } else {
amountText = <p style={{ display: 'inline' }}> ${(Math.round(Number(amountText) * 100) / 100).toFixed(2)}</p>; let amountText = noFormat ? amount : formattedAmount;
}
if (fee) { if (showPlus && amount > 0) {
amountText = __('%amount% fee', { amount: amountText }); amountText = `+${amountText}`;
}
if (showLBC && !isFiat) {
amountText = <LbcSymbol postfix={amountText} size={size} />;
} else if (showLBC && isFiat) {
amountText = <p style={{ display: 'inline' }}> ${(Math.round(Number(amountText) * 100) / 100).toFixed(2)}</p>;
}
if (fee) {
amountText = __('%amount% fee', { amount: amountText });
}
return amountText;
} }
} }
return ( return (
<span <span
title={fullPrice} title={amount ? formatFullPrice(amount, 2) : ''}
className={classnames(className, { className={classnames(className, {
'super-chat': superChat, 'super-chat': superChat,
'super-chat--light': superChatLight, 'super-chat--light': superChatLight,
})} })}
> >
<span className="credit-amount">{amountText}</span> {customAmounts
? Object.values(customAmounts).map((amount, index) => (
<span key={String(amount)} className="credit-amount">
{getAmountText(Number(amount), !index)}
</span>
))
: amount && <span className="credit-amount">{getAmountText(amount, isFiat)}</span>}
{isEstimate ? ( {isEstimate ? (
<span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}> <span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}>

View file

@ -239,7 +239,7 @@ export class FormField extends React.PureComponent<Props> {
{...inputProps} {...inputProps}
/> />
<div className="form-field__textarea-info"> <div className="form-field__textarea-info">
{!noEmojis && ( {!noEmojis && openEmoteMenu && (
<Button <Button
type="alt" type="alt"
className="button--file-action" className="button--file-action"

View file

@ -2534,4 +2534,14 @@ export const icons = {
<line x1="15" y1="9" x2="15.01" y2="9" /> <line x1="15" y1="9" x2="15.01" y2="9" />
</g> </g>
), ),
[ICONS.STICKER]: buildIcon(
<g>
<path d="M7.13,9a.38.38,0,1,1-.38.38A.38.38,0,0,1,7.13,9" />
<path d="M5.51,15.42A7.34,7.34,0,0,0,12,19.34a7.83,7.83,0,0,0,.92-.06" />
<path d="M23.24,11.52A11.25,11.25,0,1,0,12,23.25h.5" />
<path d="M14.45,9.66a2.31,2.31,0,0,1,3.91,0" />
<line x1="23.24" y1="11.52" x2="12.5" y2="23.24" />
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
</g>
),
}; };

View file

@ -21,6 +21,10 @@ import OptimizedImage from 'component/optimizedImage';
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/; const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
function isEmote(title, src) {
return title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons');
}
type SimpleTextProps = { type SimpleTextProps = {
children?: React.Node, children?: React.Node,
}; };
@ -103,8 +107,8 @@ const SimpleImageLink = (props: ImageLinkProps) => {
return null; return null;
} }
if (title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons')) { if (isEmote(title, src)) {
return <OptimizedImage title={title} src={src} />; return <OptimizedImage src={src} title={title} className="emote" waitLoad loading="lazy" />;
} }
return ( return (
@ -194,18 +198,19 @@ const MarkdownPreview = (props: MarkdownProps) => {
), ),
// Workaraund of remarkOptions.Fragment // Workaraund of remarkOptions.Fragment
div: React.Fragment, div: React.Fragment,
img: isStakeEnoughForPreview(stakedLevel) img: (imgProps) =>
? ZoomableImage isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src) ? (
: (imgProps) => ( ZoomableImage
<SimpleImageLink ) : (
src={imgProps.src} <SimpleImageLink
alt={imgProps.alt} src={imgProps.src}
title={imgProps.title} alt={imgProps.alt}
helpText={ title={imgProps.title}
SIMPLE_SITE ? __("This channel isn't staking enough LBRY Credits for inline image previews.") : '' helpText={
} SIMPLE_SITE ? __("This channel isn't staking enough LBRY Credits for inline image previews.") : ''
/> }
), />
),
}, },
}; };

View file

@ -16,7 +16,7 @@ type Props = {
type?: string, type?: string,
uri: string, uri: string,
// below props are just passed to <CreditAmount /> // below props are just passed to <CreditAmount />
customPrice: number, customPrices?: { priceFiat: number, priceLBC: number },
hideFree?: boolean, // hide the file price if it's free hideFree?: boolean, // hide the file price if it's free
isFiat?: boolean, isFiat?: boolean,
showLBC?: boolean, showLBC?: boolean,
@ -50,10 +50,10 @@ class FilePrice extends React.PureComponent<Props> {
claimWasPurchased, claimWasPurchased,
type, type,
claimIsMine, claimIsMine,
customPrice, customPrices,
} = this.props; } = this.props;
if (!customPrice && (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree))) return null; if (!customPrices && (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree))) return null;
const className = classnames(claimWasPurchased ? 'filePrice__key' : 'filePrice', { const className = classnames(claimWasPurchased ? 'filePrice__key' : 'filePrice', {
'filePrice--filepage': type === 'filepage', 'filePrice--filepage': type === 'filepage',
@ -66,7 +66,10 @@ class FilePrice extends React.PureComponent<Props> {
</span> </span>
) : ( ) : (
<CreditAmount <CreditAmount
amount={costInfo ? costInfo.cost : customPrice} amount={costInfo ? costInfo.cost : undefined}
customAmounts={
customPrices ? { amountFiat: customPrices.priceFiat, amountLBC: customPrices.priceLBC } : undefined
}
className={className} className={className}
isEstimate={!!costInfo && !costInfo.includesData} isEstimate={!!costInfo && !costInfo.includesData}
isFiat={isFiat} isFiat={isFiat}

View file

@ -64,7 +64,7 @@ function LivestreamComment(props: Props) {
)} )}
<div className="livestream-comment__body"> <div className="livestream-comment__body">
{(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />} {supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
<div <div
className={classnames('livestream-comment__info', { className={classnames('livestream-comment__info', {
'livestream-comment__info--sticker': Boolean(stickerFromMessage), 'livestream-comment__info--sticker': Boolean(stickerFromMessage),
@ -113,7 +113,7 @@ function LivestreamComment(props: Props) {
{stickerFromMessage ? ( {stickerFromMessage ? (
<div className="sticker__comment"> <div className="sticker__comment">
<OptimizedImage src={stickerFromMessage.url} waitLoad /> <OptimizedImage src={stickerFromMessage.url} waitLoad loading="lazy" />
</div> </div>
) : ( ) : (
<div className="livestream-comment__text"> <div className="livestream-comment__text">

View file

@ -284,7 +284,7 @@ export default function LivestreamComments(props: Props) {
</div> </div>
{stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && ( {stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
<div className="livestream-superchat__info--image"> <div className="livestream-superchat__info--image">
<OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad /> <OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad loading="lazy" />
</div> </div>
)} )}
</div> </div>

View file

@ -2,7 +2,9 @@ import { connect } from 'react-redux';
import { doReadNotifications, doDeleteNotification } from 'redux/actions/notifications'; import { doReadNotifications, doDeleteNotification } from 'redux/actions/notifications';
import Notification from './view'; import Notification from './view';
export default connect(null, { const perform = (dispatch, ownProps) => ({
doReadNotifications, readNotification: () => dispatch(doReadNotifications([ownProps.notification.id])),
doDeleteNotification, deleteNotification: () => dispatch(doDeleteNotification(ownProps.notification.id)),
})(Notification); });
export default connect(null, perform)(Notification);

View file

@ -1,39 +1,42 @@
// @flow // @flow
import * as PAGES from 'constants/pages'; import { formatLbryUrlForWeb } from 'util/url';
import * as ICONS from 'constants/icons'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import { NavLink } from 'react-router-dom';
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
import { parseSticker } from 'util/comments';
import { parseURI } from 'util/lbryURI';
import { RULE } from 'constants/notifications'; import { RULE } from 'constants/notifications';
import React from 'react'; import { useHistory } from 'react-router';
import classnames from 'classnames'; import * as ICONS from 'constants/icons';
import Icon from 'component/common/icon'; import * as PAGES from 'constants/pages';
import DateTime from 'component/dateTime';
import Button from 'component/button'; import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import { formatLbryUrlForWeb } from 'util/url'; import classnames from 'classnames';
import { useHistory } from 'react-router';
import { parseURI } from 'util/lbryURI';
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
import FileThumbnail from 'component/fileThumbnail';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import NotificationContentChannelMenu from 'component/notificationContentChannelMenu';
import LbcMessage from 'component/common/lbc-message';
import UriIndicator from 'component/uriIndicator';
import { NavLink } from 'react-router-dom';
import CommentReactions from 'component/commentReactions';
import CommentCreate from 'component/commentCreate'; import CommentCreate from 'component/commentCreate';
import CommentReactions from 'component/commentReactions';
import CommentsReplies from 'component/commentsReplies'; import CommentsReplies from 'component/commentsReplies';
import DateTime from 'component/dateTime';
import FileThumbnail from 'component/fileThumbnail';
import Icon from 'component/common/icon';
import LbcMessage from 'component/common/lbc-message';
import NotificationContentChannelMenu from 'component/notificationContentChannelMenu';
import OptimizedImage from 'component/optimizedImage';
import React from 'react';
import UriIndicator from 'component/uriIndicator';
type Props = { type Props = {
notification: WebNotification,
menuButton: boolean, menuButton: boolean,
children: any, notification: WebNotification,
doReadNotifications: ([number]) => void, deleteNotification: () => void,
doDeleteNotification: (number) => void, readNotification: () => void,
}; };
export default function Notification(props: Props) { export default function Notification(props: Props) {
const { notification, menuButton = false, doReadNotifications, doDeleteNotification } = props; const { menuButton = false, notification, readNotification, deleteNotification } = props;
const { notification_rule, notification_parameters, is_read } = notification;
const { push } = useHistory(); const { push } = useHistory();
const { notification_rule, notification_parameters, is_read, id } = notification;
const [isReplying, setReplying] = React.useState(false); const [isReplying, setReplying] = React.useState(false);
const [quickReply, setQuickReply] = React.useState(); const [quickReply, setQuickReply] = React.useState();
@ -42,28 +45,14 @@ export default function Notification(props: Props) {
notification_rule === RULE.COMMENT_REPLY || notification_rule === RULE.COMMENT_REPLY ||
notification_rule === RULE.CREATOR_COMMENT; notification_rule === RULE.CREATOR_COMMENT;
const commentText = isCommentNotification && notification_parameters.dynamic.comment; const commentText = isCommentNotification && notification_parameters.dynamic.comment;
const stickerFromComment = isCommentNotification && commentText && parseSticker(commentText);
const notificationTarget = getNotificationTarget();
let notificationTarget; const creatorIcon = (channelUrl) => (
switch (notification_rule) { <UriIndicator uri={channelUrl} link>
case RULE.DAILY_WATCH_AVAILABLE: <ChannelThumbnail small uri={channelUrl} />
case RULE.DAILY_WATCH_REMIND: </UriIndicator>
notificationTarget = `/$/${PAGES.CHANNELS_FOLLOWING}`; );
break;
case RULE.MISSED_OUT:
case RULE.REWARDS_APPROVAL_PROMPT:
notificationTarget = `/$/${PAGES.REWARDS_VERIFY}?redirect=/$/${PAGES.REWARDS}`;
break;
default:
notificationTarget = notification_parameters.device.target;
}
const creatorIcon = (channelUrl) => {
return (
<UriIndicator uri={channelUrl} link>
<ChannelThumbnail small uri={channelUrl} />
</UriIndicator>
);
};
let channelUrl; let channelUrl;
let icon; let icon;
switch (notification_rule) { switch (notification_rule) {
@ -109,8 +98,7 @@ export default function Notification(props: Props) {
let channelName; let channelName;
if (channelUrl) { if (channelUrl) {
try { try {
const { claimName } = parseURI(channelUrl); ({ claimName: channelName } = parseURI(channelUrl));
channelName = claimName;
} catch (e) {} } catch (e) {}
} }
@ -138,25 +126,28 @@ export default function Notification(props: Props) {
try { try {
const { isChannel } = parseURI(notificationTarget); const { isChannel } = parseURI(notificationTarget);
if (isChannel) { if (isChannel) urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
}
} catch (e) {} } catch (e) {}
notificationLink += `?${urlParams.toString()}`; notificationLink += `?${urlParams.toString()}`;
const navLinkProps = { const navLinkProps = { to: notificationLink, onClick: (e) => e.stopPropagation() };
to: notificationLink,
onClick: (e) => e.stopPropagation(), function getNotificationTarget() {
}; switch (notification_rule) {
case RULE.DAILY_WATCH_AVAILABLE:
case RULE.DAILY_WATCH_REMIND:
return `/$/${PAGES.CHANNELS_FOLLOWING}`;
case RULE.MISSED_OUT:
case RULE.REWARDS_APPROVAL_PROMPT:
return `/$/${PAGES.REWARDS_VERIFY}?redirect=/$/${PAGES.REWARDS}`;
default:
return notification_parameters.device.target;
}
}
function handleNotificationClick() { function handleNotificationClick() {
if (!is_read) { if (!is_read) readNotification();
doReadNotifications([id]); if (menuButton && notificationLink) push(notificationLink);
}
if (menuButton && notificationLink) {
push(notificationLink);
}
} }
const Wrapper = menuButton const Wrapper = menuButton
@ -181,45 +172,40 @@ export default function Notification(props: Props) {
); );
return ( return (
<div <div className={classnames('notification__wrapper', { 'notification__wrapper--unread': !is_read })}>
className={classnames('notification__wrapper', {
'notification__wrapper--unread': !is_read,
})}
>
<Wrapper> <Wrapper>
<div className="notification__icon">{icon}</div> <div className="notification__icon">{icon}</div>
<div className="notification__content-wrapper"> <div className="notificationContent__wrapper">
<div className="notification__content"> <div className="notification__content">
<div className="notification__text-wrapper"> <div className="notificationText__wrapper">
{!isCommentNotification && <div className="notification__title">{title}</div>} <div className="notification__title">{title}</div>
{isCommentNotification && commentText ? ( {!commentText ? (
<> <div
<div className="notification__title">{title}</div> title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')}
<div title={commentText} className="notification__text"> className="notification__text"
{commentText} >
</div> <LbcMessage>{notification_parameters.device.text}</LbcMessage>
</> </div>
) : stickerFromComment ? (
<div className="sticker__comment">
<OptimizedImage src={stickerFromComment.url} waitLoad loading="lazy" />
</div>
) : ( ) : (
<> <div title={commentText} className="notification__text">
<div {commentText}
title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')} </div>
className="notification__text"
>
<LbcMessage>{notification_parameters.device.text}</LbcMessage>
</div>
</>
)} )}
</div> </div>
{notification_rule === RULE.NEW_CONTENT && ( {notification_rule === RULE.NEW_CONTENT && (
<FileThumbnail uri={notification_parameters.device.target} className="notification__content-thumbnail" /> <FileThumbnail uri={notification_parameters.device.target} className="notificationContent__thumbnail" />
)} )}
{notification_rule === RULE.NEW_LIVESTREAM && ( {notification_rule === RULE.NEW_LIVESTREAM && (
<FileThumbnail <FileThumbnail
thumbnail={notification_parameters.device.image_url} thumbnail={notification_parameters.device.image_url}
className="notification__content-thumbnail" className="notificationContent__thumbnail"
/> />
)} )}
</div> </div>
@ -227,10 +213,10 @@ export default function Notification(props: Props) {
<div className="notification__extra"> <div className="notification__extra">
{!is_read && ( {!is_read && (
<Button <Button
className="notification__mark-seen" className="notification__markSeen"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
doReadNotifications([id]); readNotification();
}} }}
/> />
)} )}
@ -243,7 +229,7 @@ export default function Notification(props: Props) {
<div className="notification__menu"> <div className="notification__menu">
<Menu> <Menu>
<MenuButton <MenuButton
className={'menu__button notification__menu-button'} className="menu__button notification__menuButton"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -252,7 +238,7 @@ export default function Notification(props: Props) {
<Icon size={18} icon={ICONS.MORE_VERTICAL} /> <Icon size={18} icon={ICONS.MORE_VERTICAL} />
</MenuButton> </MenuButton>
<MenuList className="menu__list"> <MenuList className="menu__list">
<MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}> <MenuItem className="menu__link" onSelect={() => deleteNotification()}>
<Icon aria-hidden icon={ICONS.DELETE} /> <Icon aria-hidden icon={ICONS.DELETE} />
{__('Delete')} {__('Delete')}
</MenuItem> </MenuItem>

View file

@ -26,6 +26,7 @@ type Props = {
claim: StreamClaim, claim: StreamClaim,
convertedAmount?: number, convertedAmount?: number,
customTipAmount?: number, customTipAmount?: number,
exchangeRate?: any,
fiatConversion?: boolean, fiatConversion?: boolean,
tipError: boolean, tipError: boolean,
tipError: string, tipError: string,
@ -44,6 +45,7 @@ function WalletTipAmountSelector(props: Props) {
claim, claim,
convertedAmount, convertedAmount,
customTipAmount, customTipAmount,
exchangeRate,
fiatConversion, fiatConversion,
tipError, tipError,
onChange, onChange,
@ -56,10 +58,16 @@ function WalletTipAmountSelector(props: Props) {
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', true); const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', true);
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false); const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
const [exchangeRate, setExchangeRate] = React.useState();
const convertToTwoDecimalsOrMore = (number: number, decimals: number = 2) =>
Number((Math.round(number * 10 ** decimals) / 10 ** decimals).toFixed(decimals));
const tipAmountsToDisplay = const tipAmountsToDisplay =
customTipAmount && fiatConversion && activeTab === TAB_FIAT ? [customTipAmount] : DEFAULT_TIP_AMOUNTS; customTipAmount && fiatConversion && activeTab === TAB_FIAT
? [customTipAmount]
: customTipAmount && exchangeRate
? [convertToTwoDecimalsOrMore(customTipAmount / exchangeRate)]
: DEFAULT_TIP_AMOUNTS;
// if it's fiat but there's no card saved OR the creator can't receive fiat tips // if it's fiat but there's no card saved OR the creator can't receive fiat tips
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip); const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
@ -81,7 +89,7 @@ function WalletTipAmountSelector(props: Props) {
((amount > balance || balance === 0) && activeTab !== TAB_FIAT) || ((amount > balance || balance === 0) && activeTab !== TAB_FIAT) ||
shouldDisableFiatSelectors || shouldDisableFiatSelectors ||
(customTipAmount && fiatConversion && activeTab !== TAB_FIAT && exchangeRate (customTipAmount && fiatConversion && activeTab !== TAB_FIAT && exchangeRate
? amount * exchangeRate < customTipAmount ? convertToTwoDecimalsOrMore(amount * exchangeRate) < customTipAmount
: customTipAmount && amount < customTipAmount) : customTipAmount && amount < customTipAmount)
); );
} }
@ -95,19 +103,13 @@ function WalletTipAmountSelector(props: Props) {
} }
} }
function convertToTwoDecimals(number: number) {
return (Math.round(number * 100) / 100).toFixed(2);
}
React.useEffect(() => { React.useEffect(() => {
if (!exchangeRate) { if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD));
} else if ((!convertedAmount || convertedAmount !== amount * exchangeRate) && setConvertedAmount) {
setConvertedAmount(amount * exchangeRate); setConvertedAmount(amount * exchangeRate);
} }
}, [amount, convertedAmount, exchangeRate, setConvertedAmount]); }, [amount, convertedAmount, exchangeRate, setConvertedAmount]);
// check if creator has a payment method saved // check if user has a payment method saved
React.useEffect(() => { React.useEffect(() => {
if (!stripeEnvironment) return; if (!stripeEnvironment) return;
@ -129,6 +131,7 @@ function WalletTipAmountSelector(props: Props) {
}); });
}, [setHasSavedCard]); }, [setHasSavedCard]);
// check if creator has a tip account saved
React.useEffect(() => { React.useEffect(() => {
if (!stripeEnvironment) return; if (!stripeEnvironment) return;
@ -171,6 +174,25 @@ function WalletTipAmountSelector(props: Props) {
setTipError(__('Not enough Credits')); setTipError(__('Not enough Credits'));
} else if (amount < MINIMUM_PUBLISH_BID) { } else if (amount < MINIMUM_PUBLISH_BID) {
setTipError(__('Amount must be higher')); setTipError(__('Amount must be higher'));
} else if (
convertedAmount &&
exchangeRate &&
customTipAmount &&
amount < convertToTwoDecimalsOrMore(customTipAmount / exchangeRate)
) {
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
const validCustomTipInput = regexp.test(String(amount));
if (validCustomTipInput) {
setTipError(
__('Amount of $%input_amount% LBC in USB is lower than price of $%price_amount%', {
input_amount: convertToTwoDecimalsOrMore(convertedAmount, 4),
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
})
);
} else {
setTipError(__('Amount must have no more than 2 decimal places'));
}
} else { } else {
setTipError(false); setTipError(false);
} }
@ -185,35 +207,43 @@ function WalletTipAmountSelector(props: Props) {
setTipError(__('Amount must be at least one dollar')); setTipError(__('Amount must be at least one dollar'));
} else if (amount > 1000) { } else if (amount > 1000) {
setTipError(__('Amount cannot be over 1000 dollars')); setTipError(__('Amount cannot be over 1000 dollars'));
} else if (customTipAmount && amount < customTipAmount) {
setTipError(
__('Amount is lower than price of $%price_amount%', {
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
})
);
} else { } else {
setTipError(false); setTipError(false);
} }
} }
} }
}, [activeTab, amount, balance, setTipError]); }, [activeTab, amount, balance, convertedAmount, customTipAmount, exchangeRate, setTipError]);
const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>; const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>;
return ( return (
<> <>
<div className="section"> <div className="section">
{tipAmountsToDisplay.map((defaultAmount) => ( {tipAmountsToDisplay &&
<Button tipAmountsToDisplay.map((defaultAmount) => (
key={defaultAmount} <Button
disabled={shouldDisableAmountSelector(defaultAmount)} key={defaultAmount}
button="alt" disabled={shouldDisableAmountSelector(defaultAmount)}
className={classnames('button-toggle button-toggle--expandformobile', { button="alt"
'button-toggle--active': defaultAmount === amount && !useCustomTip, className={classnames('button-toggle button-toggle--expandformobile', {
'button-toggle--disabled': amount > balance, 'button-toggle--active':
})} convertToTwoDecimalsOrMore(defaultAmount) === convertToTwoDecimalsOrMore(amount) && !useCustomTip,
label={defaultAmount} 'button-toggle--disabled': amount > balance,
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} })}
onClick={() => { label={defaultAmount}
handleCustomPriceChange(defaultAmount); icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
setUseCustomTip(false); onClick={() => {
}} handleCustomPriceChange(defaultAmount);
/> setUseCustomTip(false);
))} }}
/>
))}
<Button <Button
button="alt" button="alt"
@ -240,13 +270,13 @@ function WalletTipAmountSelector(props: Props) {
fiatConversion && fiatConversion &&
activeTab !== TAB_FIAT && activeTab !== TAB_FIAT &&
getHelpMessage( getHelpMessage(
__( __('This support is priced in $USD.') +
`This support is priced in $USD. ${ (convertedAmount
convertedAmount ? ' ' +
? __(`The current exchange rate for the submitted amount is: $${convertToTwoDecimals(convertedAmount)}`) __('The current exchange rate for the submitted LBC amount is ~ $%exchange_amount%.', {
: '' exchange_amount: convertToTwoDecimalsOrMore(convertedAmount),
}` })
) : '')
)} )}
{/* custom number input form */} {/* custom number input form */}
@ -274,7 +304,7 @@ function WalletTipAmountSelector(props: Props) {
? getHelpMessage( ? getHelpMessage(
<> <>
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" /> <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
{__(' To Tip Creators')} {' ' + __('To Tip Creators')}
</> </>
) )
: !canReceiveFiatTip : !canReceiveFiatTip

View file

@ -9,24 +9,23 @@ const buildEmote = (name: string, path: string) => ({
const getEmotes = (px: string, multiplier: string) => [ const getEmotes = (px: string, multiplier: string) => [
buildEmote('ALIEN', `${px}/Alien${multiplier}.png`), buildEmote('ALIEN', `${px}/Alien${multiplier}.png`),
buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`),
buildEmote('ANGRY_2', `${px}/angry%202${multiplier}.png`), buildEmote('ANGRY_2', `${px}/angry%202${multiplier}.png`),
buildEmote('ANGRY_3', `${px}/angry%203${multiplier}.png`), buildEmote('ANGRY_3', `${px}/angry%203${multiplier}.png`),
buildEmote('ANGRY_4', `${px}/angry%204${multiplier}.png`), buildEmote('ANGRY_4', `${px}/angry%204${multiplier}.png`),
buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`),
buildEmote('BLIND', `${px}/blind${multiplier}.png`), buildEmote('BLIND', `${px}/blind${multiplier}.png`),
buildEmote('BLOCK', `${px}/block${multiplier}.png`), buildEmote('BLOCK', `${px}/block${multiplier}.png`),
buildEmote('BOMB', `${px}/bomb${multiplier}.png`), buildEmote('BOMB', `${px}/bomb${multiplier}.png`),
buildEmote('BRAIN_CHIP', `${px}/Brain%20chip${multiplier}.png`), buildEmote('BRAIN_CHIP', `${px}/Brain%20chip${multiplier}.png`),
buildEmote('CONFIRM', `${px}/CONFIRM${multiplier}.png`), buildEmote('CONFIRM', `${px}/CONFIRM${multiplier}.png`),
buildEmote('CONFUSED_1', `${px}/confused-1${multiplier}.png`), buildEmote('CONFUSED_1', `${px}/confused${multiplier}-1.png`),
buildEmote('CONFUSED_2', `${px}/confused${multiplier}.png`), buildEmote('CONFUSED_2', `${px}/confused${multiplier}.png`),
buildEmote('COOKING_SOMETHING_NICE', `${px}/cooking%20something%20nice${multiplier}.png`), buildEmote('COOKING_SOMETHING_NICE', `${px}/cooking%20something%20nice${multiplier}.png`),
buildEmote('CRY_1', `${px}/cry${multiplier}.png`),
buildEmote('CRY_2', `${px}/cry%202${multiplier}.png`), buildEmote('CRY_2', `${px}/cry%202${multiplier}.png`),
buildEmote('CRY_3', `${px}/cry%203${multiplier}.png`), buildEmote('CRY_3', `${px}/cry%203${multiplier}.png`),
buildEmote('CRY_4', `${px}/cry%204${multiplier}.png`), buildEmote('CRY_4', `${px}/cry%204${multiplier}.png`),
buildEmote('CRY_5', `${px}/cry%205${multiplier}.png`), buildEmote('CRY_5', `${px}/cry%205${multiplier}.png`),
buildEmote('CRY_1', `${px}/cry${multiplier}.png`),
buildEmote('SPACE_DOGE', `${px}/doge${multiplier}.png`),
buildEmote('DONUT', `${px}/donut${multiplier}.png`), buildEmote('DONUT', `${px}/donut${multiplier}.png`),
buildEmote('EGGPLANT_WITH_CONDOM', `${px}/eggplant%20with%20condom${multiplier}.png`), buildEmote('EGGPLANT_WITH_CONDOM', `${px}/eggplant%20with%20condom${multiplier}.png`),
buildEmote('EGGPLANT', `${px}/eggplant${multiplier}.png`), buildEmote('EGGPLANT', `${px}/eggplant${multiplier}.png`),
@ -37,27 +36,26 @@ const getEmotes = (px: string, multiplier: string) => [
buildEmote('HYPER_TROLL', `${px}/HyperTroll${multiplier}.png`), buildEmote('HYPER_TROLL', `${px}/HyperTroll${multiplier}.png`),
buildEmote('ICE_CREAM', `${px}/ice%20cream${multiplier}.png`), buildEmote('ICE_CREAM', `${px}/ice%20cream${multiplier}.png`),
buildEmote('IDK', `${px}/IDK${multiplier}.png`), buildEmote('IDK', `${px}/IDK${multiplier}.png`),
buildEmote('ILLUMINATI_1', `${px}/Illuminati-1${multiplier}.png`), buildEmote('ILLUMINATI_1', `${px}/Illuminati${multiplier}-1.png`),
buildEmote('ILLUMINATI_2', `${px}/Illuminati${multiplier}.png`), buildEmote('ILLUMINATI_2', `${px}/Illuminati${multiplier}.png`),
buildEmote('KISS_2', `${px}/kiss%202${multiplier}.png`),
buildEmote('KISS_1', `${px}/kiss${multiplier}.png`), buildEmote('KISS_1', `${px}/kiss${multiplier}.png`),
buildEmote('KISS_2', `${px}/kiss%202${multiplier}.png`),
buildEmote('LASER_GUN', `${px}/laser%20gun${multiplier}.png`), buildEmote('LASER_GUN', `${px}/laser%20gun${multiplier}.png`),
buildEmote('LAUGHING_2', `${px}/Laughing 2${multiplier}.png`),
buildEmote('LAUGHING_1', `${px}/Laughing${multiplier}.png`), buildEmote('LAUGHING_1', `${px}/Laughing${multiplier}.png`),
buildEmote('LAUGHING_2', `${px}/Laughing 2${multiplier}.png`),
buildEmote('LOLLIPOP', `${px}/Lollipop${multiplier}.png`), buildEmote('LOLLIPOP', `${px}/Lollipop${multiplier}.png`),
buildEmote('LOVE_2', `${px}/Love%202${multiplier}.png`),
buildEmote('LOVE_1', `${px}/Love${multiplier}.png`), buildEmote('LOVE_1', `${px}/Love${multiplier}.png`),
buildEmote('LOVE_2', `${px}/Love%202${multiplier}.png`),
buildEmote('MONSTER', `${px}/Monster${multiplier}.png`), buildEmote('MONSTER', `${px}/Monster${multiplier}.png`),
buildEmote('MUSHROOM', `${px}/mushroom${multiplier}.png`), buildEmote('MUSHROOM', `${px}/mushroom${multiplier}.png`),
buildEmote('NAIL_IT', `${px}/Nail%20It${multiplier}.png`), buildEmote('NAIL_IT', `${px}/Nail%20It${multiplier}.png`),
buildEmote('NO', `${px}/NO${multiplier}.png`), buildEmote('NO', `${px}/NO${multiplier}.png`),
buildEmote('OUCH', `${px}/ouch${multiplier}.png`), buildEmote('OUCH', `${px}/ouch${multiplier}.png`),
buildEmote('PREACE', `${px}/peace${multiplier}.png`),
buildEmote('PIZZA', `${px}/pizza${multiplier}.png`), buildEmote('PIZZA', `${px}/pizza${multiplier}.png`),
buildEmote('PREACE', `${px}/peace${multiplier}.png`),
buildEmote('RABBIT_HOLE', `${px}/rabbit%20hole${multiplier}.png`), buildEmote('RABBIT_HOLE', `${px}/rabbit%20hole${multiplier}.png`),
buildEmote('RAINBOW_PUKE_1', `${px}/rainbow%20puke-1${multiplier}.png`), buildEmote('RAINBOW_PUKE_1', `${px}/rainbow%20puke${multiplier}-1.png`),
buildEmote('RAINBOW_PUKE_2', `${px}/rainbow%20puke${multiplier}.png`), buildEmote('RAINBOW_PUKE_2', `${px}/rainbow%20puke${multiplier}.png`),
buildEmote('SPACE_RESITAS', `${px}/resitas${multiplier}.png`),
buildEmote('ROCK', `${px}/ROCK${multiplier}.png`), buildEmote('ROCK', `${px}/ROCK${multiplier}.png`),
buildEmote('SAD', `${px}/sad${multiplier}.png`), buildEmote('SAD', `${px}/sad${multiplier}.png`),
buildEmote('SALTY', `${px}/salty${multiplier}.png`), buildEmote('SALTY', `${px}/salty${multiplier}.png`),
@ -65,22 +63,24 @@ const getEmotes = (px: string, multiplier: string) => [
buildEmote('SLEEP', `${px}/Sleep${multiplier}.png`), buildEmote('SLEEP', `${px}/Sleep${multiplier}.png`),
buildEmote('SLIME_DOWN', `${px}/slime%20down${multiplier}.png`), buildEmote('SLIME_DOWN', `${px}/slime%20down${multiplier}.png`),
buildEmote('SMELLY_SOCKS', `${px}/smelly%20socks${multiplier}.png`), buildEmote('SMELLY_SOCKS', `${px}/smelly%20socks${multiplier}.png`),
buildEmote('SMILE_2', `${px}/smile%202${multiplier}.png`),
buildEmote('SMILE_1', `${px}/smile${multiplier}.png`), buildEmote('SMILE_1', `${px}/smile${multiplier}.png`),
buildEmote('SMILE_2', `${px}/smile%202${multiplier}.png`),
buildEmote('SPACE_CHAD', `${px}/space%20chad${multiplier}.png`), buildEmote('SPACE_CHAD', `${px}/space%20chad${multiplier}.png`),
buildEmote('SPACE_DOGE', `${px}/doge${multiplier}.png`),
buildEmote('SPACE_GREEN_WOJAK', `${px}/space%20wojak${multiplier}-1.png`),
buildEmote('SPACE_JULIAN', `${px}/Space%20Julian${multiplier}.png`), buildEmote('SPACE_JULIAN', `${px}/Space%20Julian${multiplier}.png`),
buildEmote('SPACE_TOM', `${px}/space%20Tom${multiplier}.png`),
buildEmote('SPACE_GREEN_WOJAK', `${px}/space%20wojak-1${multiplier}.png`),
buildEmote('SPACE_RED_WOJAK', `${px}/space%20wojak${multiplier}.png`), buildEmote('SPACE_RED_WOJAK', `${px}/space%20wojak${multiplier}.png`),
buildEmote('SPACE_RESITAS', `${px}/resitas${multiplier}.png`),
buildEmote('SPACE_TOM', `${px}/space%20Tom${multiplier}.png`),
buildEmote('SPOCK', `${px}/SPOCK${multiplier}.png`), buildEmote('SPOCK', `${px}/SPOCK${multiplier}.png`),
buildEmote('STAR', `${px}/Star${multiplier}.png`), buildEmote('STAR', `${px}/Star${multiplier}.png`),
buildEmote('SUNNY_DAY', `${px}/sunny%20day${multiplier}.png`), buildEmote('SUNNY_DAY', `${px}/sunny%20day${multiplier}.png`),
buildEmote('SUPRISED', `${px}/surprised${multiplier}.png`), buildEmote('SUPRISED', `${px}/surprised${multiplier}.png`),
buildEmote('SWEET', `${px}/sweet${multiplier}.png`), buildEmote('SWEET', `${px}/sweet${multiplier}.png`),
buildEmote('THINKING_1', `${px}/thinking-1${multiplier}.png`), buildEmote('THINKING_1', `${px}/thinking${multiplier}-1.png`),
buildEmote('THINKING_2', `${px}/thinking${multiplier}.png`), buildEmote('THINKING_2', `${px}/thinking${multiplier}.png`),
buildEmote('THUMB_DOWN', `${px}/thumb%20down${multiplier}.png`), buildEmote('THUMB_DOWN', `${px}/thumb%20down${multiplier}.png`),
buildEmote('THUMB_UP_1', `${px}/thumb%20up-1${multiplier}.png`), buildEmote('THUMB_UP_1', `${px}/thumb%20up${multiplier}-1.png`),
buildEmote('THUMB_UP_2', `${px}/thumb%20up${multiplier}.png`), buildEmote('THUMB_UP_2', `${px}/thumb%20up${multiplier}.png`),
buildEmote('TINFOIL_HAT', `${px}/tin%20hat${multiplier}.png`), buildEmote('TINFOIL_HAT', `${px}/tin%20hat${multiplier}.png`),
buildEmote('TROLL_KING', `${px}/Troll%20king${multiplier}.png`), buildEmote('TROLL_KING', `${px}/Troll%20king${multiplier}.png`),
@ -91,6 +91,6 @@ const getEmotes = (px: string, multiplier: string) => [
]; ];
export const EMOTES_24px = getEmotes('24%20px', ''); export const EMOTES_24px = getEmotes('24%20px', '');
export const EMOTES_36px = getEmotes('36px', '@1.5x'); export const EMOTES_36px = getEmotes('36px', '%401.5x');
export const EMOTES_48px = getEmotes('48%20px', '@2x'); export const EMOTES_48px = getEmotes('48%20px', '%402x');
export const EMOTES_72px = getEmotes('72%20px', '@3x'); export const EMOTES_72px = getEmotes('72%20px', '%403x');

View file

@ -180,3 +180,4 @@ export const ARTISTS = 'Artists';
export const MYSTERIES = 'Mysteries'; export const MYSTERIES = 'Mysteries';
export const TECHNOLOGY = 'Technology'; export const TECHNOLOGY = 'Technology';
export const EMOJI = 'Emoji'; export const EMOJI = 'Emoji';
export const STICKER = 'Sticker';

View file

@ -106,5 +106,16 @@ $thumbnailWidthSmall: 1rem;
.filePrice { .filePrice {
height: 1.5rem; height: 1.5rem;
width: 10rem; width: 10rem;
.credit-amount:not(:last-child) {
&::after {
margin-left: var(--spacing-xxs);
content: '/';
}
}
.credit-amount:not(:first-child) {
margin-left: var(--spacing-xxs);
}
} }
} }

View file

@ -489,3 +489,8 @@ $thumbnailWidthSmall: 1rem;
max-height: 100%; max-height: 100%;
} }
} }
.emote {
max-width: 1.5rem;
max-height: 1.5rem;
}

View file

@ -117,7 +117,7 @@
} }
// Image // Image
img:not(.channel-thumbnail__custom) { img:not(.channel-thumbnail__custom):not(.emote) {
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
padding-top: var(--spacing-m); padding-top: var(--spacing-m);
max-height: var(--inline-player-max-height); max-height: var(--inline-player-max-height);

View file

@ -13,6 +13,12 @@ $contentMaxWidth: 60rem;
&:first-of-type { &:first-of-type {
border-top: none; border-top: none;
} }
@media (min-width: $breakpoint-small) {
&:hover {
background-color: var(--color-card-background-highlighted);
}
}
} }
.commentCreate, .commentCreate,
@ -25,7 +31,7 @@ $contentMaxWidth: 60rem;
.notification__icon { .notification__icon {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
margin: auto; margin-top: var(--spacing-xxs);
.icon__wrapper { .icon__wrapper {
width: 1rem; width: 1rem;
@ -94,7 +100,7 @@ $contentMaxWidth: 60rem;
} }
} }
.notification__content-wrapper { .notificationContent__wrapper {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -121,7 +127,7 @@ $contentMaxWidth: 60rem;
} }
} }
.notification__content-thumbnail { .notificationContent__thumbnail {
@include thumbnail; @include thumbnail;
position: relative; position: relative;
margin-left: auto; margin-left: auto;
@ -139,8 +145,13 @@ $contentMaxWidth: 60rem;
} }
} }
.notification__text-wrapper { .notificationText__wrapper {
max-width: calc(#{$contentMaxWidth} - (#{$thumbnailWidth} * 16 / 9) - var(--spacing-m)); max-width: calc(#{$contentMaxWidth} - (#{$thumbnailWidth} * 16 / 9) - var(--spacing-m));
.sticker__comment {
width: 4.5rem;
height: 4.5rem;
}
} }
.notification__title { .notification__title {
@ -247,7 +258,7 @@ $contentMaxWidth: 60rem;
} }
} }
.notification__mark-seen { .notification__markSeen {
height: 12px; height: 12px;
width: 12px; width: 12px;
border-radius: 50%; border-radius: 50%;

View file

@ -1,4 +1,4 @@
import { EMOTES_24px as EMOTES } from 'constants/emotes'; import { EMOTES_48px as EMOTES } from 'constants/emotes';
import visit from 'unist-util-visit'; import visit from 'unist-util-visit';
const EMOTE_NODE_TYPE = 'emote'; const EMOTE_NODE_TYPE = 'emote';