[New Feature] Stickers (#131)
* Refactor filePrice * Refactor Wallet Tip Components * Add backend sticker support for comments * Add stickers * Refactor commentCreate * Add Sticker Selector and sticker comment creation * Add stickers display to comments and hyperchats * Fix wrong checks for total Super Chats
This commit is contained in:
parent
a77e59cb53
commit
5f1f702490
26 changed files with 1428 additions and 1381 deletions
|
@ -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) {
|
|||
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
||||
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
||||
</div>
|
||||
) : stickerFromMessage ? (
|
||||
<div className="sticker__comment">
|
||||
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||
</div>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<Expandable>
|
||||
<MarkdownPreview
|
||||
|
|
|
@ -6,26 +6,26 @@ import {
|
|||
selectFetchingMyChannels,
|
||||
makeSelectTagInClaimOrChannelForUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { doSendTip } from 'redux/actions/wallet';
|
||||
import { CommentCreate } from './view';
|
||||
import { DISABLE_SUPPORT_TAG } from 'constants/tags';
|
||||
import { doCommentCreate, doFetchCreatorSettings, doCommentById } from 'redux/actions/comments';
|
||||
import { doSendTip, doSendCashTip } from 'redux/actions/wallet';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectSettingsByChannelId } from 'redux/selectors/comments';
|
||||
import { CommentCreate } from './view';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { DISABLE_SUPPORT_TAG } from 'constants/tags';
|
||||
|
||||
const select = (state, props) => ({
|
||||
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);
|
||||
|
|
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
|
@ -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) => (
|
||||
<div className="stickerSelector__listBody-row">
|
||||
<div id={rowTitle} className="stickerSelector__listBody-rowTitle">
|
||||
{rowTitle}
|
||||
</div>
|
||||
<div className="stickerSelector__listBody-rowItems">
|
||||
{rowStickers.map((sticker) => (
|
||||
<Button
|
||||
key={sticker.name}
|
||||
title={sticker.name}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => onSelect(sticker)}
|
||||
>
|
||||
<OptimizedImage src={sticker.url} waitLoad />
|
||||
{sticker.price && sticker.price > 0 && (
|
||||
<CreditAmount superChatLight amount={sticker.price} size={2} isFiat />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="stickerSelector">
|
||||
<div className="stickerSelector__header card__header--between">
|
||||
<div className="stickerSelector__headerTitle card__title-section--small">{__('Stickers')}</div>
|
||||
</div>
|
||||
|
||||
<div className="stickerSelector__list">
|
||||
<div className="stickerSelector__listBody">
|
||||
{getListRow(__('Free'), FREE_GLOBAL_STICKERS)}
|
||||
{!claimIsMine && getListRow(__('Tips'), PAID_GLOBAL_STICKERS)}
|
||||
</div>
|
||||
|
||||
<div className="navigation__wrapper">
|
||||
<ul className="navigation-links">
|
||||
{STICKER_SIDE_LINKS.map(
|
||||
(linkProps) =>
|
||||
((claimIsMine && linkProps.section !== 'Tips') || !claimIsMine) && (
|
||||
<li key={linkProps.section}>
|
||||
<Button
|
||||
label={__(linkProps.section)}
|
||||
title={__(linkProps.section)}
|
||||
icon={linkProps.icon}
|
||||
iconSize={1}
|
||||
className="navigation-link"
|
||||
onClick={() => scrollToStickerSection(linkProps.section)}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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<ChannelClaim>,
|
||||
isNested: boolean,
|
||||
isFetchingChannels: boolean,
|
||||
parentId: string,
|
||||
isReply: boolean,
|
||||
activeChannel: string,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
bottom: boolean,
|
||||
livestream?: boolean,
|
||||
embed?: boolean,
|
||||
channels: ?Array<ChannelClaim>,
|
||||
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<any>,
|
||||
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<any>,
|
||||
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
||||
setQuickReply: (any) => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
fetchComment: (commentId: string) => Promise<any>,
|
||||
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<any> = React.useRef();
|
||||
const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||
const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart;
|
||||
const buttonRef: ElementRef<any> = 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 ? (
|
||||
<div className="help--notice comment--min-amount-notice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
<Icon
|
||||
customTooltipText={
|
||||
minTip
|
||||
? __('This channel requires a minimum tip for each comment.')
|
||||
: minSuper
|
||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||
: ''
|
||||
}
|
||||
className="icon--help"
|
||||
icon={ICONS.HELP}
|
||||
tooltip
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
) : 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) => (
|
||||
<Button title={title} button="alt" icon={icon} onClick={handleClick} />
|
||||
);
|
||||
|
||||
if (channelSettings && !channelSettings.comments_enabled) {
|
||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||
}
|
||||
|
@ -456,30 +405,123 @@ export function CommentCreate(props: Props) {
|
|||
>
|
||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||
<div className="section__actions--no-margin">
|
||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} requiresAuth={IS_WEB} />
|
||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} requiresAuth />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isReviewingSupportComment && activeChannelClaim) {
|
||||
return (
|
||||
<div className="comment__create">
|
||||
<div className="comment__sc-preview">
|
||||
return (
|
||||
<Form
|
||||
className={classnames('commentCreate', {
|
||||
'commentCreate--reply': isReply,
|
||||
'commentCreate--nestedReply': isNested,
|
||||
'commentCreate--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{/* Input Box/Preview Box */}
|
||||
{stickerSelector ? (
|
||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||
<div className="commentCreate__stickerPreview">
|
||||
<div className="commentCreate__stickerPreviewInfo">
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
</div>
|
||||
|
||||
<div className="commentCreate__stickerPreviewImage">
|
||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad />
|
||||
</div>
|
||||
|
||||
{selectedSticker.price && <FilePrice customPrice={selectedSticker.price} isFiat />}
|
||||
</div>
|
||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||
<div className="commentCreate__supportCommentPreview">
|
||||
<CreditAmount
|
||||
className="comment__sc-preview-amount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
amount={tipAmount}
|
||||
className="commentCreate__supportCommentPreviewAmount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
size={activeTab === TAB_LBC ? 18 : 2}
|
||||
/>
|
||||
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<div>
|
||||
<UriIndicator uri={activeChannelClaim.name} link />
|
||||
<div className="commentCreate__supportCommentBody">
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
<div>{commentValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section__actions--no-margin">
|
||||
) : (
|
||||
<>
|
||||
{showEmotes && (
|
||||
<EmoteSelector
|
||||
commentValue={commentValue}
|
||||
setCommentValue={setCommentValue}
|
||||
closeSelector={() => setShowEmotes(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!advancedEditor && (
|
||||
<ChannelMentionSuggestions
|
||||
uri={uri}
|
||||
isLivestream={livestream}
|
||||
inputRef={formFieldInputRef}
|
||||
mentionTerm={channelMention}
|
||||
creatorUri={channelUri}
|
||||
customSelectAction={handleSelectMention}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
disabled={isFetchingChannels}
|
||||
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
ref={formFieldRef}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
label={
|
||||
<span className="commentCreate__labelWrapper">
|
||||
{!livestream && (
|
||||
<div className="commentCreate__label">{isReply ? __('Replying as ') : __('Comment as ')}</div>
|
||||
)}
|
||||
<SelectChannel tiny />
|
||||
</span>
|
||||
}
|
||||
quickActionLabel={
|
||||
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
||||
}
|
||||
quickActionHandler={() => !SIMPLE_SITE && setAdvancedEditor(!advancedEditor)}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
onFocus={() => window.addEventListener('keydown', altEnterListener)}
|
||||
onBlur={() => window.removeEventListener('keydown', altEnterListener)}
|
||||
placeholder={__('Say something about this...')}
|
||||
value={commentValue}
|
||||
charCount={charCount}
|
||||
onChange={handleCommentChange}
|
||||
autoFocus={isReply}
|
||||
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(isSupportComment || (isReviewingStickerComment && selectedSticker && selectedSticker.price)) && (
|
||||
<WalletTipAmountSelector
|
||||
activeTab={activeTab}
|
||||
amount={tipAmount}
|
||||
claim={claim}
|
||||
convertedAmount={convertedAmount}
|
||||
customTipAmount={selectedSticker && selectedSticker.price}
|
||||
fiatConversion={selectedSticker && !!selectedSticker.price}
|
||||
onChange={(amount) => setTipAmount(amount)}
|
||||
setConvertedAmount={setConvertedAmount}
|
||||
setDisableSubmitButton={setDisableReviewButton}
|
||||
setTipError={setTipError}
|
||||
tipError={tipError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Bottom Action Buttons */}
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
{/* Submit Button */}
|
||||
{isReviewingSupportComment ? (
|
||||
<Button
|
||||
autoFocus
|
||||
button="primary"
|
||||
|
@ -493,166 +535,146 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
onClick={handleSupportComment}
|
||||
/>
|
||||
) : isReviewingStickerComment && selectedSticker ? (
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => setIsReviewingSupportComment(false)}
|
||||
button="primary"
|
||||
label={__('Send')}
|
||||
disabled={
|
||||
(isSupportComment && (tipError || disableReviewButton)) ||
|
||||
(selectedSticker &&
|
||||
selectedSticker.price &&
|
||||
(activeTab === TAB_FIAT
|
||||
? tipAmount < selectedSticker.price
|
||||
: convertedAmount && convertedAmount < selectedSticker.price))
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSupportComment) {
|
||||
handleSupportComment();
|
||||
} else {
|
||||
handleCreateComment();
|
||||
}
|
||||
setSelectedSticker(null);
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setIsSupportComment(false);
|
||||
}}
|
||||
/>
|
||||
) : isSupportComment ? (
|
||||
<Button
|
||||
disabled={disabled || tipError || disableReviewButton || !minAmountMet}
|
||||
type="button"
|
||||
button="primary"
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||
label={__('Review')}
|
||||
onClick={() => setReviewingSupportComment(true)}
|
||||
requiresAuth
|
||||
/>
|
||||
{MinAmountNotice}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={classnames('comment__create', {
|
||||
'comment__create--reply': isReply,
|
||||
'comment__create--nested-reply': isNested,
|
||||
'comment__create--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{showEmotes && (
|
||||
<EmoteSelector
|
||||
commentValue={commentValue}
|
||||
setCommentValue={setCommentValue}
|
||||
closeSelector={() => setShowEmotes(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!advancedEditor && (
|
||||
<ChannelMentionSuggestions
|
||||
uri={uri}
|
||||
isLivestream={livestream}
|
||||
inputRef={formFieldInputRef}
|
||||
mentionTerm={channelMention}
|
||||
creatorUri={channelUri}
|
||||
customSelectAction={handleSelectMention}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
disabled={isFetchingChannels}
|
||||
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
ref={formFieldRef}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
label={
|
||||
<span className="comment-new__label-wrapper">
|
||||
{!livestream && (
|
||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||
)}
|
||||
<SelectChannel tiny />
|
||||
</span>
|
||||
}
|
||||
quickActionLabel={
|
||||
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
||||
}
|
||||
quickActionHandler={() => !SIMPLE_SITE && setAdvancedEditor(!advancedEditor)}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
value={commentValue}
|
||||
charCount={charCount}
|
||||
onChange={handleCommentChange}
|
||||
autoFocus={isReply}
|
||||
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
{isSupportComment && (
|
||||
<WalletTipAmountSelector
|
||||
onTipErrorChange={setTipError}
|
||||
shouldDisableReviewButton={setShouldDisableReviewButton}
|
||||
claim={claim}
|
||||
activeTab={activeTab}
|
||||
amount={tipAmount}
|
||||
onChange={(amount) => setTipAmount(amount)}
|
||||
/>
|
||||
)}
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
{isSupportComment ? (
|
||||
<>
|
||||
<Button
|
||||
disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
|
||||
type="button"
|
||||
button="primary"
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||
label={__('Review')}
|
||||
onClick={() => setIsReviewingSupportComment(true)}
|
||||
requiresAuth={IS_WEB}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => setIsSupportComment(false)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
(!minTip || claimIsMine) && (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled || stickerSelector}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
requiresAuth
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{/** Stickers/Support Buttons **/}
|
||||
{!supportDisabled && !stickerSelector && (
|
||||
<>
|
||||
{(!minTip || claimIsMine) && (
|
||||
{isReviewingStickerComment ? (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
requiresAuth={IS_WEB}
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
/>
|
||||
)}
|
||||
{!supportDisabled && !claimIsMine && (
|
||||
<>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
button="alt"
|
||||
className="thatButton"
|
||||
icon={ICONS.LBC}
|
||||
onClick={() => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_LBC);
|
||||
}}
|
||||
/>
|
||||
{/* @if TARGET='web' */}
|
||||
{stripeEnvironment && (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
button="alt"
|
||||
className="thisButton"
|
||||
icon={ICONS.FINANCE}
|
||||
onClick={() => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_FIAT);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
</>
|
||||
)}
|
||||
{isReply && !minTip && (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
button="alt"
|
||||
label={__('Different Sticker')}
|
||||
onClick={() => {
|
||||
if (onCancelReplying) {
|
||||
onCancelReplying();
|
||||
}
|
||||
setReviewingStickerComment(false);
|
||||
setIsSupportComment(false);
|
||||
setStickerSelector(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
getActionButton(__('Stickers'), ICONS.TAG, () => {
|
||||
setIsSupportComment(false);
|
||||
setStickerSelector(true);
|
||||
})
|
||||
)}
|
||||
{!claimIsMine &&
|
||||
getActionButton(__('LBC'), ICONS.LBC, () => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_LBC);
|
||||
})}
|
||||
{!claimIsMine &&
|
||||
stripeEnvironment &&
|
||||
getActionButton(__('Cash'), ICONS.FINANCE, () => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_FIAT);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cancel Button */}
|
||||
{(isSupportComment ||
|
||||
isReviewingSupportComment ||
|
||||
stickerSelector ||
|
||||
isReviewingStickerComment ||
|
||||
(isReply && !minTip)) && (
|
||||
<Button
|
||||
disabled={isSupportComment && isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => {
|
||||
if (isSupportComment || isReviewingSupportComment) {
|
||||
if (!isReviewingSupportComment) setIsSupportComment(false);
|
||||
setReviewingSupportComment(false);
|
||||
if (selectedSticker && selectedSticker.price) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setSelectedSticker(null);
|
||||
}
|
||||
} else if (stickerSelector || isReviewingStickerComment) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
} else if (isReply && !minTip && onCancelReplying) {
|
||||
onCancelReplying();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Help Text */}
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
{MinAmountNotice}
|
||||
{!!minAmount && (
|
||||
<div className="help--notice commentCreate__minAmountNotice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
<Icon
|
||||
customTooltipText={
|
||||
minTip
|
||||
? __('This channel requires a minimum tip for each comment.')
|
||||
: minSuper
|
||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||
: ''
|
||||
}
|
||||
className="icon--help"
|
||||
icon={ICONS.HELP}
|
||||
tooltip
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -4,15 +4,11 @@ import { makeSelectCostInfoForUri, doFetchCostInfoForUri, makeSelectFetchingCost
|
|||
import FilePrice from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
fetchCostInfo: (uri) => dispatch(doFetchCostInfoForUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePrice);
|
||||
export default connect(select, { doFetchCostInfoForUri })(FilePrice);
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
// @flow
|
||||
import 'scss/component/_file-price.scss';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import Icon from 'component/common/icon';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
showFullPrice: boolean,
|
||||
costInfo: ?{ includesData: boolean, cost: number },
|
||||
fetchCostInfo: string => void,
|
||||
uri: string,
|
||||
fetching: boolean,
|
||||
claim: ?{},
|
||||
claimWasPurchased: boolean,
|
||||
claimIsMine: boolean,
|
||||
claimWasPurchased: boolean,
|
||||
costInfo?: ?{ includesData: boolean, cost: number },
|
||||
fetching: boolean,
|
||||
showFullPrice: boolean,
|
||||
type?: string,
|
||||
uri: string,
|
||||
// below props are just passed to <CreditAmount />
|
||||
inheritStyle?: boolean,
|
||||
showLBC?: boolean,
|
||||
customPrice: number,
|
||||
hideFree?: boolean, // hide the file price if it's free
|
||||
isFiat?: boolean,
|
||||
showLBC?: boolean,
|
||||
doFetchCostInfoForUri: (string) => void,
|
||||
};
|
||||
|
||||
class FilePrice extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
showFullPrice: false,
|
||||
};
|
||||
static defaultProps = { showFullPrice: false };
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchCost(this.props);
|
||||
|
@ -35,40 +35,44 @@ class FilePrice extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
fetchCost = (props: Props) => {
|
||||
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
|
||||
const { costInfo, uri, fetching, claim, doFetchCostInfoForUri } = props;
|
||||
|
||||
if (costInfo === undefined && !fetching && claim) {
|
||||
fetchCostInfo(uri);
|
||||
}
|
||||
if (uri && costInfo === undefined && !fetching && claim) doFetchCostInfoForUri(uri);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { costInfo, showFullPrice, showLBC, hideFree, claimWasPurchased, type, claimIsMine } = this.props;
|
||||
const {
|
||||
costInfo,
|
||||
showFullPrice,
|
||||
showLBC,
|
||||
isFiat,
|
||||
hideFree,
|
||||
claimWasPurchased,
|
||||
type,
|
||||
claimIsMine,
|
||||
customPrice,
|
||||
} = this.props;
|
||||
|
||||
if (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree)) {
|
||||
return null;
|
||||
}
|
||||
if (!customPrice && (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree))) return null;
|
||||
|
||||
const className = classnames(claimWasPurchased ? 'filePrice__key' : 'filePrice', {
|
||||
'filePrice--filepage': type === 'filepage',
|
||||
'filePrice--modal': type === 'modal',
|
||||
});
|
||||
|
||||
return claimWasPurchased ? (
|
||||
<span
|
||||
className={classnames('file-price__key', {
|
||||
'file-price__key--filepage': type === 'filepage',
|
||||
'file-price__key--modal': type === 'modal',
|
||||
})}
|
||||
>
|
||||
<span className={className}>
|
||||
<Icon icon={ICONS.PURCHASED} size={type === 'filepage' ? 22 : undefined} />
|
||||
</span>
|
||||
) : (
|
||||
<CreditAmount
|
||||
className={classnames('file-price', {
|
||||
'file-price--filepage': type === 'filepage',
|
||||
'file-price--modal': type === 'modal',
|
||||
})}
|
||||
amount={costInfo ? costInfo.cost : customPrice}
|
||||
className={className}
|
||||
isEstimate={!!costInfo && !costInfo.includesData}
|
||||
isFiat={isFiat}
|
||||
showFree
|
||||
showLBC={showLBC}
|
||||
amount={costInfo.cost}
|
||||
isEstimate={!costInfo.includesData}
|
||||
showFullPrice={showFullPrice}
|
||||
showLBC={showLBC}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import classnames from 'classnames';
|
|||
import CommentMenuList from 'component/commentMenuList';
|
||||
import Button from 'component/button';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import { parseSticker } from 'util/comments';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -45,11 +47,13 @@ function LivestreamComment(props: Props) {
|
|||
|
||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||
const { claimName } = parseURI(authorUri);
|
||||
const stickerFromMessage = parseSticker(message);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classnames('livestream-comment', {
|
||||
'livestream-comment--superchat': supportAmount > 0,
|
||||
'livestream-comment--sticker': Boolean(stickerFromMessage),
|
||||
})}
|
||||
>
|
||||
{supportAmount > 0 && (
|
||||
|
@ -60,8 +64,12 @@ function LivestreamComment(props: Props) {
|
|||
)}
|
||||
|
||||
<div className="livestream-comment__body">
|
||||
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||
<div className="livestream-comment__info">
|
||||
{(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||
<div
|
||||
className={classnames('livestream-comment__info', {
|
||||
'livestream-comment__info--sticker': Boolean(stickerFromMessage),
|
||||
})}
|
||||
>
|
||||
{isGlobalMod && (
|
||||
<Tooltip label={__('Admin')}>
|
||||
<span className="comment__badge comment__badge--global-mod">
|
||||
|
@ -103,9 +111,15 @@ function LivestreamComment(props: Props) {
|
|||
</span>
|
||||
)}
|
||||
|
||||
<div className="livestream-comment__text">
|
||||
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
|
||||
</div>
|
||||
{stickerFromMessage ? (
|
||||
<div className="sticker__comment">
|
||||
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||
</div>
|
||||
) : (
|
||||
<div className="livestream-comment__text">
|
||||
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import CreditAmount from 'component/common/credit-amount';
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import { parseSticker } from 'util/comments';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -47,7 +49,7 @@ export default function LivestreamComments(props: Props) {
|
|||
superChats: superChatsByTipAmount,
|
||||
} = props;
|
||||
|
||||
let superChatsFiatAmount, superChatsTotalAmount;
|
||||
let superChatsFiatAmount, superChatsLBCAmount, superChatsTotalAmount, hasSuperChats;
|
||||
|
||||
const commentsRef = React.createRef();
|
||||
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
||||
|
@ -58,6 +60,8 @@ export default function LivestreamComments(props: Props) {
|
|||
|
||||
// which kind of superchat to display, either
|
||||
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
|
||||
const stickerSuperChats =
|
||||
superChatsByTipAmount && superChatsByTipAmount.filter(({ comment }) => Boolean(parseSticker(comment)));
|
||||
|
||||
const discussionElement = document.querySelector('.livestream__comments');
|
||||
|
||||
|
@ -130,7 +134,9 @@ export default function LivestreamComments(props: Props) {
|
|||
}
|
||||
|
||||
superChatsFiatAmount = fiatAmount;
|
||||
superChatsTotalAmount = LBCAmount;
|
||||
superChatsLBCAmount = LBCAmount;
|
||||
superChatsTotalAmount = superChatsFiatAmount + superChatsLBCAmount;
|
||||
hasSuperChats = (superChatsTotalAmount || 0) > 0;
|
||||
}
|
||||
|
||||
let superChatsReversed;
|
||||
|
@ -160,11 +166,16 @@ export default function LivestreamComments(props: Props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function getStickerUrl(comment: string) {
|
||||
const stickerFromComment = parseSticker(comment);
|
||||
return stickerFromComment && stickerFromComment.url;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card livestream__discussion">
|
||||
<div className="card__header--between livestream-discussion__header">
|
||||
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
||||
{(superChatsTotalAmount || 0) > 0 && (
|
||||
{hasSuperChats && (
|
||||
<div className="recommended-content__toggles">
|
||||
{/* the superchats in chronological order button */}
|
||||
<Button
|
||||
|
@ -186,7 +197,7 @@ export default function LivestreamComments(props: Props) {
|
|||
})}
|
||||
label={
|
||||
<>
|
||||
<CreditAmount amount={superChatsTotalAmount || 0} size={8} /> /
|
||||
<CreditAmount amount={superChatsLBCAmount || 0} size={8} /> /
|
||||
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
|
||||
</>
|
||||
}
|
||||
|
@ -207,28 +218,48 @@ export default function LivestreamComments(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
<div ref={commentsRef} className="livestream__comments-wrapper">
|
||||
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && (superChatsTotalAmount || 0) > 0 && (
|
||||
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && hasSuperChats && (
|
||||
<div className="livestream-superchats__wrapper">
|
||||
<div className="livestream-superchats__inner">
|
||||
{superChatsByTipAmount.map((superChat: Comment) => (
|
||||
<Tooltip key={superChat.comment_id} label={superChat.comment}>
|
||||
<div className="livestream-superchat">
|
||||
<div className="livestream-superchat__thumbnail">
|
||||
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
||||
</div>
|
||||
{superChatsByTipAmount.map((superChat: Comment) => {
|
||||
const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat);
|
||||
|
||||
<div className="livestream-superchat__info">
|
||||
<UriIndicator uri={superChat.channel_url} link />
|
||||
<CreditAmount
|
||||
size={10}
|
||||
className="livestream-superchat__amount-large"
|
||||
amount={superChat.support_amount}
|
||||
isFiat={superChat.is_fiat}
|
||||
/>
|
||||
const SuperChatWrapper = !isSticker
|
||||
? ({ children }) => <Tooltip label={superChat.comment}>{children}</Tooltip>
|
||||
: ({ children }) => <>{children}</>;
|
||||
|
||||
return (
|
||||
<SuperChatWrapper key={superChat.comment_id}>
|
||||
<div className="livestream-superchat">
|
||||
<div className="livestream-superchat__thumbnail">
|
||||
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classnames('livestream-superchat__info', {
|
||||
'livestream-superchat__info--sticker': isSticker,
|
||||
'livestream-superchat__info--not-sticker': stickerSuperChats && !isSticker,
|
||||
})}
|
||||
>
|
||||
<div className="livestream-superchat__info--user">
|
||||
<UriIndicator uri={superChat.channel_url} link />
|
||||
<CreditAmount
|
||||
size={10}
|
||||
className="livestream-superchat__amount-large"
|
||||
amount={superChat.support_amount}
|
||||
isFiat={superChat.is_fiat}
|
||||
/>
|
||||
</div>
|
||||
{stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
|
||||
<div className="livestream-superchat__info--image">
|
||||
<OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</SuperChatWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -5,36 +5,26 @@ import {
|
|||
makeSelectClaimIsMine,
|
||||
selectFetchingMyChannels,
|
||||
} from 'redux/selectors/claims';
|
||||
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
||||
import { doSendTip } from 'redux/actions/wallet';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import WalletSendTip from './view';
|
||||
import { doOpenModal, doHideModal } from 'redux/actions/app';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import { doSendTip, doSendCashTip } from 'redux/actions/wallet';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
||||
import { withRouter } from 'react-router';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import WalletSendTip from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
isPending: selectIsSendingSupport(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
balance: selectBalance(state),
|
||||
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
|
||||
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
|
||||
claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
incognito: selectIncognito(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
|
||||
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
|
||||
isPending: selectIsSendingSupport(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
sendSupport: (params, isSupport) => dispatch(doSendTip(params, isSupport)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(WalletSendTip));
|
||||
export default withRouter(connect(select, { doHideModal, doSendTip, doSendCashTip })(WalletSendTip));
|
||||
|
|
|
@ -1,403 +1,174 @@
|
|||
// @flow
|
||||
import { Form } from 'component/common/form';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import Card from 'component/common/card';
|
||||
import classnames from 'classnames';
|
||||
import ChannelSelector from 'component/channelSelector';
|
||||
import classnames from 'classnames';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import React from 'react';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||
const MINIMUM_FIAT_TIP = 1;
|
||||
const MAXIMUM_FIAT_TIP = 1000;
|
||||
|
||||
const DEFAULT_TIP_ERROR = __('Sorry, there was an error in processing your payment!');
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const TAB_BOOST = 'TabBoost';
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
||||
type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
|
||||
type UserParams = { activeChannelName: ?string, activeChannelId: ?string };
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
title: string,
|
||||
claim: StreamClaim,
|
||||
isPending: boolean,
|
||||
isSupport: boolean,
|
||||
sendSupport: (SupportParams, boolean) => void,
|
||||
closeModal: () => void,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
balance: number,
|
||||
claim: StreamClaim,
|
||||
claimIsMine: boolean,
|
||||
fetchingChannels: boolean,
|
||||
incognito: boolean,
|
||||
instantTipEnabled: boolean,
|
||||
instantTipMax: { amount: number, currency: string },
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
incognito: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
isAuthenticated: boolean,
|
||||
isPending: boolean,
|
||||
isSupport: boolean,
|
||||
title: string,
|
||||
uri: string,
|
||||
doHideModal: () => void,
|
||||
doSendCashTip: (TipParams, boolean, UserParams, string, ?string) => string,
|
||||
doSendTip: (SupportParams, boolean) => void, // function that comes from lbry-redux
|
||||
};
|
||||
|
||||
function WalletSendTip(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
title,
|
||||
isPending,
|
||||
claimIsMine,
|
||||
activeChannelClaim,
|
||||
balance,
|
||||
claim = {},
|
||||
instantTipEnabled,
|
||||
instantTipMax,
|
||||
sendSupport,
|
||||
closeModal,
|
||||
claimIsMine,
|
||||
fetchingChannels,
|
||||
incognito,
|
||||
activeChannelClaim,
|
||||
doToast,
|
||||
isAuthenticated,
|
||||
instantTipEnabled,
|
||||
instantTipMax,
|
||||
isPending,
|
||||
title,
|
||||
uri,
|
||||
doHideModal,
|
||||
doSendCashTip,
|
||||
doSendTip,
|
||||
} = props;
|
||||
|
||||
/** REACT STATE **/
|
||||
const [presetTipAmount, setPresetTipAmount] = usePersistedState('comment-support:presetTip', DEFAULT_TIP_AMOUNTS[0]);
|
||||
const [customTipAmount, setCustomTipAmount] = usePersistedState('comment-support:customTip', 1.0);
|
||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||
const [isConfirming, setIsConfirming] = React.useState(false);
|
||||
/** STATE **/
|
||||
|
||||
// only allow certain creators to receive tips
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
||||
|
||||
// show things conditionally based on if a user has a card already
|
||||
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
|
||||
|
||||
// show the tip error on the frontend
|
||||
const [tipAmount, setTipAmount] = usePersistedState('comment-support:customTip', 1.0);
|
||||
const [isOnConfirmationPage, setConfirmationPage] = React.useState(false);
|
||||
const [tipError, setTipError] = React.useState();
|
||||
|
||||
// denote which tab to show on the frontend
|
||||
const [activeTab, setActiveTab] = usePersistedState(TAB_BOOST);
|
||||
const [disableSubmitButton, setDisableSubmitButton] = React.useState();
|
||||
|
||||
// handle default active tab
|
||||
React.useEffect(() => {
|
||||
// force to boost tab if it's someone's own upload
|
||||
if (claimIsMine) {
|
||||
setActiveTab(TAB_BOOST);
|
||||
} else {
|
||||
// or set LBC tip as the default if none is set yet
|
||||
if (!activeTab || activeTab === 'undefined') {
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
/** CONSTS **/
|
||||
|
||||
// alphanumeric claim id
|
||||
const claimTypeText = getClaimTypeText();
|
||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||
const titleText = claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||
const { claim_id: claimId } = claim;
|
||||
|
||||
// channel name used in url
|
||||
const { channelName } = parseURI(uri);
|
||||
|
||||
let channelName;
|
||||
try {
|
||||
({ channelName } = parseURI(uri));
|
||||
} catch (e) {}
|
||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
|
||||
// setup variables for backend 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;
|
||||
|
||||
// otherwise it's on the channel page
|
||||
} else {
|
||||
channelClaimId = claim.claim_id;
|
||||
tipChannelName = claim.name;
|
||||
}
|
||||
|
||||
const sourceClaimId = claim.claim_id;
|
||||
|
||||
// check if creator has a payment method saved
|
||||
React.useEffect(() => {
|
||||
if (channelClaimId && isAuthenticated && stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerStatusResponse) => {
|
||||
const defaultPaymentMethodId =
|
||||
customerStatusResponse.Customer &&
|
||||
customerStatusResponse.Customer.invoice_settings &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||
|
||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}
|
||||
}, [channelClaimId, isAuthenticated, stripeEnvironment]);
|
||||
|
||||
// focus tip element if it exists
|
||||
React.useEffect(() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// check if user can receive tips
|
||||
React.useEffect(() => {
|
||||
if (channelClaimId && stripeEnvironment) {
|
||||
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(function (error) {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
}, [channelClaimId, stripeEnvironment]);
|
||||
|
||||
// if user has no balance, used to show conditional frontend
|
||||
const noBalance = balance === 0;
|
||||
|
||||
// the tip amount, based on if a preset or custom tip amount is being used
|
||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
||||
|
||||
// get type of claim (stream/channel/repost/collection) for display on frontend
|
||||
function getClaimTypeText() {
|
||||
if (claim.value_type === 'stream') {
|
||||
return __('Content');
|
||||
} else if (claim.value_type === 'channel') {
|
||||
return __('Channel');
|
||||
} else if (claim.value_type === 'repost') {
|
||||
return __('Repost');
|
||||
} else if (claim.value_type === 'collection') {
|
||||
return __('List');
|
||||
} else {
|
||||
return __('Claim');
|
||||
}
|
||||
}
|
||||
const claimTypeText = getClaimTypeText();
|
||||
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
||||
// icon to use or explainer text to show per tab
|
||||
let iconToUse;
|
||||
let explainerText = '';
|
||||
if (activeTab === TAB_BOOST) {
|
||||
iconToUse = ICONS.LBC;
|
||||
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {
|
||||
claimTypeText,
|
||||
});
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
iconToUse = ICONS.FINANCE;
|
||||
explainerText = __('Show this channel your appreciation by sending a donation in USD.');
|
||||
// if (!hasCardSaved) {
|
||||
// explainerText += __('You must add a card to use this functionality.');
|
||||
// }
|
||||
} else if (activeTab === TAB_LBC) {
|
||||
iconToUse = ICONS.LBC;
|
||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
||||
let explainerText = '',
|
||||
confirmLabel = '';
|
||||
switch (activeTab) {
|
||||
case TAB_BOOST:
|
||||
explainerText = __(
|
||||
'This refundable boost will improve the discoverability of this %claimTypeText% while active. ',
|
||||
{ claimTypeText }
|
||||
);
|
||||
confirmLabel = __('Boosting');
|
||||
break;
|
||||
case TAB_FIAT:
|
||||
explainerText = __('Show this channel your appreciation by sending a donation in USD. ');
|
||||
confirmLabel = __('Tipping Fiat (USD)');
|
||||
break;
|
||||
case TAB_LBC:
|
||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits. ');
|
||||
confirmLabel = __('Tipping Credit');
|
||||
break;
|
||||
}
|
||||
|
||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||
/** FUNCTIONS **/
|
||||
|
||||
React.useEffect(() => {
|
||||
// Regex for number up to 8 decimal places
|
||||
let regexp;
|
||||
let tipError;
|
||||
|
||||
if (tipAmount === 0) {
|
||||
tipError = __('Amount must be a positive number');
|
||||
} else if (!tipAmount || typeof tipAmount !== 'number') {
|
||||
tipError = __('Amount must be a number');
|
||||
function getClaimTypeText() {
|
||||
switch (claim.value_type) {
|
||||
case 'stream':
|
||||
return __('Content');
|
||||
case 'channel':
|
||||
return __('Channel');
|
||||
case 'repost':
|
||||
return __('Repost');
|
||||
case 'collection':
|
||||
return __('List');
|
||||
default:
|
||||
return __('Claim');
|
||||
}
|
||||
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
else if (activeTab !== TAB_FIAT) {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||
const validTipInput = regexp.test(String(tipAmount));
|
||||
|
||||
if (!validTipInput) {
|
||||
tipError = __('Amount must have no more than 8 decimal places');
|
||||
} else if (!validTipInput) {
|
||||
tipError = __('Amount must have no more than 8 decimal places');
|
||||
} else if (tipAmount === balance) {
|
||||
tipError = __('Please decrease the amount to account for transaction fees');
|
||||
} else if (tipAmount > balance) {
|
||||
tipError = __('Not enough Credits');
|
||||
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
|
||||
tipError = __('Amount must be higher');
|
||||
}
|
||||
// if tip fiat tab
|
||||
} else {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||
const validTipInput = regexp.test(String(tipAmount));
|
||||
|
||||
if (!validTipInput) {
|
||||
tipError = __('Amount must have no more than 2 decimal places');
|
||||
} else if (tipAmount < MINIMUM_FIAT_TIP) {
|
||||
tipError = __('Amount must be at least one dollar');
|
||||
} else if (tipAmount > MAXIMUM_FIAT_TIP) {
|
||||
tipError = __('Amount cannot be over 1000 dollars');
|
||||
}
|
||||
}
|
||||
|
||||
setTipError(tipError);
|
||||
}, [tipAmount, balance, setTipError, activeTab]);
|
||||
}
|
||||
|
||||
// make call to the backend to send lbc or fiat
|
||||
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
||||
// send a tip
|
||||
if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||
setIsConfirming(true);
|
||||
if (!isOnConfirmationPage && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||
setConfirmationPage(true);
|
||||
} else {
|
||||
// send a boost
|
||||
const supportParams: SupportParams = { amount: tipAmount, claim_id: claimId };
|
||||
|
||||
// include channel name if donation not anonymous
|
||||
if (activeChannelClaim && !incognito) {
|
||||
supportParams.channel_id = activeChannelClaim.claim_id;
|
||||
}
|
||||
const supportParams: SupportParams = {
|
||||
amount: tipAmount,
|
||||
claim_id: claimId,
|
||||
channel_id: activeChannelClaim && !incognito ? activeChannelClaim.claim_id : undefined,
|
||||
};
|
||||
|
||||
// send tip/boost
|
||||
sendSupport(supportParams, isSupport);
|
||||
closeModal();
|
||||
doSendTip(supportParams, isSupport);
|
||||
doHideModal();
|
||||
}
|
||||
}
|
||||
|
||||
// when the form button is clicked
|
||||
function handleSubmit() {
|
||||
if (tipAmount && claimId) {
|
||||
// send an instant tip (no need to go to an exchange first)
|
||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||
if (instantTipMax.currency === 'LBC') {
|
||||
sendSupportOrConfirm(instantTipMax.amount);
|
||||
} else {
|
||||
// Need to convert currency of instant purchase maximum before trying to send support
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
||||
sendSupportOrConfirm(instantTipMax.amount / LBC_USD);
|
||||
});
|
||||
}
|
||||
// sending fiat tip
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
if (!isConfirming) {
|
||||
setIsConfirming(true);
|
||||
} else if (isConfirming) {
|
||||
let sendAnonymously = !activeChannelClaim || incognito;
|
||||
if (!tipAmount || !claimId) return;
|
||||
|
||||
// hit backend to send tip
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'tip',
|
||||
{
|
||||
// round to fix issues with floating point numbers
|
||||
amount: Math.round(100 * tipAmount), // convert from dollars to cents
|
||||
creator_channel_name: tipChannelName, // creator_channel_name
|
||||
creator_channel_claim_id: channelClaimId,
|
||||
tipper_channel_name: sendAnonymously ? '' : activeChannelName,
|
||||
tipper_channel_claim_id: sendAnonymously ? '' : activeChannelId,
|
||||
currency: 'USD',
|
||||
anonymous: sendAnonymously,
|
||||
source_claim_id: sourceClaimId,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((customerTipResponse) => {
|
||||
doToast({
|
||||
message: __("You sent $%amount% as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||
amount: tipAmount,
|
||||
tipChannelName,
|
||||
}),
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
// show error message from Stripe if one exists (being passed from backend by Beamer's API currently)
|
||||
let displayError;
|
||||
if (error.message) {
|
||||
displayError = error.message;
|
||||
} else {
|
||||
displayError = DEFAULT_TIP_ERROR;
|
||||
}
|
||||
|
||||
doToast({ message: displayError, isError: true });
|
||||
});
|
||||
|
||||
closeModal();
|
||||
}
|
||||
// if it's a boost (?)
|
||||
// send an instant tip (no need to go to an exchange first)
|
||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||
if (instantTipMax.currency === 'LBC') {
|
||||
sendSupportOrConfirm(instantTipMax.amount);
|
||||
} else {
|
||||
sendSupportOrConfirm();
|
||||
// Need to convert currency of instant purchase maximum before trying to send support
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => sendSupportOrConfirm(instantTipMax.amount / LBC_USD));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const countDecimals = function (value) {
|
||||
const text = value.toString();
|
||||
const index = text.indexOf('.');
|
||||
return text.length - index - 1;
|
||||
};
|
||||
|
||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
||||
let tipAmountAsString = event.target.value;
|
||||
|
||||
let tipAmount = parseFloat(tipAmountAsString);
|
||||
|
||||
const howManyDecimals = countDecimals(tipAmountAsString);
|
||||
|
||||
// fiat tip input
|
||||
if (activeTab === TAB_FIAT) {
|
||||
if (Number.isNaN(tipAmount)) {
|
||||
setCustomTipAmount('');
|
||||
}
|
||||
|
||||
// allow maximum of two decimal places
|
||||
if (howManyDecimals > 2) {
|
||||
tipAmount = Math.floor(tipAmount * 100) / 100;
|
||||
}
|
||||
|
||||
// remove decimals, and then get number of digits
|
||||
const howManyDigits = Math.trunc(tipAmount).toString().length;
|
||||
|
||||
if (howManyDigits > 4 && tipAmount !== 1000) {
|
||||
setTipError('Amount cannot be over 1000 dollars');
|
||||
setCustomTipAmount(tipAmount);
|
||||
} else if (tipAmount > 1000) {
|
||||
setTipError('Amount cannot be over 1000 dollars');
|
||||
setCustomTipAmount(tipAmount);
|
||||
// sending fiat tip
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
if (!isOnConfirmationPage) {
|
||||
setConfirmationPage(true);
|
||||
} else {
|
||||
setCustomTipAmount(tipAmount);
|
||||
const tipParams: TipParams = { tipAmount, tipChannelName, channelClaimId };
|
||||
const userParams: UserParams = { activeChannelName, activeChannelId };
|
||||
|
||||
// hit backend to send tip
|
||||
doSendCashTip(tipParams, !activeChannelClaim || incognito, userParams, claimId, stripeEnvironment);
|
||||
doHideModal();
|
||||
}
|
||||
// LBC tip input
|
||||
// if it's a boost (?)
|
||||
} else {
|
||||
// TODO: this is a bit buggy, needs a touchup
|
||||
// if (howManyDecimals > 9) {
|
||||
// // only allows up to 8 decimal places
|
||||
// tipAmount = Number(tipAmount.toString().match(/^-?\d+(?:\.\d{0,8})?/)[0]);
|
||||
//
|
||||
// setTipError('Please only use up to 8 decimals');
|
||||
// }
|
||||
setCustomTipAmount(tipAmount);
|
||||
sendSupportOrConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,10 +178,7 @@ function WalletSendTip(props: Props) {
|
|||
// testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137
|
||||
// also sometimes it's returned as a string
|
||||
// eslint-disable-next-line
|
||||
if (tipAmount !== tipAmount || tipAmount === 'NaN') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return tipAmount !== tipAmount || tipAmount === 'NaN';
|
||||
}
|
||||
|
||||
function convertToTwoDecimals(number) {
|
||||
|
@ -423,129 +191,65 @@ function WalletSendTip(props: Props) {
|
|||
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
|
||||
|
||||
// build button text based on tab
|
||||
if (activeTab === TAB_BOOST) {
|
||||
return claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
return __('Send a $%displayAmount% Tip', { displayAmount });
|
||||
} else if (activeTab === TAB_LBC) {
|
||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||
switch (activeTab) {
|
||||
case TAB_BOOST:
|
||||
return titleText;
|
||||
case TAB_FIAT:
|
||||
return __('Send a $%displayAmount% Tip', { displayAmount });
|
||||
case TAB_LBC:
|
||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||
}
|
||||
}
|
||||
|
||||
// dont allow user to click send button
|
||||
function shouldDisableAmountSelector(amount) {
|
||||
return (
|
||||
(amount > balance && activeTab !== TAB_FIAT) || (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
);
|
||||
}
|
||||
/** RENDER **/
|
||||
|
||||
// showed on confirm page above amount
|
||||
function setConfirmLabel() {
|
||||
if (activeTab === TAB_LBC) {
|
||||
return __('Tipping Credit');
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
return __('Tipping Fiat (USD)');
|
||||
} else if (activeTab === TAB_BOOST) {
|
||||
return __('Boosting');
|
||||
}
|
||||
}
|
||||
const getTabButton = (tabIcon: string, tabLabel: string, tabName: string) => (
|
||||
<Button
|
||||
key={tabName}
|
||||
icon={tabIcon}
|
||||
label={tabLabel}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) tipInputElement.focus();
|
||||
if (!isOnConfirmationPage) setActiveTab(tabName);
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === tabName })}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
{/* if there is no LBC balance, show user frontend to get credits */}
|
||||
{/* if there is lbc, the main tip/boost gui with the 3 tabs at the top */}
|
||||
<Card
|
||||
title={
|
||||
<LbcSymbol
|
||||
postfix={
|
||||
claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Support This %claimTypeText%', { claimTypeText })
|
||||
}
|
||||
size={22}
|
||||
/>
|
||||
}
|
||||
title={<LbcSymbol postfix={titleText} size={22} />}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
<>
|
||||
{!claimIsMine && (
|
||||
<div className="section">
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="tip"
|
||||
icon={ICONS.LBC}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_LBC })}
|
||||
/>
|
||||
{getTabButton(ICONS.LBC, __('Tip'), TAB_LBC)}
|
||||
|
||||
{/* tip fiat tab button */}
|
||||
{/* @if TARGET='web' */}
|
||||
{stripeEnvironment && (
|
||||
<Button
|
||||
key="tip-fiat"
|
||||
icon={ICONS.FINANCE}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_FIAT);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_FIAT })}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="boost"
|
||||
icon={ICONS.TRENDING}
|
||||
label={__('Boost')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_BOOST);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_BOOST })}
|
||||
/>
|
||||
{stripeEnvironment && getTabButton(ICONS.FINANCE, __('Tip'), TAB_FIAT)}
|
||||
|
||||
{/* support LBC tab button */}
|
||||
{getTabButton(ICONS.TRENDING, __('Boost'), TAB_BOOST)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* short explainer under the button */}
|
||||
<div className="section__subtitle">
|
||||
{explainerText + ' '}
|
||||
{explainerText}
|
||||
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
||||
{
|
||||
<Button
|
||||
label={__('Learn more')}
|
||||
button="link"
|
||||
href="https://odysee.com/@OdyseeHelp:b/Monetization-of-Content:3"
|
||||
/>
|
||||
}
|
||||
<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
// confirmation modal, allow user to confirm or cancel transaction
|
||||
isConfirming ? (
|
||||
isOnConfirmationPage ? (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
|
@ -555,10 +259,10 @@ function WalletSendTip(props: Props) {
|
|||
<div className="confirm__value">
|
||||
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
||||
</div>
|
||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
||||
<div className="confirm__label">{confirmLabel}</div>
|
||||
<div className="confirm__value">
|
||||
{activeTab === TAB_FIAT ? (
|
||||
<p>$ {(Math.round(tipAmount * 100) / 100).toFixed(2)}</p>
|
||||
<p>{`$ ${(Math.round(tipAmount * 100) / 100).toFixed(2)}`}</p>
|
||||
) : (
|
||||
<LbcSymbol postfix={tipAmount} size={22} />
|
||||
)}
|
||||
|
@ -567,98 +271,23 @@ function WalletSendTip(props: Props) {
|
|||
</div>
|
||||
<div className="section__actions">
|
||||
<Button autoFocus onClick={handleSubmit} button="primary" disabled={isPending} label={__('Confirm')} />
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setIsConfirming(false)} />
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setConfirmationPage(false)} />
|
||||
</div>
|
||||
</>
|
||||
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && noBalance) ? (
|
||||
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && balance === 0) ? (
|
||||
<>
|
||||
<div className="section">
|
||||
<ChannelSelector />
|
||||
</div>
|
||||
|
||||
{/* prompt to save a card */}
|
||||
{activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
<h3 className="add-card-prompt">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />{' '}
|
||||
{__('To Tip Creators')}
|
||||
</h3>
|
||||
)}
|
||||
<ChannelSelector />
|
||||
|
||||
{/* section to pick tip/boost amount */}
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
||||
<Button
|
||||
key={amount}
|
||||
disabled={shouldDisableAmountSelector(amount)}
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': tipAmount === amount && !useCustomTip,
|
||||
'button-toggle--disabled': amount > balance,
|
||||
})}
|
||||
label={amount}
|
||||
icon={iconToUse}
|
||||
onClick={() => {
|
||||
setPresetTipAmount(amount);
|
||||
setUseCustomTip(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': useCustomTip, // set as active
|
||||
})}
|
||||
icon={iconToUse}
|
||||
label={__('Custom')}
|
||||
onClick={() => setUseCustomTip(true)}
|
||||
// disabled if it's receive fiat and there is no card or creator can't receive tips
|
||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
||||
/>
|
||||
|
||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && activeTab !== TAB_FIAT && (
|
||||
<Button
|
||||
button="secondary"
|
||||
className="button-toggle-group-action"
|
||||
icon={ICONS.BUY}
|
||||
title={__('Buy or swap more LBRY Credits')}
|
||||
navigate={`/$/${PAGES.BUY}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{useCustomTip && (
|
||||
<div className="section">
|
||||
<FormField
|
||||
autoFocus
|
||||
name="tip-input"
|
||||
label={
|
||||
<React.Fragment>
|
||||
{__('Custom support amount')}{' '}
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<I18nMessage
|
||||
tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} showLBC={false} /> }}
|
||||
>
|
||||
(%lbc_balance% Credits available)
|
||||
</I18nMessage>
|
||||
) : (
|
||||
'in USD'
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
error={tipError}
|
||||
min="0"
|
||||
step="any"
|
||||
type="number"
|
||||
style={{
|
||||
width: activeTab === TAB_FIAT ? '99px' : '160px',
|
||||
}}
|
||||
placeholder="1.23"
|
||||
value={customTipAmount}
|
||||
onChange={(event) => handleCustomPriceChange(event)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<WalletTipAmountSelector
|
||||
setTipError={setTipError}
|
||||
tipError={tipError}
|
||||
claim={claim}
|
||||
activeTab={activeTab === TAB_BOOST ? TAB_LBC : activeTab}
|
||||
amount={tipAmount}
|
||||
onChange={(amount) => setTipAmount(amount)}
|
||||
setDisableSubmitButton={setDisableSubmitButton}
|
||||
/>
|
||||
|
||||
{/* send tip/boost button */}
|
||||
<div className="section__actions">
|
||||
|
@ -667,35 +296,25 @@ function WalletSendTip(props: Props) {
|
|||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||
button="primary"
|
||||
type="submit"
|
||||
disabled={
|
||||
fetchingChannels ||
|
||||
isPending ||
|
||||
tipError ||
|
||||
!tipAmount ||
|
||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
}
|
||||
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
|
||||
label={buildButtonText()}
|
||||
/>
|
||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||
</div>
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<WalletSpendableBalanceHelp />
|
||||
) : !canReceiveFiatTip ? (
|
||||
<div className="help">{__('Only creators that verify cash accounts can receive tips')}</div>
|
||||
) : (
|
||||
<div className="help">{__('The payment will be made from your saved card')}</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
||||
<Card
|
||||
title={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Supporting content requires %lbc%</I18nMessage>
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>
|
||||
{__('Supporting content requires %lbc%')}
|
||||
</I18nMessage>
|
||||
}
|
||||
subtitle={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>
|
||||
With %lbc%, you can send tips to your favorite creators, or help boost their content for more people
|
||||
to see.
|
||||
{__(
|
||||
'With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.'
|
||||
)}
|
||||
</I18nMessage>
|
||||
}
|
||||
actions={
|
||||
|
@ -712,7 +331,7 @@ function WalletSendTip(props: Props) {
|
|||
label={__('Buy/Swap Credits')}
|
||||
navigate={`/$/${PAGES.BUY}`}
|
||||
/>
|
||||
<Button button="link" label={__('Nevermind')} onClick={closeModal} />
|
||||
<Button button="link" label={__('Nevermind')} onClick={doHideModal} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -2,8 +2,6 @@ import { connect } from 'react-redux';
|
|||
import { selectBalance } from 'redux/selectors/wallet';
|
||||
import WalletSpendableBalanceHelp from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
const select = (state) => ({ balance: selectBalance(state) });
|
||||
|
||||
export default connect(select)(WalletSpendableBalanceHelp);
|
||||
|
|
|
@ -1,33 +1,21 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
inline?: boolean,
|
||||
};
|
||||
type Props = { balance: number, inline?: boolean };
|
||||
|
||||
function WalletSpendableBalanceHelp(props: Props) {
|
||||
const { balance, inline } = props;
|
||||
|
||||
if (!balance) {
|
||||
return null;
|
||||
}
|
||||
const getMessage = (text: string) => (
|
||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>{text}</I18nMessage>
|
||||
);
|
||||
|
||||
return inline ? (
|
||||
<span className="help--spendable">
|
||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
||||
%balance% available.
|
||||
</I18nMessage>
|
||||
</span>
|
||||
return !balance ? null : inline ? (
|
||||
<span className="help--spendable">{getMessage(__('%balance% available.'))}</span>
|
||||
) : (
|
||||
<div className="help">
|
||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
||||
Your immediately spendable balance is %balance%.
|
||||
</I18nMessage>
|
||||
</div>
|
||||
<div className="help">{getMessage(__('Your immediately spendable balance is %balance%.'))}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'redux/selectors/wallet';
|
||||
import WalletTipAmountSelector from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => ({
|
||||
balance: selectBalance(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
// claim: makeSelectClaimForUri(props.uri)(state),
|
||||
// claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||
});
|
||||
const select = (state) => ({ balance: selectBalance(state) });
|
||||
|
||||
export default connect(select)(WalletTipAmountSelector);
|
||||
|
|
|
@ -1,175 +1,203 @@
|
|||
// @flow
|
||||
import 'scss/component/_wallet-tip-selector.scss';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
amount: number,
|
||||
onChange: (number) => void,
|
||||
isAuthenticated: boolean,
|
||||
claim: StreamClaim,
|
||||
uri: string,
|
||||
onTipErrorChange: (string) => void,
|
||||
activeTab: string,
|
||||
shouldDisableReviewButton: (boolean) => void,
|
||||
amount: number,
|
||||
balance: number,
|
||||
claim: StreamClaim,
|
||||
convertedAmount?: number,
|
||||
customTipAmount?: number,
|
||||
fiatConversion?: boolean,
|
||||
tipError: boolean,
|
||||
tipError: string,
|
||||
uri: string,
|
||||
onChange: (number) => void,
|
||||
setConvertedAmount?: (number) => void,
|
||||
setDisableSubmitButton: (boolean) => void,
|
||||
setTipError: (any) => void,
|
||||
};
|
||||
|
||||
function WalletTipAmountSelector(props: Props) {
|
||||
const { balance, amount, onChange, activeTab, claim, onTipErrorChange, shouldDisableReviewButton } = props;
|
||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const {
|
||||
activeTab,
|
||||
amount,
|
||||
balance,
|
||||
claim,
|
||||
convertedAmount,
|
||||
customTipAmount,
|
||||
fiatConversion,
|
||||
tipError,
|
||||
onChange,
|
||||
setConvertedAmount,
|
||||
setDisableSubmitButton,
|
||||
setTipError,
|
||||
} = props;
|
||||
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
||||
const isMobile = useIsMobile();
|
||||
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 tipAmountsToDisplay =
|
||||
customTipAmount && fiatConversion && activeTab === TAB_FIAT ? [customTipAmount] : 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);
|
||||
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
|
||||
|
||||
// setup variables for tip API
|
||||
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
||||
/**
|
||||
* whether tip amount selection/review functionality should be disabled
|
||||
* @param [amount] LBC amount (optional)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldDisableAmountSelector(amount) {
|
||||
function shouldDisableAmountSelector(amount: number) {
|
||||
// if it's LBC but the balance isn't enough, or fiat conditions met
|
||||
// $FlowFixMe
|
||||
return (amount > balance && activeTab !== TAB_FIAT) || shouldDisableFiatSelectors;
|
||||
return (
|
||||
((amount > balance || balance === 0) && activeTab !== TAB_FIAT) ||
|
||||
shouldDisableFiatSelectors ||
|
||||
(customTipAmount && fiatConversion && activeTab !== TAB_FIAT && exchangeRate
|
||||
? amount * exchangeRate < customTipAmount
|
||||
: customTipAmount && amount < customTipAmount)
|
||||
);
|
||||
}
|
||||
|
||||
shouldDisableReviewButton(shouldDisableFiatSelectors);
|
||||
|
||||
// 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;
|
||||
|
||||
// otherwise it's on the channel page
|
||||
} else {
|
||||
channelClaimId = claim.claim_id;
|
||||
tipChannelName = claim.name;
|
||||
}
|
||||
|
||||
// check if creator has a payment method saved
|
||||
React.useEffect(() => {
|
||||
if (stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerStatusResponse) => {
|
||||
const defaultPaymentMethodId =
|
||||
customerStatusResponse.Customer &&
|
||||
customerStatusResponse.Customer.invoice_settings &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||
|
||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
//
|
||||
React.useEffect(() => {
|
||||
if (stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
{
|
||||
channel_claim_id: channelClaimId,
|
||||
channel_name: tipChannelName,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((accountCheckResponse) => {
|
||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||
setCanReceiveFiatTip(true);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// setHasSavedCard(false);
|
||||
// setCanReceiveFiatTip(true);
|
||||
|
||||
let regexp,
|
||||
tipError = '';
|
||||
|
||||
if (amount === 0) {
|
||||
tipError = __('Amount must be a positive number');
|
||||
} else if (!amount || typeof amount !== 'number') {
|
||||
tipError = __('Amount must be a number');
|
||||
}
|
||||
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
else if (activeTab !== TAB_FIAT) {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
tipError = __('Amount must have no more than 8 decimal places');
|
||||
} else if (amount === balance) {
|
||||
tipError = __('Please decrease the amount to account for transaction fees');
|
||||
} else if (amount > balance) {
|
||||
tipError = __('Not enough Credits');
|
||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||
tipError = __('Amount must be higher');
|
||||
}
|
||||
// if tip fiat tab
|
||||
} else {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
tipError = __('Amount must have no more than 2 decimal places');
|
||||
} else if (amount < 1) {
|
||||
tipError = __('Amount must be at least one dollar');
|
||||
} else if (amount > 1000) {
|
||||
tipError = __('Amount cannot be over 1000 dollars');
|
||||
}
|
||||
}
|
||||
|
||||
setTipError(tipError);
|
||||
onTipErrorChange(tipError);
|
||||
}, [amount, balance, setTipError, activeTab]);
|
||||
|
||||
// parse number as float and sets it in the parent component
|
||||
function handleCustomPriceChange(amount: number) {
|
||||
const tipAmount = parseFloat(amount);
|
||||
|
||||
onChange(tipAmount);
|
||||
const tipAmountValue = parseFloat(amount);
|
||||
onChange(tipAmountValue);
|
||||
if (fiatConversion && exchangeRate && setConvertedAmount && convertedAmount !== tipAmountValue * exchangeRate) {
|
||||
setConvertedAmount(tipAmountValue * exchangeRate);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
setConvertedAmount(amount * exchangeRate);
|
||||
}
|
||||
}, [amount, convertedAmount, exchangeRate, setConvertedAmount]);
|
||||
|
||||
// check if creator has a payment method saved
|
||||
React.useEffect(() => {
|
||||
if (!stripeEnvironment) return;
|
||||
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerStatusResponse) => {
|
||||
const defaultPaymentMethodId =
|
||||
customerStatusResponse.Customer &&
|
||||
customerStatusResponse.Customer.invoice_settings &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||
|
||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}, [setHasSavedCard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!stripeEnvironment) return;
|
||||
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
{
|
||||
channel_claim_id: channelClaimId,
|
||||
channel_name: tipChannelName,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((accountCheckResponse) => {
|
||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||
setCanReceiveFiatTip(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, channelClaimId, tipChannelName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let regexp;
|
||||
|
||||
if (amount === 0) {
|
||||
setTipError(__('Amount cannot be zero.'));
|
||||
} else if (!amount || typeof amount !== 'number') {
|
||||
setTipError(__('Amount must be a number.'));
|
||||
} else {
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
if (activeTab !== TAB_FIAT) {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
setTipError(__('Amount must have no more than 8 decimal places'));
|
||||
} else if (amount === balance) {
|
||||
setTipError(__('Please decrease the amount to account for transaction fees'));
|
||||
} else if (amount > balance || balance === 0) {
|
||||
setTipError(__('Not enough Credits'));
|
||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||
setTipError(__('Amount must be higher'));
|
||||
} else {
|
||||
setTipError(false);
|
||||
}
|
||||
// if tip fiat tab
|
||||
} else {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||
} else if (amount < 1) {
|
||||
setTipError(__('Amount must be at least one dollar'));
|
||||
} else if (amount > 1000) {
|
||||
setTipError(__('Amount cannot be over 1000 dollars'));
|
||||
} else {
|
||||
setTipError(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [activeTab, amount, balance, setTipError]);
|
||||
|
||||
const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
||||
{tipAmountsToDisplay.map((defaultAmount) => (
|
||||
<Button
|
||||
key={defaultAmount}
|
||||
disabled={shouldDisableAmountSelector(defaultAmount)}
|
||||
|
@ -186,9 +214,10 @@ function WalletTipAmountSelector(props: Props) {
|
|||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
button="alt"
|
||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
||||
disabled={shouldDisableFiatSelectors}
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': useCustomTip,
|
||||
})}
|
||||
|
@ -207,60 +236,26 @@ function WalletTipAmountSelector(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||
{__('Tip Creators')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Send a tip directly from your attached card</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{customTipAmount &&
|
||||
fiatConversion &&
|
||||
activeTab !== TAB_FIAT &&
|
||||
getHelpMessage(
|
||||
__(
|
||||
`This support is priced in $USD. ${
|
||||
convertedAmount
|
||||
? __(`The current exchange rate for the submitted amount is: $${convertToTwoDecimals(convertedAmount)}`)
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
)}
|
||||
|
||||
{/* custom number input form */}
|
||||
{useCustomTip && (
|
||||
<div className="comment__tip-input">
|
||||
<div className="walletTipSelector__input">
|
||||
<FormField
|
||||
autoFocus
|
||||
autoFocus={!isMobile}
|
||||
name="tip-input"
|
||||
disabled={shouldDisableAmountSelector()}
|
||||
label={
|
||||
activeTab === TAB_LBC ? (
|
||||
<React.Fragment>
|
||||
{__('Custom support amount')}{' '}
|
||||
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
|
||||
(%lbc_balance% available)
|
||||
</I18nMessage>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
// <>
|
||||
// <div className="">
|
||||
// <span className="help--spendable">Send a tip directly from your attached card</span>
|
||||
// </div>
|
||||
// </>
|
||||
}
|
||||
disabled={!customTipAmount && shouldDisableAmountSelector(0)}
|
||||
error={tipError}
|
||||
min="0"
|
||||
step="any"
|
||||
|
@ -274,35 +269,17 @@ function WalletTipAmountSelector(props: Props) {
|
|||
|
||||
{/* lbc tab */}
|
||||
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
||||
{/* fiat button but no card saved */}
|
||||
{!useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||
{__('Tip Creators')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Send a tip directly from your attached card</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === TAB_FIAT &&
|
||||
(!hasCardSaved
|
||||
? getHelpMessage(
|
||||
<>
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||
{__(' To Tip Creators')}
|
||||
</>
|
||||
)
|
||||
: !canReceiveFiatTip
|
||||
? getHelpMessage(__('Only creators that verify cash accounts can receive tips'))
|
||||
: getHelpMessage(__('Send a tip directly from your attached card')))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
107
ui/constants/stickers.js
Normal file
107
ui/constants/stickers.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
// @flow
|
||||
|
||||
const buildCDNUrl = (path: string) => `https://static.odycdn.com/stickers/${path}`;
|
||||
|
||||
const buildSticker = (name: string, path: string, price?: number) => ({
|
||||
name: __(`:${name}:`),
|
||||
url: buildCDNUrl(path),
|
||||
price: price,
|
||||
});
|
||||
|
||||
const CAT_BORDER = 'CAT/PNG/cat_with_border.png';
|
||||
const FAIL_BORDER = 'FAIL/PNG/fail_with_border.png';
|
||||
const HYPE_BORDER = 'HYPE/PNG/hype_with_border.png';
|
||||
const PANTS_1_WITH_FRAME = 'PANTS/PNG/PANTS_1_with_frame.png';
|
||||
const PANTS_2_WITH_FRAME = 'PANTS/PNG/PANTS_2_with_frame.png';
|
||||
const PISS = 'PISS/PNG/piss_with_frame.png';
|
||||
const PREGNANT_MAN_ASIA_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_asia.png';
|
||||
const PREGNANT_MAN_BLACK_HAIR_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_black%20hair.png';
|
||||
const PREGNANT_MAN_BLACK_SKIN_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_black%20skin.png';
|
||||
const PREGNANT_MAN_BLONDE_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_blondie.png';
|
||||
const PREGNANT_MAN_RED_HAIR_WHITE_BORDER =
|
||||
'pregnant%20man/png/Pregnant%20man_white%20border_red%20hair%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.png';
|
||||
const PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT_WHITE_BORDER =
|
||||
'pregnant%20woman/png/Pregnant%20woman_white_border_black%20hair%20green%20shirt.png';
|
||||
const PREGNANT_WOMAN_BLACK_HAIR_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_black%20hair.png';
|
||||
const PREGNANT_WOMAN_BLACK_SKIN_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_black%20woman.png';
|
||||
const PREGNANT_WOMAN_BLONDE_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_blondie.png';
|
||||
const PREGNANT_WOMAN_BROWN_HAIR_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_brown%20hair.png';
|
||||
const PREGNANT_WOMAN_RED_HAIR_WHITE_BORDER =
|
||||
'pregnant%20woman/png/Pregnant%20woman_white_border_red%20hair%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.png';
|
||||
const ROCKET_SPACEMAN_WITH_BORDER = 'ROCKET%20SPACEMAN/PNG/rocket-spaceman_with-border.png';
|
||||
const SALTY = 'SALTY/PNG/salty.png';
|
||||
const SICK_2_WITH_BORDER = 'SICK/PNG/sick2_with_border.png';
|
||||
const SICK_1_WITH_BORDERDARK_WITH_FRAME = 'SICK/PNG/with%20borderdark%20with%20frame.png';
|
||||
const SLIME_WITH_FRAME = 'SLIME/PNG/slime_with_frame.png';
|
||||
const SPHAGETTI_BATH_WITH_FRAME = 'SPHAGETTI%20BATH/PNG/sphagetti%20bath_with_frame.png';
|
||||
const THUG_LIFE_WITH_BORDER = 'THUG%20LIFE/PNG/thug_life_with_border_clean.png';
|
||||
const WHUUT_WITH_FRAME = 'WHUUT/PNG/whuut_with-frame.png';
|
||||
const COMET_TIP = 'TIPS/png/$%20comet%20tip%20with%20border.png';
|
||||
const BIG_LBC_TIP = 'TIPS/png/big_LBC_TIPV.png';
|
||||
const BIG_TIP = 'TIPS/png/with%20borderbig$tip.png';
|
||||
const BITE_TIP = 'TIPS/png/bite_$tip_with%20border.png';
|
||||
const BITE_TIP_CLOSEUP = 'TIPS/png/bite_$tip_closeup.png';
|
||||
const FORTUNE_CHEST_LBC = 'TIPS/png/with%20borderfortunechest_LBC_tip.png';
|
||||
const FORTUNE_CHEST = 'TIPS/png/with%20borderfortunechest$_tip.png';
|
||||
const LARGE_LBC_TIP = 'TIPS/png/with%20borderlarge_LBC_tip%20.png';
|
||||
const LARGE_TIP = 'TIPS/png/with%20borderlarge$tip.png';
|
||||
const BITE_LBC_CLOSEUP = 'TIPS/png/LBC%20bite.png';
|
||||
const LBC_COMET_TIP = 'TIPS/png/LBC%20comet%20tip%20with%20border.png';
|
||||
const MEDIUM_LBC_TIP = 'TIPS/png/with%20bordermedium_LBC_tip%20%20%20%20%20%20%20%20%20%20.png';
|
||||
const MEDIUM_TIP = 'TIPS/png/with%20bordermedium$_%20tip.png';
|
||||
const SILVER_ODYSEE_COIN = 'TIPS/png/with%20bordersilver_odysee_coinv.png';
|
||||
const SMALL_LBC_TIP = 'TIPS/png/with%20bordersmall_LBC_tip%20.png';
|
||||
const SMALL_TIP = 'TIPS/png/with%20bordersmall$_tip.png';
|
||||
const TIP_HAND_FLIP = 'TIPS/png/tip_hand_flip_$%20_with_border.png';
|
||||
const TIP_HAND_FLIP_COIN = 'TIPS/png/tip_hand_flip_coin_with_border.png';
|
||||
const TIP_HAND_FLIP_LBC = 'TIPS/png/tip_hand_flip_lbc_with_border.png';
|
||||
|
||||
export const FREE_GLOBAL_STICKERS = [
|
||||
buildSticker('CAT', CAT_BORDER),
|
||||
buildSticker('FAIL', FAIL_BORDER),
|
||||
buildSticker('HYPE', HYPE_BORDER),
|
||||
buildSticker('PANTS_1', PANTS_1_WITH_FRAME),
|
||||
buildSticker('PANTS_2', PANTS_2_WITH_FRAME),
|
||||
buildSticker('PISS', PISS),
|
||||
buildSticker('PREGNANT_MAN_ASIA', PREGNANT_MAN_ASIA_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_BLACK_HAIR', PREGNANT_MAN_BLACK_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_BLACK_SKIN', PREGNANT_MAN_BLACK_SKIN_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_BLONDE', PREGNANT_MAN_BLONDE_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_RED_HAIR', PREGNANT_MAN_RED_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT', PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLACK_HAIR', PREGNANT_WOMAN_BLACK_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLACK_SKIN', PREGNANT_WOMAN_BLACK_SKIN_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLONDE', PREGNANT_WOMAN_BLONDE_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BROWN_HAIR', PREGNANT_WOMAN_BROWN_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_RED_HAIR', PREGNANT_WOMAN_RED_HAIR_WHITE_BORDER),
|
||||
buildSticker('ROCKET_SPACEMAN', ROCKET_SPACEMAN_WITH_BORDER),
|
||||
buildSticker('SALTY', SALTY),
|
||||
buildSticker('SICK_FLAME', SICK_2_WITH_BORDER),
|
||||
buildSticker('SICK_SKULL', SICK_1_WITH_BORDERDARK_WITH_FRAME),
|
||||
buildSticker('SLIME', SLIME_WITH_FRAME),
|
||||
buildSticker('SPHAGETTI_BATH', SPHAGETTI_BATH_WITH_FRAME),
|
||||
buildSticker('THUG_LIFE', THUG_LIFE_WITH_BORDER),
|
||||
buildSticker('WHUUT', WHUUT_WITH_FRAME),
|
||||
];
|
||||
|
||||
export const PAID_GLOBAL_STICKERS = [
|
||||
buildSticker('TIP_HAND_FLIP', TIP_HAND_FLIP, 1),
|
||||
buildSticker('TIP_HAND_FLIP_COIN', TIP_HAND_FLIP_COIN, 1),
|
||||
buildSticker('TIP_HAND_FLIP_LBC', TIP_HAND_FLIP_LBC, 1),
|
||||
buildSticker('COMET_TIP', COMET_TIP, 25),
|
||||
buildSticker('LBC_COMET_TIP', LBC_COMET_TIP, 25),
|
||||
buildSticker('SMALL_TIP', SMALL_TIP, 25),
|
||||
buildSticker('SILVER_ODYSEE_COIN', SILVER_ODYSEE_COIN, 25),
|
||||
buildSticker('SMALL_LBC_TIP', SMALL_LBC_TIP, 25),
|
||||
buildSticker('BITE_TIP', BITE_TIP, 50),
|
||||
buildSticker('BITE_TIP_CLOSEUP', BITE_TIP_CLOSEUP, 50),
|
||||
buildSticker('BITE_LBC_CLOSEUP', BITE_LBC_CLOSEUP, 50),
|
||||
buildSticker('MEDIUM_TIP', MEDIUM_TIP, 50),
|
||||
buildSticker('MEDIUM_LBC_TIP', MEDIUM_LBC_TIP, 50),
|
||||
buildSticker('LARGE_TIP', LARGE_TIP, 100),
|
||||
buildSticker('LARGE_LBC_TIP', LARGE_LBC_TIP, 100),
|
||||
buildSticker('BIG_TIP', BIG_TIP, 150),
|
||||
buildSticker('BIG_LBC_TIP', BIG_LBC_TIP, 150),
|
||||
buildSticker('FORTUNE_CHEST', FORTUNE_CHEST, 200),
|
||||
buildSticker('FORTUNE_CHEST_LBC', FORTUNE_CHEST_LBC, 200),
|
||||
];
|
|
@ -553,6 +553,7 @@ export function doCommentReact(commentId: string, type: string) {
|
|||
* @param parent_id - What is this?
|
||||
* @param uri
|
||||
* @param livestream
|
||||
* @param sticker
|
||||
* @param {string} [txid] Optional transaction id
|
||||
* @param {string} [payment_intent_id] Optional transaction id
|
||||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||
|
@ -566,7 +567,8 @@ export function doCommentCreate(
|
|||
livestream?: boolean = false,
|
||||
txid?: string,
|
||||
payment_intent_id?: string,
|
||||
environment?: string
|
||||
environment?: string,
|
||||
sticker: boolean
|
||||
) {
|
||||
return async (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
|
@ -579,9 +581,7 @@ export function doCommentCreate(
|
|||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_CREATE_STARTED,
|
||||
});
|
||||
dispatch({ type: ACTIONS.COMMENT_CREATE_STARTED });
|
||||
|
||||
let signatureData;
|
||||
if (activeChannelClaim) {
|
||||
|
@ -594,12 +594,8 @@ export function doCommentCreate(
|
|||
}
|
||||
|
||||
// send a notification
|
||||
if (parent_id) {
|
||||
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
||||
if (notification && !notification.is_seen) {
|
||||
dispatch(doSeeNotifications([notification.id]));
|
||||
}
|
||||
}
|
||||
const notification = parent_id && makeSelectNotificationForCommentId(parent_id)(state);
|
||||
if (notification && !notification.is_seen) dispatch(doSeeNotifications([notification.id]));
|
||||
|
||||
if (!signatureData) {
|
||||
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||
|
@ -615,6 +611,7 @@ export function doCommentCreate(
|
|||
parent_id: parent_id,
|
||||
signature: signatureData.signature,
|
||||
signing_ts: signatureData.signing_ts,
|
||||
sticker: sticker,
|
||||
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
|
||||
...(payment_intent_id ? { payment_intent_id } : {}), // add payment_intent_id if it exists
|
||||
...(environment ? { environment } : {}), // add environment for stripe if it exists
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import Lbry from 'lbry';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import {
|
||||
selectBalance,
|
||||
|
@ -12,7 +13,6 @@ import {
|
|||
import { creditsToString } from 'util/format-credits';
|
||||
import { selectMyClaimsRaw, selectClaimsById } from 'redux/selectors/claims';
|
||||
import { doFetchChannelListMine, doFetchClaimListMine, doClaimSearch } from 'redux/actions/claims';
|
||||
|
||||
const FIFTEEN_SECONDS = 15000;
|
||||
let walletBalancePromise = null;
|
||||
|
||||
|
@ -705,3 +705,46 @@ export const doCheckPendingTxs = () => (dispatch, getState) => {
|
|||
checkTxList();
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
export const doSendCashTip = (tipParams, anonymous, userParams, claimId, stripeEnvironment, successCallback) => (
|
||||
dispatch
|
||||
) => {
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'tip',
|
||||
{
|
||||
// round to fix issues with floating point numbers
|
||||
amount: Math.round(100 * tipParams.tipAmount), // convert from dollars to cents
|
||||
creator_channel_name: tipParams.tipChannelName, // creator_channel_name
|
||||
creator_channel_claim_id: tipParams.channelClaimId,
|
||||
tipper_channel_name: anonymous ? '' : userParams.activeChannelName,
|
||||
tipper_channel_claim_id: anonymous ? '' : userParams.activeChannelId,
|
||||
currency: 'USD',
|
||||
anonymous: anonymous,
|
||||
source_claim_id: claimId,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((customerTipResponse) => {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __("You sent $%tipAmount% as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||
tipAmount: tipParams.tipAmount,
|
||||
tipChannelName: tipParams.tipChannelName,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
if (successCallback) successCallback(customerTipResponse);
|
||||
})
|
||||
.catch((error) => {
|
||||
// show error message from Stripe if one exists (being passed from backend by Beamer's API currently)
|
||||
dispatch(
|
||||
doToast({
|
||||
message: error.message || __('Sorry, there was an error in processing your payment!'),
|
||||
isError: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
$thumbnailWidth: 1.5rem;
|
||||
$thumbnailWidthSmall: 1rem;
|
||||
|
||||
.comment__create {
|
||||
.content_comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
|
@ -19,16 +23,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create--reply {
|
||||
.commentCreate--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content_comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment__create--nested-reply {
|
||||
.commentCreate--nestedReply {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
|
@ -37,11 +37,11 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create--bottom {
|
||||
.commentCreate--bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-new__label-wrapper {
|
||||
.commentCreate__labelWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
@ -49,6 +49,11 @@ $thumbnailWidthSmall: 1rem;
|
|||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.commentCreate__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
|
@ -56,27 +61,50 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment-new__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comment__sc-preview {
|
||||
.commentCreate__supportCommentPreview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
|
||||
.commentCreate__supportCommentPreviewAmount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__sc-preview-amount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
.comment--min-amount-notice {
|
||||
.commentCreate__minAmountNotice {
|
||||
.icon {
|
||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreview {
|
||||
@extend .commentCreate;
|
||||
display: flex;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
|
||||
.commentCreate__stickerPreviewInfo {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreviewImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
.filePrice {
|
||||
height: 1.5rem;
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -418,10 +418,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__tip-input {
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.comment--blocked {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
@ -482,3 +478,14 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticker__comment {
|
||||
margin-left: var(--spacing-m);
|
||||
height: 6rem;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
144
ui/scss/component/_file-price.scss
Normal file
144
ui/scss/component/_file-price.scss
Normal file
|
@ -0,0 +1,144 @@
|
|||
@import '../init/vars.scss';
|
||||
|
||||
.filePrice {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-purchased-text);
|
||||
|
||||
.credit-amount,
|
||||
.icon--Key {
|
||||
position: relative;
|
||||
margin-left: var(--spacing-m);
|
||||
white-space: nowrap;
|
||||
color: var(--color-purchased-text);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
left: 0;
|
||||
width: 250%;
|
||||
height: 160%;
|
||||
transform: skew(15deg);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-purchased-alt);
|
||||
border: 2px solid var(--color-purchased);
|
||||
}
|
||||
}
|
||||
|
||||
.filePrice--filepage {
|
||||
font-size: var(--font-body);
|
||||
top: calc(var(--spacing-xxs) * -1);
|
||||
margin-left: var(--spacing-m);
|
||||
|
||||
.credit-amount {
|
||||
margin: 0 var(--spacing-m);
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
height: 250%;
|
||||
left: calc(var(--spacing-m) * -1);
|
||||
border-radius: 0;
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-width: 5px;
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
|
||||
&::before {
|
||||
height: 140%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filePrice--modal {
|
||||
border: 5px solid var(--color-purchased);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-body);
|
||||
height: 4rem;
|
||||
background-color: var(--color-purchased-alt);
|
||||
transform: skew(15deg);
|
||||
|
||||
.icon,
|
||||
.credit-amount {
|
||||
transform: skew(-15deg);
|
||||
}
|
||||
|
||||
.credit-amount {
|
||||
margin: 0 var(--spacing-m);
|
||||
margin-left: var(--spacing-l);
|
||||
font-weight: var(--font-bold);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filePrice__key {
|
||||
@extend .filePrice;
|
||||
color: var(--color-gray-5);
|
||||
|
||||
.icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-purchased);
|
||||
height: 180%;
|
||||
}
|
||||
}
|
||||
|
||||
.filePrice__key--filepage {
|
||||
@extend .filePrice--filepage;
|
||||
top: 0;
|
||||
|
||||
&::before {
|
||||
height: 300%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 var(--spacing-m);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
&::before {
|
||||
top: calc(-1 * var(--spacing-s));
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
top: calc(-1 * var(--spacing-s));
|
||||
margin: 0 var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filePrice__key--modal {
|
||||
@extend .filePrice--modal;
|
||||
top: var(--spacing-m);
|
||||
|
||||
.icon {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
left: calc(var(--spacing-xl) * 1.5);
|
||||
animation: moveKey 2.5s 1 ease-out;
|
||||
overflow: visible;
|
||||
stroke: var(--color-black);
|
||||
|
||||
g {
|
||||
animation: turnKey 2.5s 1 ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
transform: skew(15deg);
|
||||
animation: expand 2.5s 1 ease-out;
|
||||
}
|
||||
}
|
|
@ -109,6 +109,8 @@ $recent-msg-button__height: 2rem;
|
|||
}
|
||||
|
||||
.livestream-comment--superchat {
|
||||
background-color: var(--color-card-background-highlighted);
|
||||
|
||||
+ .livestream-comment--superchat {
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
}
|
||||
|
@ -139,11 +141,15 @@ $recent-msg-button__height: 2rem;
|
|||
|
||||
.livestream-comment__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 2;
|
||||
align-items: flex-start;
|
||||
margin-left: var(--spacing-s);
|
||||
overflow: hidden;
|
||||
|
||||
.livestream-comment__info--sticker {
|
||||
display: flex;
|
||||
margin: var(--spacing-xxs) 0;
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(2rem);
|
||||
margin-top: var(--spacing-xxs);
|
||||
|
@ -338,6 +344,28 @@ $recent-msg-button__height: 2rem;
|
|||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
.livestream-superchat__info--sticker {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
width: 8rem;
|
||||
height: 3rem;
|
||||
|
||||
.livestream-superchat__info--image {
|
||||
padding-left: var(--spacing-m);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-top: calc(var(--spacing-xxs) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.livestream-superchat__info--not-sticker {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.livestream-superchat__banner {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
|
|
@ -15,7 +15,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create,
|
||||
.commentCreate,
|
||||
.comment__content {
|
||||
margin: var(--spacing-m);
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -7,155 +7,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-price {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-purchased-text);
|
||||
|
||||
.credit-amount,
|
||||
.icon--Key {
|
||||
position: relative;
|
||||
margin-left: var(--spacing-m);
|
||||
white-space: nowrap;
|
||||
color: var(--color-purchased-text);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
left: 0;
|
||||
width: 250%;
|
||||
height: 160%;
|
||||
transform: skew(15deg);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-purchased-alt);
|
||||
border: 2px solid var(--color-purchased);
|
||||
}
|
||||
}
|
||||
|
||||
.file-price__key {
|
||||
@extend .file-price;
|
||||
color: var(--color-gray-5);
|
||||
|
||||
.icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-purchased);
|
||||
height: 180%;
|
||||
}
|
||||
}
|
||||
|
||||
.file-price--filepage {
|
||||
font-size: var(--font-body);
|
||||
top: calc(var(--spacing-xxs) * -1);
|
||||
margin-left: var(--spacing-m);
|
||||
|
||||
.credit-amount {
|
||||
margin: 0 var(--spacing-m);
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
height: 250%;
|
||||
left: calc(var(--spacing-m) * -1);
|
||||
border-radius: 0;
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-width: 5px;
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
|
||||
&::before {
|
||||
height: 140%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-price__key--filepage {
|
||||
@extend .file-price--filepage;
|
||||
top: 0;
|
||||
|
||||
&::before {
|
||||
height: 300%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 var(--spacing-m);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
&::before {
|
||||
top: calc(-1 * var(--spacing-s));
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
top: calc(-1 * var(--spacing-s));
|
||||
margin: 0 var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-price--modal {
|
||||
border: 5px solid var(--color-purchased);
|
||||
|
||||
.credit-amount {
|
||||
margin: 0 var(--spacing-m);
|
||||
margin-left: var(--spacing-l);
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
}
|
||||
|
||||
.file-price--modal {
|
||||
font-size: var(--font-body);
|
||||
height: 4rem;
|
||||
background-color: var(--color-purchased-alt);
|
||||
border-radius: var(--border-radius);
|
||||
transform: skew(15deg);
|
||||
|
||||
.icon,
|
||||
.credit-amount {
|
||||
transform: skew(-15deg);
|
||||
}
|
||||
|
||||
.credit-amount {
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-price__key--modal {
|
||||
@extend .file-price--modal;
|
||||
top: var(--spacing-m);
|
||||
|
||||
.icon {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
left: calc(var(--spacing-xl) * 1.5);
|
||||
animation: moveKey 2.5s 1 ease-out;
|
||||
overflow: visible;
|
||||
stroke: var(--color-black);
|
||||
|
||||
g {
|
||||
animation: turnKey 2.5s 1 ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
transform: skew(15deg);
|
||||
animation: expand 2.5s 1 ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.purchase-stuff {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
82
ui/scss/component/_sticker-selector.scss
Normal file
82
ui/scss/component/_sticker-selector.scss
Normal file
|
@ -0,0 +1,82 @@
|
|||
@import '../init/vars';
|
||||
|
||||
.stickerSelector {
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
.stickerSelector__header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: var(--spacing-s);
|
||||
margin-bottom: 0;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xxs);
|
||||
|
||||
.stickerSelector__headerTitle {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__wrapper {
|
||||
height: unset;
|
||||
border-left: 1px solid var(--color-border);
|
||||
|
||||
.navigation-links {
|
||||
li {
|
||||
.button {
|
||||
padding: unset;
|
||||
|
||||
.button__content {
|
||||
justify-content: unset;
|
||||
flex-direction: unset;
|
||||
width: unset;
|
||||
|
||||
.button__label {
|
||||
font-size: var(--font-small);
|
||||
margin: 0 var(--spacing-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stickerSelector__list {
|
||||
display: flex;
|
||||
|
||||
.stickerSelector__listBody {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
max-height: 25vh;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.button--file-action {
|
||||
width: 5rem;
|
||||
height: 5.3rem;
|
||||
overflow: hidden;
|
||||
margin: unset;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.button__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.super-chat--light {
|
||||
position: absolute;
|
||||
display: inline;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-xsmall) {
|
||||
width: 4rem;
|
||||
height: 4.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
ui/scss/component/_wallet-tip-selector.scss
Normal file
3
ui/scss/component/_wallet-tip-selector.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.walletTipSelector__input {
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
// @flow
|
||||
import * as REACTION_TYPES from 'constants/reactions';
|
||||
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
|
||||
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||
import * as REACTION_TYPES from 'constants/reactions';
|
||||
|
||||
const ALL_VALID_STICKERS = [...FREE_GLOBAL_STICKERS, ...PAID_GLOBAL_STICKERS];
|
||||
const stickerRegex = /(<stkr>:[A-Z0-9_]+:<stkr>)/;
|
||||
|
||||
// Mostly taken from Reddit's sorting functions
|
||||
// https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx
|
||||
|
@ -88,3 +92,19 @@ export function sortComments(sortProps: SortProps): Array<Comment> {
|
|||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export const buildValidSticker = (sticker: string) => `<stkr>${sticker}<stkr>`;
|
||||
|
||||
export function parseSticker(comment: string) {
|
||||
const matchSticker = comment.match(stickerRegex);
|
||||
const stickerValue = matchSticker && matchSticker[0];
|
||||
const commentIsSticker = stickerValue && stickerValue.length === comment.length;
|
||||
|
||||
return (
|
||||
commentIsSticker &&
|
||||
ALL_VALID_STICKERS.find((sticker) => {
|
||||
// $FlowFixMe
|
||||
return sticker.name === stickerValue.replaceAll('<stkr>', '');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue