diff --git a/static/app-strings.json b/static/app-strings.json index 9a9fbc5a1..092880224 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2235,133 +2235,8 @@ "Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.": "Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.", "Content: Limit (GB)": "Content: Limit (GB)", "Network: Allow (GB)": "Network: Allow (GB)", - "Failed to view lbry://@Destiny#6/destiny-crashes-conservative-panel-w#a, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@Destiny#6/destiny-crashes-conservative-panel-w#a, please try again. If this problem persists, visit https://lbry.com/faq/support for support.", "A channel is required to repost on LBRY": "A channel is required to repost on LBRY", - "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.", "Admin": "Admin", - ":ALIEN:": ":ALIEN:", - ":ANGRY_2:": ":ANGRY_2:", - ":ANGRY_3:": ":ANGRY_3:", - ":ANGRY_4:": ":ANGRY_4:", - ":ANGRY_1:": ":ANGRY_1:", - ":BLIND:": ":BLIND:", - ":BLOCK:": ":BLOCK:", - ":BOMB:": ":BOMB:", - ":BRAIN_CHIP:": ":BRAIN_CHIP:", - ":CONFIRM:": ":CONFIRM:", - ":CONFUSED_1:": ":CONFUSED_1:", - ":CONFUSED_2:": ":CONFUSED_2:", - ":COOKING_SOMETHING_NICE:": ":COOKING_SOMETHING_NICE:", - ":CRY_2:": ":CRY_2:", - ":CRY_3:": ":CRY_3:", - ":CRY_4:": ":CRY_4:", - ":CRY_5:": ":CRY_5:", - ":CRY_1:": ":CRY_1:", - ":SPACE_DOGE:": ":SPACE_DOGE:", - ":DONUT:": ":DONUT:", - ":EGGPLANT_WITH_CONDOM:": ":EGGPLANT_WITH_CONDOM:", - ":EGGPLANT:": ":EGGPLANT:", - ":FIRE_UP:": ":FIRE_UP:", - ":FLAT_EARTH:": ":FLAT_EARTH:", - ":FLYING_SAUCER:": ":FLYING_SAUCER:", - ":HEART_CHOPPER:": ":HEART_CHOPPER:", - ":HYPER_TROLL:": ":HYPER_TROLL:", - ":ICE_CREAM:": ":ICE_CREAM:", - ":IDK:": ":IDK:", - ":ILLUMINATI_1:": ":ILLUMINATI_1:", - ":ILLUMINATI_2:": ":ILLUMINATI_2:", - ":KISS_2:": ":KISS_2:", - ":KISS_1:": ":KISS_1:", - ":LASER_GUN:": ":LASER_GUN:", - ":LAUGHING_2:": ":LAUGHING_2:", - ":LAUGHING_1:": ":LAUGHING_1:", - ":LOLLIPOP:": ":LOLLIPOP:", - ":LOVE_2:": ":LOVE_2:", - ":LOVE_1:": ":LOVE_1:", - ":MONSTER:": ":MONSTER:", - ":MUSHROOM:": ":MUSHROOM:", - ":NAIL_IT:": ":NAIL_IT:", - ":NO:": ":NO:", - ":OUCH:": ":OUCH:", - ":PREACE:": ":PREACE:", - ":PIZZA:": ":PIZZA:", - ":RABBIT_HOLE:": ":RABBIT_HOLE:", - ":RAINBOW_PUKE_1:": ":RAINBOW_PUKE_1:", - ":RAINBOW_PUKE_2:": ":RAINBOW_PUKE_2:", - ":SPACE_RESITAS:": ":SPACE_RESITAS:", - ":ROCK:": ":ROCK:", - ":SAD:": ":SAD:", - ":SALTY:": ":SALTY:", - ":SCARY:": ":SCARY:", - ":SLEEP:": ":SLEEP:", - ":SLIME_DOWN:": ":SLIME_DOWN:", - ":SMELLY_SOCKS:": ":SMELLY_SOCKS:", - ":SMILE_2:": ":SMILE_2:", - ":SMILE_1:": ":SMILE_1:", - ":SPACE_CHAD:": ":SPACE_CHAD:", - ":SPACE_JULIAN:": ":SPACE_JULIAN:", - ":SPACE_TOM:": ":SPACE_TOM:", - ":SPACE_WOJAK_1:": ":SPACE_WOJAK_1:", - ":SPOCK:": ":SPOCK:", - ":STAR:": ":STAR:", - ":SUNNY_DAY:": ":SUNNY_DAY:", - ":SUPRISED:": ":SUPRISED:", - ":SWEET:": ":SWEET:", - ":THINKING_1:": ":THINKING_1:", - ":THINKING_2:": ":THINKING_2:", - ":THUMB_DOWN:": ":THUMB_DOWN:", - ":THUMB_UP_1:": ":THUMB_UP_1:", - ":THUMB_UP_2:": ":THUMB_UP_2:", - ":TINFOIL_HAT:": ":TINFOIL_HAT:", - ":TROLL_KING:": ":TROLL_KING:", - ":UFO:": ":UFO:", - ":WAITING:": ":WAITING:", - ":WHAT:": ":WHAT:", - ":WOODOO_DOLL:": ":WOODOO_DOLL:", - "Global Emotes": "Global Emotes", - ":CAT:": ":CAT:", - ":FAIL:": ":FAIL:", - ":HYPE:": ":HYPE:", - ":PANTS_1:": ":PANTS_1:", - ":PANTS_2:": ":PANTS_2:", - ":PISS:": ":PISS:", - ":PREGNANT_MAN_ASIA:": ":PREGNANT_MAN_ASIA:", - ":PREGNANT_MAN_BLACK_HAIR:": ":PREGNANT_MAN_BLACK_HAIR:", - ":PREGNANT_MAN_BLACK_SKIN:": ":PREGNANT_MAN_BLACK_SKIN:", - ":PREGNANT_MAN_BLONDE:": ":PREGNANT_MAN_BLONDE:", - ":PREGNANT_MAN_RED_HAIR:": ":PREGNANT_MAN_RED_HAIR:", - ":PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT:": ":PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT:", - ":PREGNANT_WOMAN_BLACK_HAIR:": ":PREGNANT_WOMAN_BLACK_HAIR:", - ":PREGNANT_WOMAN_BLACK_SKIN:": ":PREGNANT_WOMAN_BLACK_SKIN:", - ":PREGNANT_WOMAN_BLONDE:": ":PREGNANT_WOMAN_BLONDE:", - ":PREGNANT_WOMAN_BROWN_HAIR:": ":PREGNANT_WOMAN_BROWN_HAIR:", - ":PREGNANT_WOMAN_RED_HAIR:": ":PREGNANT_WOMAN_RED_HAIR:", - ":ROCKET_SPACEMAN:": ":ROCKET_SPACEMAN:", - ":SICK_FLAME:": ":SICK_FLAME:", - ":SICK_SKULL:": ":SICK_SKULL:", - ":SLIME:": ":SLIME:", - ":SPHAGETTI_BATH:": ":SPHAGETTI_BATH:", - ":THUG_LIFE:": ":THUG_LIFE:", - ":WHUUT:": ":WHUUT:", - ":TIP_HAND_FLIP:": ":TIP_HAND_FLIP:", - ":TIP_HAND_FLIP_COIN:": ":TIP_HAND_FLIP_COIN:", - ":TIP_HAND_FLIP_LBC:": ":TIP_HAND_FLIP_LBC:", - ":COMET_TIP:": ":COMET_TIP:", - ":LBC_COMET_TIP:": ":LBC_COMET_TIP:", - ":SMALL_TIP:": ":SMALL_TIP:", - ":SILVER_ODYSEE_COIN:": ":SILVER_ODYSEE_COIN:", - ":SMALL_LBC_TIP:": ":SMALL_LBC_TIP:", - ":BITE_TIP:": ":BITE_TIP:", - ":BITE_TIP_CLOSEUP:": ":BITE_TIP_CLOSEUP:", - ":BITE_LBC_CLOSEUP:": ":BITE_LBC_CLOSEUP:", - ":MEDIUM_TIP:": ":MEDIUM_TIP:", - ":MEDIUM_LBC_TIP:": ":MEDIUM_LBC_TIP:", - ":LARGE_TIP:": ":LARGE_TIP:", - ":LARGE_LBC_TIP:": ":LARGE_LBC_TIP:", - ":BIG_TIP:": ":BIG_TIP:", - ":BIG_LBC_TIP:": ":BIG_LBC_TIP:", - ":FORTUNE_CHEST:": ":FORTUNE_CHEST:", - ":FORTUNE_CHEST_LBC:": ":FORTUNE_CHEST_LBC:", "Stickers": "Stickers", "Different Sticker": "Different Sticker", "LBC": "LBC", diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index 46410a77e..b33af33d8 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -329,7 +329,7 @@ function Comment(props: Props) { </div> ) : stickerFromMessage ? ( <div className="sticker__comment"> - <OptimizedImage src={stickerFromMessage.url} waitLoad /> + <OptimizedImage src={stickerFromMessage.url} waitLoad loading="lazy" /> </div> ) : editedMessage.length >= LENGTH_TO_COLLAPSE ? ( <Expandable> diff --git a/ui/component/commentCreate/emote-selector.jsx b/ui/component/commentCreate/emote-selector.jsx index 1dc1a1bd6..20fa2683e 100644 --- a/ui/component/commentCreate/emote-selector.jsx +++ b/ui/component/commentCreate/emote-selector.jsx @@ -1,6 +1,6 @@ // @flow 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 Button from 'component/button'; import EMOJIS from 'emoji-dictionary'; @@ -32,8 +32,6 @@ export default function EmoteSelector(props: Props) { <div className="emoteSelector__list"> <div className="emoteSelector__listRow"> - <div className="emoteSelector__listRowTitle">{__('Global Emotes')}</div> - <div className="emoteSelector__listRowItems"> {OLD_QUICK_EMOJIS.map((emoji) => ( <Button @@ -50,7 +48,7 @@ export default function EmoteSelector(props: Props) { return ( <Button - key={String(emote)} + key={emoteName} title={emoteName} button="alt" className="button--file-action" diff --git a/ui/component/commentCreate/sticker-selector.jsx b/ui/component/commentCreate/sticker-selector.jsx index 85fda7d82..c82aa9df8 100644 --- a/ui/component/commentCreate/sticker-selector.jsx +++ b/ui/component/commentCreate/sticker-selector.jsx @@ -47,7 +47,7 @@ export default function StickerSelector(props: Props) { className="button--file-action" onClick={() => onSelect(sticker)} > - <OptimizedImage src={sticker.url} waitLoad /> + <OptimizedImage src={sticker.url} waitLoad loading="lazy" /> {sticker.price && sticker.price > 0 && ( <CreditAmount superChatLight amount={sticker.price} size={2} isFiat /> )} diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 44b683332..07de2d431 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -26,6 +26,7 @@ import type { ElementRef } from 'react'; import UriIndicator from 'component/uriIndicator'; import usePersistedState from 'effects/use-persisted-state'; import WalletTipAmountSelector from 'component/walletTipAmountSelector'; +import { Lbryio } from 'lbryinc'; import { getStripeEnvironment } from 'util/stripe'; const stripeEnvironment = getStripeEnvironment(); @@ -111,12 +112,14 @@ export function CommentCreate(props: Props) { const [commentValue, setCommentValue] = React.useState(''); const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false); const [stickerSelector, setStickerSelector] = React.useState(); - const [activeTab, setActiveTab] = React.useState(''); + const [activeTab, setActiveTab] = React.useState(); const [tipError, setTipError] = React.useState(); const [deletedComment, setDeletedComment] = React.useState(false); const [pauseQuickSend, setPauseQuickSend] = React.useState(false); const [showEmotes, setShowEmotes] = React.useState(false); const [disableReviewButton, setDisableReviewButton] = React.useState(); + const [exchangeRate, setExchangeRate] = React.useState(); + const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined); const selectedMentionIndex = commentValue.indexOf('@', selectionIndex) === selectionIndex @@ -145,6 +148,7 @@ export function CommentCreate(props: Props) { const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0; const minAmount = minTip || minSuper || 0; const minAmountMet = minAmount === 0 || tipAmount >= minAmount; + const stickerPrice = selectedSticker && selectedSticker.price; const minAmountRef = React.useRef(minAmount); minAmountRef.current = minAmount; @@ -320,7 +324,7 @@ export function CommentCreate(props: Props) { if (setQuickReply) setQuickReply(res); if (res && res.signature) { - setCommentValue(''); + if (!stickerValue) setCommentValue(''); setReviewingSupportComment(false); setIsSupportComment(false); setCommentFailure(false); @@ -374,12 +378,45 @@ export function CommentCreate(props: Props) { return () => clearTimeout(timer); }, [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 // ************************************************************************** - const getActionButton = (title: string, icon: string, handleClick: () => void) => ( - <Button title={title} button="alt" icon={icon} onClick={handleClick} /> + const getActionButton = (title: string, label?: string, icon: string, handleClick: () => void) => ( + <Button title={title} label={label} button="alt" icon={icon} onClick={handleClick} /> ); if (channelSettings && !channelSettings.comments_enabled) { @@ -405,6 +442,7 @@ export function CommentCreate(props: Props) { return ( <Form + onSubmit={() => {}} className={classnames('commentCreate', { 'commentCreate--reply': isReply, 'commentCreate--nestedReply': isNested, @@ -421,9 +459,15 @@ export function CommentCreate(props: Props) { <UriIndicator uri={activeChannelClaim.canonical_url} link /> </div> <div className="commentCreate__stickerPreviewImage"> - <OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad /> + <OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" /> </div> - {selectedSticker.price && <FilePrice customPrice={selectedSticker.price} isFiat />} + {/* figure out lbc sticker prices */} + {selectedSticker.price && exchangeRate && ( + <FilePrice + customPrices={{ priceFiat: selectedSticker.price, priceLBC: selectedSticker.price / exchangeRate }} + isFiat + /> + )} </div> ) : isReviewingSupportComment && activeChannelClaim ? ( <div className="commentCreate__supportCommentPreview"> @@ -483,14 +527,16 @@ export function CommentCreate(props: Props) { /> </> )} - {(isSupportComment || (isReviewingStickerComment && selectedSticker && selectedSticker.price)) && ( + + {(isSupportComment || (isReviewingStickerComment && stickerPrice)) && ( <WalletTipAmountSelector activeTab={activeTab} amount={tipAmount} claim={claim} convertedAmount={convertedAmount} - customTipAmount={selectedSticker && selectedSticker.price} - fiatConversion={selectedSticker && !!selectedSticker.price} // remove + customTipAmount={stickerPrice} + exchangeRate={exchangeRate} + fiatConversion={selectedSticker && !!selectedSticker.price} // REMOVE / figure out onChange={(amount) => setTipAmount(amount)} setConvertedAmount={setConvertedAmount} setDisableSubmitButton={setDisableReviewButton} @@ -520,14 +566,7 @@ export function CommentCreate(props: Props) { <Button button="primary" label={__('Send')} - disabled={ - (isSupportComment && (tipError || disableReviewButton)) || - (selectedSticker && - selectedSticker.price && - (activeTab === TAB_FIAT - ? tipAmount < selectedSticker.price - : convertedAmount && convertedAmount < selectedSticker.price)) - } + disabled={isSupportComment && (tipError || disableReviewButton)} onClick={() => { if (isSupportComment) { handleSupportComment(); @@ -575,33 +614,38 @@ export function CommentCreate(props: Props) { {/** Stickers/Support Buttons **/} {!supportDisabled && !stickerSelector && ( <> - {isReviewingStickerComment ? ( - <Button - button="alt" - label={__('Different Sticker')} - onClick={() => { - setReviewingStickerComment(false); - setIsSupportComment(false); - setStickerSelector(true); - }} - /> - ) : ( - getActionButton(__('Stickers'), ICONS.TAG, () => { + {getActionButton( + __('Stickers'), + isReviewingStickerComment ? __('Different Sticker') : undefined, + ICONS.STICKER, + () => { + if (isReviewingStickerComment) setReviewingStickerComment(false); setIsSupportComment(false); 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); - })} </> )} @@ -619,7 +663,7 @@ export function CommentCreate(props: Props) { if (isSupportComment || isReviewingSupportComment) { if (!isReviewingSupportComment) setIsSupportComment(false); setReviewingSupportComment(false); - if (selectedSticker && selectedSticker.price) { + if (stickerPrice) { setReviewingStickerComment(false); setStickerSelector(false); setSelectedSticker(null); diff --git a/ui/component/common/credit-amount.jsx b/ui/component/common/credit-amount.jsx index 99136699e..225842e6d 100644 --- a/ui/component/common/credit-amount.jsx +++ b/ui/component/common/credit-amount.jsx @@ -1,102 +1,112 @@ // @flow -import React from 'react'; +import { formatCredits, formatFullPrice } from 'util/format-credits'; import classnames from 'classnames'; import LbcSymbol from 'component/common/lbc-symbol'; -import { formatCredits, formatFullPrice } from 'util/format-credits'; +import React from 'react'; type Props = { - amount: number, + amount?: number, + className?: string, + customAmounts?: { amountFiat: number, amountLBC: number }, + fee?: boolean, + isEstimate?: boolean, + isFiat?: boolean, + noFormat?: boolean, precision: number, showFree: boolean, showFullPrice: boolean, - showPlus: boolean, - isEstimate?: boolean, showLBC?: boolean, - fee?: boolean, - className?: string, - noFormat?: boolean, + showPlus: boolean, size?: number, superChat?: boolean, superChatLight?: boolean, - isFiat?: boolean, }; class CreditAmount extends React.PureComponent<Props> { static defaultProps = { + noFormat: false, precision: 2, showFree: false, showFullPrice: false, - showPlus: false, showLBC: true, - noFormat: false, + showPlus: false, }; render() { const { amount, - precision, - showFullPrice, - showFree, - showPlus, - isEstimate, - fee, - showLBC, className, + customAmounts, + fee, + isEstimate, + isFiat, noFormat, + precision, + showFree, + showFullPrice, + showLBC, + showPlus, size, superChat, superChatLight, - isFiat, } = this.props; const minimumRenderableAmount = 10 ** (-1 * precision); // return null, otherwise it will try and convert undefined to a string - if (amount === undefined) { - return null; - } - const fullPrice = formatFullPrice(amount, 2); - const isFree = parseFloat(amount) === 0; + if (amount === undefined && customAmounts === undefined) return null; - let formattedAmount; - if (showFullPrice) { - formattedAmount = fullPrice; - } else { - formattedAmount = - amount > 0 && amount < minimumRenderableAmount - ? `<${minimumRenderableAmount}` - : formatCredits(amount, precision, true); - } + function getAmountText(amount: number, isFiat?: boolean) { + const fullPrice = formatFullPrice(amount, 2); + const isFree = parseFloat(amount) === 0; + let formattedAmount; - let amountText; - if (showFree && isFree) { - amountText = __('Free'); - } else { - amountText = noFormat ? amount : formattedAmount; - - if (showPlus && amount > 0) { - amountText = `+${amountText}`; + if (showFullPrice) { + formattedAmount = fullPrice; + } else { + formattedAmount = + amount > 0 && amount < minimumRenderableAmount + ? `<${minimumRenderableAmount}` + : formatCredits(amount, precision, true); } - 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 (showFree && isFree) { + return __('Free'); + } else { + let amountText = noFormat ? amount : formattedAmount; - if (fee) { - amountText = __('%amount% fee', { amount: amountText }); + if (showPlus && amount > 0) { + 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 ( <span - title={fullPrice} + title={amount ? formatFullPrice(amount, 2) : ''} className={classnames(className, { 'super-chat': superChat, '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 ? ( <span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}> diff --git a/ui/component/common/form-components/form-field.jsx b/ui/component/common/form-components/form-field.jsx index 3453fdbd8..4a4a028ed 100644 --- a/ui/component/common/form-components/form-field.jsx +++ b/ui/component/common/form-components/form-field.jsx @@ -239,7 +239,7 @@ export class FormField extends React.PureComponent<Props> { {...inputProps} /> <div className="form-field__textarea-info"> - {!noEmojis && ( + {!noEmojis && openEmoteMenu && ( <Button type="alt" className="button--file-action" diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx index b56e37489..1365d93b1 100644 --- a/ui/component/common/icon-custom.jsx +++ b/ui/component/common/icon-custom.jsx @@ -2022,4 +2022,14 @@ export const icons = { <line x1="15" y1="9" x2="15.01" y2="9" /> </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> + ), }; diff --git a/ui/component/common/markdown-preview.jsx b/ui/component/common/markdown-preview.jsx index fd876f747..db0131d7d 100644 --- a/ui/component/common/markdown-preview.jsx +++ b/ui/component/common/markdown-preview.jsx @@ -22,6 +22,10 @@ import OptimizedImage from 'component/optimizedImage'; const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/; +function isEmote(title, src) { + return title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons'); +} + type SimpleTextProps = { children?: React.Node, }; @@ -104,8 +108,8 @@ const SimpleImageLink = (props: ImageLinkProps) => { return null; } - if (title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons')) { - return <OptimizedImage title={title} src={src} />; + if (isEmote(title, src)) { + return <OptimizedImage src={src} title={title} className="emote" waitLoad loading="lazy" />; } return ( @@ -195,9 +199,12 @@ const MarkdownPreview = (props: MarkdownProps) => { ), // Workaraund of remarkOptions.Fragment div: React.Fragment, - img: isStakeEnoughForPreview(stakedLevel) - ? ZoomableImage - : (imgProps) => <SimpleImageLink src={imgProps.src} alt={imgProps.alt} title={imgProps.title} />, + img: (imgProps) => + isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src) ? ( + ZoomableImage + ) : ( + <SimpleImageLink src={imgProps.src} alt={imgProps.alt} title={imgProps.title} /> + ), }, }; diff --git a/ui/component/filePrice/view.jsx b/ui/component/filePrice/view.jsx index 4aefbfb50..bf45595ea 100644 --- a/ui/component/filePrice/view.jsx +++ b/ui/component/filePrice/view.jsx @@ -16,7 +16,7 @@ type Props = { type?: string, uri: string, // below props are just passed to <CreditAmount /> - customPrice: number, + customPrices?: { priceFiat: number, priceLBC: number }, hideFree?: boolean, // hide the file price if it's free isFiat?: boolean, showLBC?: boolean, @@ -50,10 +50,10 @@ class FilePrice extends React.PureComponent<Props> { claimWasPurchased, type, claimIsMine, - customPrice, + customPrices, } = 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', { 'filePrice--filepage': type === 'filepage', @@ -66,7 +66,10 @@ class FilePrice extends React.PureComponent<Props> { </span> ) : ( <CreditAmount - amount={costInfo ? costInfo.cost : customPrice} + amount={costInfo ? costInfo.cost : undefined} + customAmounts={ + customPrices ? { amountFiat: customPrices.priceFiat, amountLBC: customPrices.priceLBC } : undefined + } className={className} isEstimate={!!costInfo && !costInfo.includesData} isFiat={isFiat} // this goes diff --git a/ui/component/notification/index.js b/ui/component/notification/index.js index 6a81ea01f..fd7557aee 100644 --- a/ui/component/notification/index.js +++ b/ui/component/notification/index.js @@ -2,7 +2,9 @@ import { connect } from 'react-redux'; import { doReadNotifications, doDeleteNotification } from 'redux/actions/notifications'; import Notification from './view'; -export default connect(null, { - doReadNotifications, - doDeleteNotification, -})(Notification); +const perform = (dispatch, ownProps) => ({ + readNotification: () => dispatch(doReadNotifications([ownProps.notification.id])), + deleteNotification: () => dispatch(doDeleteNotification(ownProps.notification.id)), +}); + +export default connect(null, perform)(Notification); diff --git a/ui/component/notification/view.jsx b/ui/component/notification/view.jsx index 0b94bd91e..c2da3271b 100644 --- a/ui/component/notification/view.jsx +++ b/ui/component/notification/view.jsx @@ -1,64 +1,63 @@ // @flow -import * as ICONS from 'constants/icons'; +import { formatLbryUrlForWeb } from 'util/url'; +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 React from 'react'; -import classnames from 'classnames'; -import Icon from 'component/common/icon'; -import DateTime from 'component/dateTime'; +import { useHistory } from 'react-router'; +import * as ICONS from 'constants/icons'; import Button from 'component/button'; import ChannelThumbnail from 'component/channelThumbnail'; -import { formatLbryUrlForWeb } from 'util/url'; -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 classnames from 'classnames'; import CommentCreate from 'component/commentCreate'; +import CommentReactions from 'component/commentReactions'; 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 = { - notification: WebNotification, menuButton: boolean, - children: any, - doReadNotifications: ([number]) => void, - doDeleteNotification: (number) => void, + notification: WebNotification, + deleteNotification: () => void, + readNotification: () => void, }; 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 { notification_rule, notification_parameters, is_read, id } = notification; const [isReplying, setReplying] = React.useState(false); const [quickReply, setQuickReply] = React.useState(); + // ? const isIgnoredNotification = notification_rule === RULE.NEW_LIVESTREAM; if (isIgnoredNotification) { return null; } + const isCommentNotification = notification_rule === RULE.COMMENT || notification_rule === RULE.COMMENT_REPLY || notification_rule === RULE.CREATOR_COMMENT; const commentText = isCommentNotification && notification_parameters.dynamic.comment; + const stickerFromComment = isCommentNotification && commentText && parseSticker(commentText); + const notificationTarget = getNotificationTarget(); - let notificationTarget; - switch (notification_rule) { - default: - notificationTarget = notification_parameters.device.target; - } - - const creatorIcon = (channelUrl) => { - return ( - <UriIndicator uri={channelUrl} link> - <ChannelThumbnail small uri={channelUrl} /> - </UriIndicator> - ); - }; + const creatorIcon = (channelUrl) => ( + <UriIndicator uri={channelUrl} link> + <ChannelThumbnail small uri={channelUrl} /> + </UriIndicator> + ); let channelUrl; let icon; switch (notification_rule) { @@ -94,8 +93,7 @@ export default function Notification(props: Props) { let channelName; if (channelUrl) { try { - const { claimName } = parseURI(channelUrl); - channelName = claimName; + ({ claimName: channelName } = parseURI(channelUrl)); } catch (e) {} } @@ -123,25 +121,28 @@ export default function Notification(props: Props) { try { const { isChannel } = parseURI(notificationTarget); - if (isChannel) { - urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE); - } + if (isChannel) urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE); } catch (e) {} notificationLink += `?${urlParams.toString()}`; - const navLinkProps = { - to: notificationLink, - onClick: (e) => e.stopPropagation(), - }; + const navLinkProps = { 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() { - if (!is_read) { - doReadNotifications([id]); - } - - if (menuButton && notificationLink) { - push(notificationLink); - } + if (!is_read) readNotification(); + if (menuButton && notificationLink) push(notificationLink); } const Wrapper = menuButton @@ -166,45 +167,40 @@ export default function Notification(props: Props) { ); return ( - <div - className={classnames('notification__wrapper', { - 'notification__wrapper--unread': !is_read, - })} - > + <div className={classnames('notification__wrapper', { 'notification__wrapper--unread': !is_read })}> <Wrapper> <div className="notification__icon">{icon}</div> - <div className="notification__content-wrapper"> + <div className="notificationContent__wrapper"> <div className="notification__content"> - <div className="notification__text-wrapper"> - {!isCommentNotification && <div className="notification__title">{title}</div>} + <div className="notificationText__wrapper"> + <div className="notification__title">{title}</div> - {isCommentNotification && commentText ? ( - <> - <div className="notification__title">{title}</div> - <div title={commentText} className="notification__text"> - {commentText} - </div> - </> + {!commentText ? ( + <div + title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')} + className="notification__text" + > + <LbcMessage>{notification_parameters.device.text}</LbcMessage> + </div> + ) : stickerFromComment ? ( + <div className="sticker__comment"> + <OptimizedImage src={stickerFromComment.url} waitLoad loading="lazy" /> + </div> ) : ( - <> - <div - title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')} - className="notification__text" - > - <LbcMessage>{notification_parameters.device.text}</LbcMessage> - </div> - </> + <div title={commentText} className="notification__text"> + {commentText} + </div> )} </div> {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 && ( <FileThumbnail thumbnail={notification_parameters.device.image_url} - className="notification__content-thumbnail" + className="notificationContent__thumbnail" /> )} </div> @@ -212,10 +208,10 @@ export default function Notification(props: Props) { <div className="notification__extra"> {!is_read && ( <Button - className="notification__mark-seen" + className="notification__markSeen" onClick={(e) => { e.stopPropagation(); - doReadNotifications([id]); + readNotification(); }} /> )} @@ -228,7 +224,7 @@ export default function Notification(props: Props) { <div className="notification__menu"> <Menu> <MenuButton - className={'menu__button notification__menu-button'} + className="menu__button notification__menuButton" onClick={(e) => { e.preventDefault(); e.stopPropagation(); @@ -237,7 +233,7 @@ export default function Notification(props: Props) { <Icon size={18} icon={ICONS.MORE_VERTICAL} /> </MenuButton> <MenuList className="menu__list"> - <MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}> + <MenuItem className="menu__link" onSelect={() => deleteNotification()}> <Icon aria-hidden icon={ICONS.DELETE} /> {__('Delete')} </MenuItem> diff --git a/ui/component/walletTipAmountSelector/view.jsx b/ui/component/walletTipAmountSelector/view.jsx index 0e9b04674..24c2bbf9c 100644 --- a/ui/component/walletTipAmountSelector/view.jsx +++ b/ui/component/walletTipAmountSelector/view.jsx @@ -26,6 +26,7 @@ type Props = { claim: StreamClaim, convertedAmount?: number, customTipAmount?: number, + exchangeRate?: any, fiatConversion?: boolean, tipError: boolean, tipError: string, @@ -44,6 +45,7 @@ function WalletTipAmountSelector(props: Props) { claim, convertedAmount, customTipAmount, + exchangeRate, fiatConversion, tipError, onChange, @@ -56,10 +58,16 @@ function WalletTipAmountSelector(props: Props) { const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', true); 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 [exchangeRate, setExchangeRate] = React.useState(); + + const convertToTwoDecimalsOrMore = (number: number, decimals: number = 2) => + Number((Math.round(number * 10 ** decimals) / 10 ** decimals).toFixed(decimals)); 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 const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip); @@ -81,7 +89,7 @@ function WalletTipAmountSelector(props: Props) { ((amount > balance || balance === 0) && activeTab !== TAB_FIAT) || shouldDisableFiatSelectors || (customTipAmount && fiatConversion && activeTab !== TAB_FIAT && exchangeRate - ? amount * exchangeRate < customTipAmount + ? convertToTwoDecimalsOrMore(amount * exchangeRate) < 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(() => { - if (!exchangeRate) { - Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD)); - } else if ((!convertedAmount || convertedAmount !== amount * exchangeRate) && setConvertedAmount) { + if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) { setConvertedAmount(amount * exchangeRate); } }, [amount, convertedAmount, exchangeRate, setConvertedAmount]); - // check if creator has a payment method saved + // check if user has a payment method saved // REMOVE React.useEffect(() => { if (!stripeEnvironment) return; @@ -130,7 +132,7 @@ function WalletTipAmountSelector(props: Props) { }); }, [setHasSavedCard]); - // REMOVE + // check if creator has a tip account saved REMOVE React.useEffect(() => { if (!stripeEnvironment) return; @@ -173,6 +175,25 @@ function WalletTipAmountSelector(props: Props) { setTipError(__('Not enough Credits')); } else if (amount < MINIMUM_PUBLISH_BID) { 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 { setTipError(false); } @@ -187,35 +208,43 @@ function WalletTipAmountSelector(props: Props) { setTipError(__('Amount must be at least one dollar')); } else if (amount > 1000) { 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 { setTipError(false); } } } - }, [activeTab, amount, balance, setTipError]); + }, [activeTab, amount, balance, convertedAmount, customTipAmount, exchangeRate, setTipError]); const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>; return ( <> <div className="section"> - {tipAmountsToDisplay.map((defaultAmount) => ( - <Button - key={defaultAmount} - disabled={shouldDisableAmountSelector(defaultAmount)} - button="alt" - className={classnames('button-toggle button-toggle--expandformobile', { - 'button-toggle--active': defaultAmount === amount && !useCustomTip, - 'button-toggle--disabled': amount > balance, - })} - label={defaultAmount} - icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} - onClick={() => { - handleCustomPriceChange(defaultAmount); - setUseCustomTip(false); - }} - /> - ))} + {tipAmountsToDisplay && + tipAmountsToDisplay.map((defaultAmount) => ( + <Button + key={defaultAmount} + disabled={shouldDisableAmountSelector(defaultAmount)} + button="alt" + className={classnames('button-toggle button-toggle--expandformobile', { + 'button-toggle--active': + convertToTwoDecimalsOrMore(defaultAmount) === convertToTwoDecimalsOrMore(amount) && !useCustomTip, + 'button-toggle--disabled': amount > balance, + })} + label={defaultAmount} + icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} + onClick={() => { + handleCustomPriceChange(defaultAmount); + setUseCustomTip(false); + }} + /> + ))} <Button button="alt" @@ -242,13 +271,13 @@ function WalletTipAmountSelector(props: Props) { fiatConversion && activeTab !== TAB_FIAT && getHelpMessage( - __( - `This support is priced in $USD. ${ - convertedAmount - ? __(`The current exchange rate for the submitted amount is: $${convertToTwoDecimals(convertedAmount)}`) - : '' - }` - ) + __('This support is priced in $USD.') + + (convertedAmount + ? ' ' + + __('The current exchange rate for the submitted LBC amount is ~ $%exchange_amount%.', { + exchange_amount: convertToTwoDecimalsOrMore(convertedAmount), + }) + : '') )} {/* custom number input form */} @@ -276,7 +305,7 @@ function WalletTipAmountSelector(props: Props) { ? getHelpMessage( <> <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" /> - {__(' To Tip Creators')} + {' ' + __('To Tip Creators')} </> ) : !canReceiveFiatTip diff --git a/ui/constants/emotes.js b/ui/constants/emotes.js index 3f00b0862..99f731f5d 100644 --- a/ui/constants/emotes.js +++ b/ui/constants/emotes.js @@ -9,24 +9,23 @@ const buildEmote = (name: string, path: string) => ({ const getEmotes = (px: string, multiplier: string) => [ buildEmote('ALIEN', `${px}/Alien${multiplier}.png`), + buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`), buildEmote('ANGRY_2', `${px}/angry%202${multiplier}.png`), buildEmote('ANGRY_3', `${px}/angry%203${multiplier}.png`), buildEmote('ANGRY_4', `${px}/angry%204${multiplier}.png`), - buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`), buildEmote('BLIND', `${px}/blind${multiplier}.png`), buildEmote('BLOCK', `${px}/block${multiplier}.png`), buildEmote('BOMB', `${px}/bomb${multiplier}.png`), buildEmote('BRAIN_CHIP', `${px}/Brain%20chip${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('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_3', `${px}/cry%203${multiplier}.png`), buildEmote('CRY_4', `${px}/cry%204${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('EGGPLANT_WITH_CONDOM', `${px}/eggplant%20with%20condom${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('ICE_CREAM', `${px}/ice%20cream${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('KISS_2', `${px}/kiss%202${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('LAUGHING_2', `${px}/Laughing 2${multiplier}.png`), buildEmote('LAUGHING_1', `${px}/Laughing${multiplier}.png`), + buildEmote('LAUGHING_2', `${px}/Laughing 2${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_2', `${px}/Love%202${multiplier}.png`), buildEmote('MONSTER', `${px}/Monster${multiplier}.png`), buildEmote('MUSHROOM', `${px}/mushroom${multiplier}.png`), buildEmote('NAIL_IT', `${px}/Nail%20It${multiplier}.png`), buildEmote('NO', `${px}/NO${multiplier}.png`), buildEmote('OUCH', `${px}/ouch${multiplier}.png`), - buildEmote('PREACE', `${px}/peace${multiplier}.png`), buildEmote('PIZZA', `${px}/pizza${multiplier}.png`), + buildEmote('PREACE', `${px}/peace${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('SPACE_RESITAS', `${px}/resitas${multiplier}.png`), buildEmote('ROCK', `${px}/ROCK${multiplier}.png`), buildEmote('SAD', `${px}/sad${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('SLIME_DOWN', `${px}/slime%20down${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_2', `${px}/smile%202${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_RED_WOJAK', `${px}/space%20wojak${multiplier}.png`), + buildEmote('SPACE_RESITAS', `${px}/resitas${multiplier}.png`), buildEmote('SPACE_TOM', `${px}/space%20Tom${multiplier}.png`), - buildEmote('SPACE_WOJAK_1', `${px}/space%20wojak-1${multiplier}.png`), - buildEmote('ANGRY_3', `${px}/space%20wojak${multiplier}.png`), buildEmote('SPOCK', `${px}/SPOCK${multiplier}.png`), buildEmote('STAR', `${px}/Star${multiplier}.png`), buildEmote('SUNNY_DAY', `${px}/sunny%20day${multiplier}.png`), buildEmote('SUPRISED', `${px}/surprised${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('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('TINFOIL_HAT', `${px}/tin%20hat${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_36px = getEmotes('36px', '@1.5x'); -export const EMOTES_48px = getEmotes('48%20px', '@2x'); -export const EMOTES_72px = getEmotes('72%20px', '@3x'); +export const EMOTES_36px = getEmotes('36px', '%401.5x'); +export const EMOTES_48px = getEmotes('48%20px', '%402x'); +export const EMOTES_72px = getEmotes('72%20px', '%403x'); diff --git a/ui/constants/icons.js b/ui/constants/icons.js index 418da4575..ecee93456 100644 --- a/ui/constants/icons.js +++ b/ui/constants/icons.js @@ -179,3 +179,4 @@ export const ARTISTS = 'Artists'; export const MYSTERIES = 'Mysteries'; export const TECHNOLOGY = 'Technology'; export const EMOJI = 'Emoji'; +export const STICKER = 'Sticker'; diff --git a/ui/scss/component/_comment-create.scss b/ui/scss/component/_comment-create.scss index da47fc919..a8eff5714 100644 --- a/ui/scss/component/_comment-create.scss +++ b/ui/scss/component/_comment-create.scss @@ -106,5 +106,16 @@ $thumbnailWidthSmall: 1rem; .filePrice { height: 1.5rem; width: 10rem; + + .credit-amount:not(:last-child) { + &::after { + margin-left: var(--spacing-xxs); + content: '/'; + } + } + + .credit-amount:not(:first-child) { + margin-left: var(--spacing-xxs); + } } } diff --git a/ui/scss/component/_comments.scss b/ui/scss/component/_comments.scss index cedae7ee0..3182f4580 100644 --- a/ui/scss/component/_comments.scss +++ b/ui/scss/component/_comments.scss @@ -486,3 +486,8 @@ $thumbnailWidthSmall: 1rem; max-height: 100%; } } + +.emote { + max-width: 1.5rem; + max-height: 1.5rem; +} diff --git a/ui/scss/component/_markdown-preview.scss b/ui/scss/component/_markdown-preview.scss index ae39627d0..2afb45130 100644 --- a/ui/scss/component/_markdown-preview.scss +++ b/ui/scss/component/_markdown-preview.scss @@ -117,7 +117,7 @@ } // Image - img:not(.channel-thumbnail__custom) { + img:not(.channel-thumbnail__custom):not(.emote) { margin-bottom: var(--spacing-m); padding-top: var(--spacing-m); max-height: var(--inline-player-max-height); diff --git a/ui/scss/component/_notification.scss b/ui/scss/component/_notification.scss index a586b9780..252ab5013 100644 --- a/ui/scss/component/_notification.scss +++ b/ui/scss/component/_notification.scss @@ -13,6 +13,12 @@ $contentMaxWidth: 60rem; &:first-of-type { border-top: none; } + + @media (min-width: $breakpoint-small) { + &:hover { + background-color: var(--color-card-background-highlighted); + } + } } .commentCreate, @@ -25,7 +31,7 @@ $contentMaxWidth: 60rem; .notification__icon { display: flex; align-items: flex-start; - margin: auto; + margin-top: var(--spacing-xxs); .icon__wrapper { width: 1rem; @@ -94,7 +100,7 @@ $contentMaxWidth: 60rem; } } -.notification__content-wrapper { +.notificationContent__wrapper { flex: 1; display: flex; justify-content: space-between; @@ -121,7 +127,7 @@ $contentMaxWidth: 60rem; } } -.notification__content-thumbnail { +.notificationContent__thumbnail { @include thumbnail; position: relative; margin-left: auto; @@ -139,8 +145,13 @@ $contentMaxWidth: 60rem; } } -.notification__text-wrapper { +.notificationText__wrapper { max-width: calc(#{$contentMaxWidth} - (#{$thumbnailWidth} * 16 / 9) - var(--spacing-m)); + + .sticker__comment { + width: 4.5rem; + height: 4.5rem; + } } .notification__title { @@ -247,7 +258,7 @@ $contentMaxWidth: 60rem; } } -.notification__mark-seen { +.notification__markSeen { height: 12px; width: 12px; border-radius: 50%; diff --git a/ui/util/remark-emote.js b/ui/util/remark-emote.js index b13934be7..2bcd784c1 100644 --- a/ui/util/remark-emote.js +++ b/ui/util/remark-emote.js @@ -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'; const EMOTE_NODE_TYPE = 'emote';