diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index 2e9034b99..e0ebeb2fa 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; @@ -138,6 +140,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 { @@ -338,6 +341,10 @@ function Comment(props: Props) {
setDisplayDeadComment(true)} className="comment__dead"> {__('This comment was slimed to death.')}
+ ) : stickerFromMessage ? ( +
+ +
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? ( ({ - claim: makeSelectClaimForUri(props.uri)(state), - channels: selectMyChannelClaims(state), - isFetchingChannels: selectFetchingMyChannels(state), activeChannelClaim: selectActiveChannelClaim(state), + channels: selectMyChannelClaims(state), + claim: makeSelectClaimForUri(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state), + isFetchingChannels: selectFetchingMyChannels(state), settingsByChannelId: selectSettingsByChannelId(state), supportDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_SUPPORT_TAG)(state), }); const perform = (dispatch, ownProps) => ({ - createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) => + createComment: (comment, claimId, parentId, txid, payment_intent_id, environment, sticker) => dispatch( doCommentCreate( comment, @@ -35,13 +35,16 @@ const perform = (dispatch, ownProps) => ({ ownProps.livestream, txid, payment_intent_id, - environment + environment, + sticker ) ), - sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)), - doToast: (options) => dispatch(doToast(options)), doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)), + doToast: (options) => dispatch(doToast(options)), fetchComment: (commentId) => dispatch(doCommentById(commentId, false)), + sendCashTip: (tipParams, userParams, claimId, environment, successCallback) => + dispatch(doSendCashTip(tipParams, false, userParams, claimId, environment, successCallback)), + sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)), }); export default connect(select, perform)(CommentCreate); 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 7f4d2af87..e177cbe3c 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -1,9 +1,9 @@ // @flow import 'scss/component/_comment-create.scss'; +import { buildValidSticker } from 'util/comments'; import { FF_MAX_CHARS_IN_COMMENT, FF_MAX_CHARS_IN_LIVESTREAM_COMMENT } from 'constants/form-field'; import { FormField, Form } from 'component/common/form'; import { getChannelIdFromClaim } from 'util/claim'; -import { Lbryio } from 'lbryinc'; import { SIMPLE_SITE } from 'config'; import { useHistory } from 'react-router'; import * as ICONS from 'constants/icons'; @@ -16,99 +16,113 @@ 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; +type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string }; +type UserParams = { activeChannelName: ?string, activeChannelId: ?string }; + type Props = { - uri: string, - claim: StreamClaim, - channels: ?Array, - isNested: boolean, - isFetchingChannels: boolean, - parentId: string, - isReply: boolean, activeChannel: string, activeChannelClaim: ?ChannelClaim, bottom: boolean, - livestream?: boolean, - embed?: boolean, + channels: ?Array, + claim: StreamClaim, claimIsMine: boolean, - supportDisabled: boolean, + embed?: boolean, + isFetchingChannels: boolean, + isNested: boolean, + isReply: boolean, + livestream?: 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, + sendCashTip: (TipParams, UserParams, string, ?string, (any) => void) => string, + sendTip: ({}, (any) => void, (any) => void) => void, + setQuickReply: (any) => void, + toast: (string) => void, }; export function CommentCreate(props: Props) { const { - uri, - claim, - channels, - isNested, - isFetchingChannels, - isReply, - parentId, activeChannelClaim, bottom, - livestream, - embed, + channels, + claim, claimIsMine, + embed, + isFetchingChannels, + isNested, + isReply, + livestream, + parentId, settingsByChannelId, - supportDisabled, shouldFetchComment, - doToast, + supportDisabled, + uri, createComment, - onDoneReplying, - onCancelReplying, - sendTip, doFetchCreatorSettings, - setQuickReply, + doToast, fetchComment, + onCancelReplying, + onDoneReplying, + sendCashTip, + 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 @@ -128,8 +142,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 hasChannels = channels && channels.length; const charCount = commentValue ? commentValue.length : 0; const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend; @@ -143,31 +156,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) { @@ -200,18 +205,8 @@ export function CommentCreate(props: Props) { } } - function onTextareaFocus() { - window.addEventListener('keydown', altEnterListener); - } - - function onTextareaBlur() { - window.removeEventListener('keydown', altEnterListener); - } - function handleSupportComment() { - if (!activeChannelClaim) { - return; - } + if (!activeChannelClaim) return; if (!channelId) { doToast({ @@ -239,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; } @@ -248,33 +243,17 @@ export function CommentCreate(props: Props) { } function doSubmitTip() { - if (!activeChannelClaim) { - return; - } - - const params = { - amount: tipAmount, - claim_id: claimId, - channel_id: activeChannelClaim.claim_id, - }; + 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; // 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 @@ -302,59 +281,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; + const tipParams: TipParams = { tipAmount: Math.round(tipAmount * 100) / 100, tipChannelName, channelClaimId }; + const userParams: UserParams = { activeChannelName, activeChannelId }; - 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; + sendCashTip(tipParams, userParams, claim.claim_id, stripeEnvironment, (customerTipResponse) => { + const { payment_intent_id } = customerTipResponse; - handleCreateComment(null, paymentIntendId, stripeEnvironment); + handleCreateComment(null, payment_intent_id, 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, - }); - }); + setCommentValue(''); + setReviewingSupportComment(false); + setIsSupportComment(false); + setCommentFailure(false); + setSubmitting(false); + }); } } @@ -366,16 +310,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); @@ -385,7 +330,7 @@ export function CommentCreate(props: Props) { } }) .catch(() => { - setIsSubmitting(false); + setSubmitting(false); setCommentFailure(true); if (channelId) { @@ -432,6 +377,10 @@ export function CommentCreate(props: Props) { // Render // ************************************************************************** + const getActionButton = (title: string, icon: string, handleClick: () => void) => ( +