diff --git a/static/app-strings.json b/static/app-strings.json index 600ca762d..9a9fbc5a1 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2319,5 +2319,53 @@ ":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", + "Add a Card": "Add a Card", + " To Tip Creators": " To Tip Creators", "--end--": "--end--" } diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index 13a997128..46410a77e 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -26,6 +26,8 @@ import CommentCreate from 'component/commentCreate'; import CommentMenuList from 'component/commentMenuList'; import UriIndicator from 'component/uriIndicator'; import CreditAmount from 'component/common/credit-amount'; +import OptimizedImage from 'component/optimizedImage'; +import { parseSticker } from 'util/comments'; const AUTO_EXPAND_ALL_REPLIES = false; @@ -130,6 +132,7 @@ function Comment(props: Props) { const totalLikesAndDislikes = likesCount + dislikesCount; const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8; const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri; + const stickerFromMessage = parseSticker(message); let channelOwnerOfContent; try { @@ -324,6 +327,10 @@ function Comment(props: Props) {
setDisplayDeadComment(true)} className="comment__dead"> {__('This comment was slimed to death.')}
+ ) : stickerFromMessage ? ( +
+ +
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? ( { const claim = selectClaimForUri(state, props.uri); @@ -28,8 +28,8 @@ const select = (state, props) => { }; const perform = (dispatch, ownProps) => ({ - createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) => - dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment)), + createComment: (comment, claimId, parentId, txid, payment_intent_id, environment, sticker) => + dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment, sticker)), sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)), doToast: (options) => dispatch(doToast(options)), doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)), diff --git a/ui/component/commentCreate/sticker-selector.jsx b/ui/component/commentCreate/sticker-selector.jsx new file mode 100644 index 000000000..85fda7d82 --- /dev/null +++ b/ui/component/commentCreate/sticker-selector.jsx @@ -0,0 +1,94 @@ +// @flow +import 'scss/component/_sticker-selector.scss'; +import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers'; +import * as ICONS from 'constants/icons'; +import Button from 'component/button'; +import CreditAmount from 'component/common/credit-amount'; +import OptimizedImage from 'component/optimizedImage'; +import React from 'react'; + +const buildStickerSideLink = (section: string, icon: string) => ({ section, icon }); + +const STICKER_SIDE_LINKS = [ + buildStickerSideLink(__('Free'), ICONS.TAG), + buildStickerSideLink(__('Tips'), ICONS.FINANCE), + // Future work may include Channel, Subscriptions, ... +]; + +type Props = { claimIsMine: boolean, onSelect: (any) => void }; + +export default function StickerSelector(props: Props) { + const { claimIsMine, onSelect } = props; + + function scrollToStickerSection(section: string) { + const listBodyEl = document.querySelector('.stickerSelector__listBody'); + const sectionToScroll = document.getElementById(section); + + if (listBodyEl && sectionToScroll) { + // $FlowFixMe + listBodyEl.scrollTo({ + top: sectionToScroll.offsetTop - sectionToScroll.getBoundingClientRect().height * 2, + behavior: 'smooth', + }); + } + } + + const getListRow = (rowTitle: string, rowStickers: any) => ( +
+
+ {rowTitle} +
+
+ {rowStickers.map((sticker) => ( + + ))} +
+
+ ); + + return ( +
+
+
{__('Stickers')}
+
+ +
+
+ {getListRow(__('Free'), FREE_GLOBAL_STICKERS)} + {!claimIsMine && getListRow(__('Tips'), PAID_GLOBAL_STICKERS)} +
+ +
+
    + {STICKER_SIDE_LINKS.map( + (linkProps) => + ((claimIsMine && linkProps.section !== 'Tips') || !claimIsMine) && ( +
  • +
  • + ) + )} +
+
+
+
+ ); +} diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 216a15d79..44b683332 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -1,9 +1,9 @@ // @flow -import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import 'scss/component/_comment-create.scss'; +import { buildValidSticker } from 'util/comments'; +import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { FormField, Form } from 'component/common/form'; import { getChannelIdFromClaim } from 'util/claim'; -import { Lbryio } from 'lbryinc'; import { useHistory } from 'react-router'; import * as ICONS from 'constants/icons'; import * as KEYCODES from 'constants/keycodes'; @@ -15,96 +15,108 @@ import classnames from 'classnames'; import CreditAmount from 'component/common/credit-amount'; import EmoteSelector from './emote-selector'; import Empty from 'component/common/empty'; +import FilePrice from 'component/filePrice'; import I18nMessage from 'component/i18nMessage'; import Icon from 'component/common/icon'; +import OptimizedImage from 'component/optimizedImage'; import React from 'react'; import SelectChannel from 'component/selectChannel'; +import StickerSelector from './sticker-selector'; import type { ElementRef } from 'react'; import UriIndicator from 'component/uriIndicator'; import usePersistedState from 'effects/use-persisted-state'; import WalletTipAmountSelector from 'component/walletTipAmountSelector'; import { getStripeEnvironment } from 'util/stripe'; -let stripeEnvironment = getStripeEnvironment(); +const stripeEnvironment = getStripeEnvironment(); const TAB_FIAT = 'TabFiat'; const TAB_LBC = 'TabLBC'; const MENTION_DEBOUNCE_MS = 100; +// for sendCashTip REMOVE +// type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string }; +// type UserParams = { activeChannelName: ?string, activeChannelId: ?string }; + type Props = { - uri: string, - claim: StreamClaim, - hasChannels: boolean, - isNested: boolean, - isFetchingChannels: boolean, - parentId: string, - isReply: boolean, activeChannel: string, activeChannelClaim: ?ChannelClaim, bottom: boolean, - embed?: boolean, + hasChannels: boolean, // + claim: StreamClaim, claimIsMine: boolean, - supportDisabled: boolean, + isFetchingChannels: boolean, + isNested: boolean, + isReply: boolean, + parentId: string, settingsByChannelId: { [channelId: string]: PerChannelSettings }, shouldFetchComment: boolean, - doToast: ({ message: string }) => void, - createComment: (string, string, string, ?string, ?string, ?string) => Promise, - onDoneReplying?: () => void, - onCancelReplying?: () => void, - toast: (string) => void, - sendTip: ({}, (any) => void, (any) => void) => void, + supportDisabled: boolean, + uri: string, + createComment: (string, string, string, ?string, ?string, ?string, ?boolean) => Promise, doFetchCreatorSettings: (channelId: string) => Promise, - setQuickReply: (any) => void, + doToast: ({ message: string }) => void, fetchComment: (commentId: string) => Promise, + onCancelReplying?: () => void, + onDoneReplying?: () => void, + sendTip: ({}, (any) => void, (any) => void) => void, + setQuickReply: (any) => void, + toast: (string) => void, }; export function CommentCreate(props: Props) { const { - uri, - claim, - hasChannels, - isNested, - isFetchingChannels, - isReply, - parentId, activeChannelClaim, bottom, + hasChannels, + claim, claimIsMine, + isFetchingChannels, + isNested, + isReply, + parentId, settingsByChannelId, - supportDisabled, shouldFetchComment, - doToast, + supportDisabled, + uri, createComment, - onDoneReplying, - onCancelReplying, - sendTip, doFetchCreatorSettings, - setQuickReply, + doToast, fetchComment, + onCancelReplying, + onDoneReplying, + sendTip, + setQuickReply, } = props; + const formFieldRef: ElementRef = React.useRef(); const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input; const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart; const buttonRef: ElementRef = React.useRef(); + const { push, location: { pathname }, } = useHistory(); - const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitting, setSubmitting] = React.useState(false); const [commentFailure, setCommentFailure] = React.useState(false); const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined }); const [isSupportComment, setIsSupportComment] = React.useState(); - const [isReviewingSupportComment, setIsReviewingSupportComment] = React.useState(); + const [isReviewingSupportComment, setReviewingSupportComment] = React.useState(); + const [isReviewingStickerComment, setReviewingStickerComment] = React.useState(); + const [selectedSticker, setSelectedSticker] = React.useState(); const [tipAmount, setTipAmount] = React.useState(1); + const [convertedAmount, setConvertedAmount] = React.useState(); const [commentValue, setCommentValue] = React.useState(''); const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false); + const [stickerSelector, setStickerSelector] = 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 [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState(); const [showEmotes, setShowEmotes] = React.useState(false); + const [disableReviewButton, setDisableReviewButton] = React.useState(); const selectedMentionIndex = commentValue.indexOf('@', selectionIndex) === selectionIndex @@ -124,8 +136,7 @@ export function CommentCreate(props: Props) { : ''; const claimId = claim && claim.claim_id; - const signingChannel = (claim && claim.signing_channel) || claim; - const channelUri = signingChannel && signingChannel.permanent_url; + const channelUri = claim && (claim.signing_channel ? claim.signing_channel.permanent_url : claim.permanent_url); const charCount = commentValue ? commentValue.length : 0; const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend; const channelId = getChannelIdFromClaim(claim); @@ -138,31 +149,23 @@ export function CommentCreate(props: Props) { const minAmountRef = React.useRef(minAmount); minAmountRef.current = minAmount; - const MinAmountNotice = minAmount ? ( -
- }}> - {minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''} - - -
- ) : null; - // ************************************************************************** // Functions // ************************************************************************** + function handleSelectSticker(sticker: any) { + // $FlowFixMe + setSelectedSticker(sticker); + setReviewingStickerComment(true); + setTipAmount(sticker.price || 0); + setStickerSelector(false); + + if (sticker.price && sticker.price > 0) { + setActiveTab(TAB_FIAT); + setIsSupportComment(true); + } + } + function handleCommentChange(event) { let commentValue; if (isReply) { @@ -203,9 +206,7 @@ export function CommentCreate(props: Props) { } function handleSupportComment() { - if (!activeChannelClaim) { - return; - } + if (!activeChannelClaim) return; if (!channelId) { doToast({ @@ -233,7 +234,7 @@ export function CommentCreate(props: Props) { message: __('The creator just updated the minimum setting. Please revise or double-check your tip amount.'), isError: true, }); - setIsReviewingSupportComment(false); + setReviewingSupportComment(false); return; } @@ -242,33 +243,18 @@ export function CommentCreate(props: Props) { } function doSubmitTip() { - if (!activeChannelClaim) { - return; - } + if (!activeChannelClaim) return; - const params = { - amount: tipAmount, - claim_id: claimId, - channel_id: activeChannelClaim.claim_id, - }; - - const activeChannelName = activeChannelClaim && activeChannelClaim.name; - const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id; + const params = { amount: tipAmount, claim_id: claimId, channel_id: activeChannelClaim.claim_id }; + // FIAT ONLY - REMOVE + // const activeChannelName = activeChannelClaim && activeChannelClaim.name; + // const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id; // setup variables for tip API - let channelClaimId, tipChannelName; - // if there is a signing channel it's on a file - if (claim.signing_channel) { - channelClaimId = claim.signing_channel.claim_id; - tipChannelName = claim.signing_channel.name; + // const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id; + const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name; - // otherwise it's on the channel page - } else { - channelClaimId = claim.claim_id; - tipChannelName = claim.name; - } - - setIsSubmitting(true); + setSubmitting(true); if (activeTab === TAB_LBC) { // call sendTip and then run the callback from the response @@ -296,59 +282,24 @@ export function CommentCreate(props: Props) { }, () => { // reset the frontend so people can send a new comment - setIsSubmitting(false); + setSubmitting(false); } ); } else { - const sourceClaimId = claim.claim_id; - const roundedAmount = Math.round(tipAmount * 100) / 100; - - Lbryio.call( - 'customer', - 'tip', - { - // round to deal with floating point precision - amount: Math.round(100 * roundedAmount), // convert from dollars to cents - creator_channel_name: tipChannelName, // creator_channel_name - creator_channel_claim_id: channelClaimId, - tipper_channel_name: activeChannelName, - tipper_channel_claim_id: activeChannelId, - currency: 'USD', - anonymous: false, - source_claim_id: sourceClaimId, - environment: stripeEnvironment, - }, - 'post' - ) - .then((customerTipResponse) => { - const paymentIntendId = customerTipResponse.payment_intent_id; - - handleCreateComment(null, paymentIntendId, stripeEnvironment); - - setCommentValue(''); - setIsReviewingSupportComment(false); - setIsSupportComment(false); - setCommentFailure(false); - setIsSubmitting(false); - - doToast({ - message: __("You sent $%formattedAmount% as a tip to %tipChannelName%, I'm sure they appreciate it!", { - formattedAmount: roundedAmount.toFixed(2), // force show decimal places - tipChannelName, - }), - }); - - // handleCreateComment(null); - }) - .catch((error) => { - doToast({ - message: - error.message !== 'payment intent failed to confirm' - ? error.message - : 'Sorry, there was an error in processing your payment!', - isError: true, - }); - }); + // No cash tips + // const tipParams: TipParams = { tipAmount: Math.round(tipAmount * 100) / 100, tipChannelName, channelClaimId }; + // const userParams: UserParams = { activeChannelName, activeChannelId }; + // sendCashTip(tipParams, userParams, claim.claim_id, stripeEnvironment, (customerTipResponse) => { + // const { payment_intent_id } = customerTipResponse; + // + // handleCreateComment(null, payment_intent_id, stripeEnvironment); + // + // setCommentValue(''); + // setReviewingSupportComment(false); + // setIsSupportComment(false); + // setCommentFailure(false); + // setSubmitting(false); + // }); } } @@ -360,16 +311,17 @@ export function CommentCreate(props: Props) { */ function handleCreateComment(txid, payment_intent_id, environment) { setShowEmotes(false); - setIsSubmitting(true); + setSubmitting(true); + const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name); - createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment) + createComment(stickerValue || commentValue, claimId, parentId, txid, payment_intent_id, environment, !!stickerValue) .then((res) => { - setIsSubmitting(false); + setSubmitting(false); if (setQuickReply) setQuickReply(res); if (res && res.signature) { setCommentValue(''); - setIsReviewingSupportComment(false); + setReviewingSupportComment(false); setIsSupportComment(false); setCommentFailure(false); @@ -379,7 +331,7 @@ export function CommentCreate(props: Props) { } }) .catch(() => { - setIsSubmitting(false); + setSubmitting(false); setCommentFailure(true); if (channelId) { @@ -426,6 +378,10 @@ export function CommentCreate(props: Props) { // Render // ************************************************************************** + const getActionButton = (title: string, icon: string, handleClick: () => void) => ( +