[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 CommentMenuList from 'component/commentMenuList';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import { parseSticker } from 'util/comments';
|
||||||
|
|
||||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||||
|
|
||||||
|
@ -138,6 +140,7 @@ function Comment(props: Props) {
|
||||||
const totalLikesAndDislikes = likesCount + dislikesCount;
|
const totalLikesAndDislikes = likesCount + dislikesCount;
|
||||||
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
||||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||||
|
const stickerFromMessage = parseSticker(message);
|
||||||
|
|
||||||
let channelOwnerOfContent;
|
let channelOwnerOfContent;
|
||||||
try {
|
try {
|
||||||
|
@ -338,6 +341,10 @@ function Comment(props: Props) {
|
||||||
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
||||||
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
||||||
</div>
|
</div>
|
||||||
|
) : stickerFromMessage ? (
|
||||||
|
<div className="sticker__comment">
|
||||||
|
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||||
|
</div>
|
||||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||||
<Expandable>
|
<Expandable>
|
||||||
<MarkdownPreview
|
<MarkdownPreview
|
||||||
|
|
|
@ -6,26 +6,26 @@ import {
|
||||||
selectFetchingMyChannels,
|
selectFetchingMyChannels,
|
||||||
makeSelectTagInClaimOrChannelForUri,
|
makeSelectTagInClaimOrChannelForUri,
|
||||||
} from 'redux/selectors/claims';
|
} 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 { 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 { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectSettingsByChannelId } from 'redux/selectors/comments';
|
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) => ({
|
const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
|
||||||
channels: selectMyChannelClaims(state),
|
|
||||||
isFetchingChannels: selectFetchingMyChannels(state),
|
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
channels: selectMyChannelClaims(state),
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
isFetchingChannels: selectFetchingMyChannels(state),
|
||||||
settingsByChannelId: selectSettingsByChannelId(state),
|
settingsByChannelId: selectSettingsByChannelId(state),
|
||||||
supportDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_SUPPORT_TAG)(state),
|
supportDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_SUPPORT_TAG)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
|
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment, sticker) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
doCommentCreate(
|
doCommentCreate(
|
||||||
comment,
|
comment,
|
||||||
|
@ -35,13 +35,16 @@ const perform = (dispatch, ownProps) => ({
|
||||||
ownProps.livestream,
|
ownProps.livestream,
|
||||||
txid,
|
txid,
|
||||||
payment_intent_id,
|
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)),
|
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||||
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
fetchComment: (commentId) => dispatch(doCommentById(commentId, false)),
|
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);
|
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
|
// @flow
|
||||||
import 'scss/component/_comment-create.scss';
|
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 { FF_MAX_CHARS_IN_COMMENT, FF_MAX_CHARS_IN_LIVESTREAM_COMMENT } from 'constants/form-field';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import { SIMPLE_SITE } from 'config';
|
import { SIMPLE_SITE } from 'config';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
@ -16,99 +16,113 @@ import classnames from 'classnames';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import EmoteSelector from './emote-selector';
|
import EmoteSelector from './emote-selector';
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
|
import FilePrice from 'component/filePrice';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SelectChannel from 'component/selectChannel';
|
import SelectChannel from 'component/selectChannel';
|
||||||
|
import StickerSelector from './sticker-selector';
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
const TAB_FIAT = 'TabFiat';
|
const TAB_FIAT = 'TabFiat';
|
||||||
const TAB_LBC = 'TabLBC';
|
const TAB_LBC = 'TabLBC';
|
||||||
const MENTION_DEBOUNCE_MS = 100;
|
const MENTION_DEBOUNCE_MS = 100;
|
||||||
|
|
||||||
|
type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
|
||||||
|
type UserParams = { activeChannelName: ?string, activeChannelId: ?string };
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
|
||||||
claim: StreamClaim,
|
|
||||||
channels: ?Array<ChannelClaim>,
|
|
||||||
isNested: boolean,
|
|
||||||
isFetchingChannels: boolean,
|
|
||||||
parentId: string,
|
|
||||||
isReply: boolean,
|
|
||||||
activeChannel: string,
|
activeChannel: string,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
bottom: boolean,
|
bottom: boolean,
|
||||||
livestream?: boolean,
|
channels: ?Array<ChannelClaim>,
|
||||||
embed?: boolean,
|
claim: StreamClaim,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
supportDisabled: boolean,
|
embed?: boolean,
|
||||||
|
isFetchingChannels: boolean,
|
||||||
|
isNested: boolean,
|
||||||
|
isReply: boolean,
|
||||||
|
livestream?: boolean,
|
||||||
|
parentId: string,
|
||||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||||
shouldFetchComment: boolean,
|
shouldFetchComment: boolean,
|
||||||
doToast: ({ message: string }) => void,
|
supportDisabled: boolean,
|
||||||
createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
|
uri: string,
|
||||||
onDoneReplying?: () => void,
|
createComment: (string, string, string, ?string, ?string, ?string, boolean) => Promise<any>,
|
||||||
onCancelReplying?: () => void,
|
|
||||||
toast: (string) => void,
|
|
||||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
|
||||||
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
||||||
setQuickReply: (any) => void,
|
doToast: ({ message: string }) => void,
|
||||||
fetchComment: (commentId: string) => Promise<any>,
|
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) {
|
export function CommentCreate(props: Props) {
|
||||||
const {
|
const {
|
||||||
uri,
|
|
||||||
claim,
|
|
||||||
channels,
|
|
||||||
isNested,
|
|
||||||
isFetchingChannels,
|
|
||||||
isReply,
|
|
||||||
parentId,
|
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
bottom,
|
bottom,
|
||||||
livestream,
|
channels,
|
||||||
embed,
|
claim,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
|
embed,
|
||||||
|
isFetchingChannels,
|
||||||
|
isNested,
|
||||||
|
isReply,
|
||||||
|
livestream,
|
||||||
|
parentId,
|
||||||
settingsByChannelId,
|
settingsByChannelId,
|
||||||
supportDisabled,
|
|
||||||
shouldFetchComment,
|
shouldFetchComment,
|
||||||
doToast,
|
supportDisabled,
|
||||||
|
uri,
|
||||||
createComment,
|
createComment,
|
||||||
onDoneReplying,
|
|
||||||
onCancelReplying,
|
|
||||||
sendTip,
|
|
||||||
doFetchCreatorSettings,
|
doFetchCreatorSettings,
|
||||||
setQuickReply,
|
doToast,
|
||||||
fetchComment,
|
fetchComment,
|
||||||
|
onCancelReplying,
|
||||||
|
onDoneReplying,
|
||||||
|
sendCashTip,
|
||||||
|
sendTip,
|
||||||
|
setQuickReply,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const formFieldRef: ElementRef<any> = React.useRef();
|
const formFieldRef: ElementRef<any> = React.useRef();
|
||||||
const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||||
const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart;
|
const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart;
|
||||||
const buttonRef: ElementRef<any> = React.useRef();
|
const buttonRef: ElementRef<any> = React.useRef();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setSubmitting] = React.useState(false);
|
||||||
const [commentFailure, setCommentFailure] = React.useState(false);
|
const [commentFailure, setCommentFailure] = React.useState(false);
|
||||||
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
|
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
|
||||||
const [isSupportComment, setIsSupportComment] = React.useState();
|
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 [tipAmount, setTipAmount] = React.useState(1);
|
||||||
|
const [convertedAmount, setConvertedAmount] = React.useState();
|
||||||
const [commentValue, setCommentValue] = React.useState('');
|
const [commentValue, setCommentValue] = React.useState('');
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
|
const [stickerSelector, setStickerSelector] = React.useState();
|
||||||
const [activeTab, setActiveTab] = React.useState('');
|
const [activeTab, setActiveTab] = React.useState('');
|
||||||
const [tipError, setTipError] = React.useState();
|
const [tipError, setTipError] = React.useState();
|
||||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||||
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
||||||
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
|
||||||
const [showEmotes, setShowEmotes] = React.useState(false);
|
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||||
|
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
||||||
|
|
||||||
const selectedMentionIndex =
|
const selectedMentionIndex =
|
||||||
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
||||||
|
@ -128,8 +142,7 @@ export function CommentCreate(props: Props) {
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const signingChannel = (claim && claim.signing_channel) || claim;
|
const channelUri = claim && (claim.signing_channel ? claim.signing_channel.permanent_url : claim.permanent_url);
|
||||||
const channelUri = signingChannel && signingChannel.permanent_url;
|
|
||||||
const hasChannels = channels && channels.length;
|
const hasChannels = channels && channels.length;
|
||||||
const charCount = commentValue ? commentValue.length : 0;
|
const charCount = commentValue ? commentValue.length : 0;
|
||||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend;
|
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend;
|
||||||
|
@ -143,31 +156,23 @@ export function CommentCreate(props: Props) {
|
||||||
const minAmountRef = React.useRef(minAmount);
|
const minAmountRef = React.useRef(minAmount);
|
||||||
minAmountRef.current = 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
|
// 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) {
|
function handleCommentChange(event) {
|
||||||
let commentValue;
|
let commentValue;
|
||||||
if (isReply) {
|
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() {
|
function handleSupportComment() {
|
||||||
if (!activeChannelClaim) {
|
if (!activeChannelClaim) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelId) {
|
if (!channelId) {
|
||||||
doToast({
|
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.'),
|
message: __('The creator just updated the minimum setting. Please revise or double-check your tip amount.'),
|
||||||
isError: true,
|
isError: true,
|
||||||
});
|
});
|
||||||
setIsReviewingSupportComment(false);
|
setReviewingSupportComment(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,33 +243,17 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSubmitTip() {
|
function doSubmitTip() {
|
||||||
if (!activeChannelClaim) {
|
if (!activeChannelClaim) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
amount: tipAmount,
|
|
||||||
claim_id: claimId,
|
|
||||||
channel_id: activeChannelClaim.claim_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const params = { amount: tipAmount, claim_id: claimId, channel_id: activeChannelClaim.claim_id };
|
||||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
|
|
||||||
// setup variables for tip API
|
// setup variables for tip API
|
||||||
let channelClaimId, tipChannelName;
|
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||||
// if there is a signing channel it's on a file
|
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||||
if (claim.signing_channel) {
|
|
||||||
channelClaimId = claim.signing_channel.claim_id;
|
|
||||||
tipChannelName = claim.signing_channel.name;
|
|
||||||
|
|
||||||
// otherwise it's on the channel page
|
setSubmitting(true);
|
||||||
} else {
|
|
||||||
channelClaimId = claim.claim_id;
|
|
||||||
tipChannelName = claim.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
if (activeTab === TAB_LBC) {
|
if (activeTab === TAB_LBC) {
|
||||||
// call sendTip and then run the callback from the response
|
// call sendTip and then run the callback from the response
|
||||||
|
@ -302,58 +281,23 @@ export function CommentCreate(props: Props) {
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
// reset the frontend so people can send a new comment
|
// reset the frontend so people can send a new comment
|
||||||
setIsSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const sourceClaimId = claim.claim_id;
|
const tipParams: TipParams = { tipAmount: Math.round(tipAmount * 100) / 100, tipChannelName, channelClaimId };
|
||||||
const roundedAmount = Math.round(tipAmount * 100) / 100;
|
const userParams: UserParams = { activeChannelName, activeChannelId };
|
||||||
|
|
||||||
Lbryio.call(
|
sendCashTip(tipParams, userParams, claim.claim_id, stripeEnvironment, (customerTipResponse) => {
|
||||||
'customer',
|
const { payment_intent_id } = customerTipResponse;
|
||||||
'tip',
|
|
||||||
{
|
|
||||||
// round to deal with floating point precision
|
|
||||||
amount: Math.round(100 * roundedAmount), // convert from dollars to cents
|
|
||||||
creator_channel_name: tipChannelName, // creator_channel_name
|
|
||||||
creator_channel_claim_id: channelClaimId,
|
|
||||||
tipper_channel_name: activeChannelName,
|
|
||||||
tipper_channel_claim_id: activeChannelId,
|
|
||||||
currency: 'USD',
|
|
||||||
anonymous: false,
|
|
||||||
source_claim_id: sourceClaimId,
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
)
|
|
||||||
.then((customerTipResponse) => {
|
|
||||||
const paymentIntendId = customerTipResponse.payment_intent_id;
|
|
||||||
|
|
||||||
handleCreateComment(null, paymentIntendId, stripeEnvironment);
|
handleCreateComment(null, payment_intent_id, stripeEnvironment);
|
||||||
|
|
||||||
setCommentValue('');
|
setCommentValue('');
|
||||||
setIsReviewingSupportComment(false);
|
setReviewingSupportComment(false);
|
||||||
setIsSupportComment(false);
|
setIsSupportComment(false);
|
||||||
setCommentFailure(false);
|
setCommentFailure(false);
|
||||||
setIsSubmitting(false);
|
setSubmitting(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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,16 +310,17 @@ export function CommentCreate(props: Props) {
|
||||||
*/
|
*/
|
||||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||||
setShowEmotes(false);
|
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) => {
|
.then((res) => {
|
||||||
setIsSubmitting(false);
|
setSubmitting(false);
|
||||||
if (setQuickReply) setQuickReply(res);
|
if (setQuickReply) setQuickReply(res);
|
||||||
|
|
||||||
if (res && res.signature) {
|
if (res && res.signature) {
|
||||||
setCommentValue('');
|
setCommentValue('');
|
||||||
setIsReviewingSupportComment(false);
|
setReviewingSupportComment(false);
|
||||||
setIsSupportComment(false);
|
setIsSupportComment(false);
|
||||||
setCommentFailure(false);
|
setCommentFailure(false);
|
||||||
|
|
||||||
|
@ -385,7 +330,7 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setIsSubmitting(false);
|
setSubmitting(false);
|
||||||
setCommentFailure(true);
|
setCommentFailure(true);
|
||||||
|
|
||||||
if (channelId) {
|
if (channelId) {
|
||||||
|
@ -432,6 +377,10 @@ export function CommentCreate(props: Props) {
|
||||||
// Render
|
// Render
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
|
const getActionButton = (title: string, icon: string, handleClick: () => void) => (
|
||||||
|
<Button title={title} button="alt" icon={icon} onClick={handleClick} />
|
||||||
|
);
|
||||||
|
|
||||||
if (channelSettings && !channelSettings.comments_enabled) {
|
if (channelSettings && !channelSettings.comments_enabled) {
|
||||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||||
}
|
}
|
||||||
|
@ -456,50 +405,7 @@ export function CommentCreate(props: Props) {
|
||||||
>
|
>
|
||||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||||
<div className="section__actions--no-margin">
|
<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">
|
|
||||||
<CreditAmount
|
|
||||||
className="comment__sc-preview-amount"
|
|
||||||
isFiat={activeTab === TAB_FIAT}
|
|
||||||
amount={tipAmount}
|
|
||||||
size={activeTab === TAB_LBC ? 18 : 2}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
|
||||||
<div>
|
|
||||||
<UriIndicator uri={activeChannelClaim.name} link />
|
|
||||||
<div>{commentValue}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="section__actions--no-margin">
|
|
||||||
<Button
|
|
||||||
autoFocus
|
|
||||||
button="primary"
|
|
||||||
disabled={disabled || !minAmountMet}
|
|
||||||
label={
|
|
||||||
isSubmitting
|
|
||||||
? __('Sending...')
|
|
||||||
: commentFailure && tipAmount === successTip.tipAmount
|
|
||||||
? __('Re-submit')
|
|
||||||
: __('Send')
|
|
||||||
}
|
|
||||||
onClick={handleSupportComment}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
button="link"
|
|
||||||
label={__('Cancel')}
|
|
||||||
onClick={() => setIsReviewingSupportComment(false)}
|
|
||||||
/>
|
|
||||||
{MinAmountNotice}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -507,12 +413,45 @@ export function CommentCreate(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
className={classnames('comment__create', {
|
className={classnames('commentCreate', {
|
||||||
'comment__create--reply': isReply,
|
'commentCreate--reply': isReply,
|
||||||
'comment__create--nested-reply': isNested,
|
'commentCreate--nestedReply': isNested,
|
||||||
'comment__create--bottom': bottom,
|
'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
|
||||||
|
amount={tipAmount}
|
||||||
|
className="commentCreate__supportCommentPreviewAmount"
|
||||||
|
isFiat={activeTab === TAB_FIAT}
|
||||||
|
size={activeTab === TAB_LBC ? 18 : 2}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
|
<div className="commentCreate__supportCommentBody">
|
||||||
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
|
<div>{commentValue}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{showEmotes && (
|
{showEmotes && (
|
||||||
<EmoteSelector
|
<EmoteSelector
|
||||||
commentValue={commentValue}
|
commentValue={commentValue}
|
||||||
|
@ -531,6 +470,7 @@ export function CommentCreate(props: Props) {
|
||||||
customSelectAction={handleSelectMention}
|
customSelectAction={handleSelectMention}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
disabled={isFetchingChannels}
|
disabled={isFetchingChannels}
|
||||||
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
||||||
|
@ -538,9 +478,9 @@ export function CommentCreate(props: Props) {
|
||||||
ref={formFieldRef}
|
ref={formFieldRef}
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={isReply ? 'content_reply' : 'content_comment'}
|
||||||
label={
|
label={
|
||||||
<span className="comment-new__label-wrapper">
|
<span className="commentCreate__labelWrapper">
|
||||||
{!livestream && (
|
{!livestream && (
|
||||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
<div className="commentCreate__label">{isReply ? __('Replying as ') : __('Comment as ')}</div>
|
||||||
)}
|
)}
|
||||||
<SelectChannel tiny />
|
<SelectChannel tiny />
|
||||||
</span>
|
</span>
|
||||||
|
@ -550,8 +490,8 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
quickActionHandler={() => !SIMPLE_SITE && setAdvancedEditor(!advancedEditor)}
|
quickActionHandler={() => !SIMPLE_SITE && setAdvancedEditor(!advancedEditor)}
|
||||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||||
onFocus={onTextareaFocus}
|
onFocus={() => window.addEventListener('keydown', altEnterListener)}
|
||||||
onBlur={onTextareaBlur}
|
onBlur={() => window.removeEventListener('keydown', altEnterListener)}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
value={commentValue}
|
value={commentValue}
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
|
@ -559,43 +499,82 @@ export function CommentCreate(props: Props) {
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
{isSupportComment && (
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isSupportComment || (isReviewingStickerComment && selectedSticker && selectedSticker.price)) && (
|
||||||
<WalletTipAmountSelector
|
<WalletTipAmountSelector
|
||||||
onTipErrorChange={setTipError}
|
|
||||||
shouldDisableReviewButton={setShouldDisableReviewButton}
|
|
||||||
claim={claim}
|
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
|
claim={claim}
|
||||||
|
convertedAmount={convertedAmount}
|
||||||
|
customTipAmount={selectedSticker && selectedSticker.price}
|
||||||
|
fiatConversion={selectedSticker && !!selectedSticker.price}
|
||||||
onChange={(amount) => setTipAmount(amount)}
|
onChange={(amount) => setTipAmount(amount)}
|
||||||
|
setConvertedAmount={setConvertedAmount}
|
||||||
|
setDisableSubmitButton={setDisableReviewButton}
|
||||||
|
setTipError={setTipError}
|
||||||
|
tipError={tipError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bottom Action Buttons */}
|
||||||
<div className="section__actions section__actions--no-margin">
|
<div className="section__actions section__actions--no-margin">
|
||||||
{isSupportComment ? (
|
{/* Submit Button */}
|
||||||
<>
|
{isReviewingSupportComment ? (
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
|
autoFocus
|
||||||
|
button="primary"
|
||||||
|
disabled={disabled || !minAmountMet}
|
||||||
|
label={
|
||||||
|
isSubmitting
|
||||||
|
? __('Sending...')
|
||||||
|
: commentFailure && tipAmount === successTip.tipAmount
|
||||||
|
? __('Re-submit')
|
||||||
|
: __('Send')
|
||||||
|
}
|
||||||
|
onClick={handleSupportComment}
|
||||||
|
/>
|
||||||
|
) : isReviewingStickerComment && selectedSticker ? (
|
||||||
|
<Button
|
||||||
|
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"
|
type="button"
|
||||||
button="primary"
|
button="primary"
|
||||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||||
label={__('Review')}
|
label={__('Review')}
|
||||||
onClick={() => setIsReviewingSupportComment(true)}
|
onClick={() => setReviewingSupportComment(true)}
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
button="link"
|
|
||||||
label={__('Cancel')}
|
|
||||||
onClick={() => setIsSupportComment(false)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
(!minTip || claimIsMine) && (
|
||||||
{(!minTip || claimIsMine) && (
|
|
||||||
<Button
|
<Button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
button="primary"
|
button="primary"
|
||||||
disabled={disabled}
|
disabled={disabled || stickerSelector}
|
||||||
type="submit"
|
type="submit"
|
||||||
label={
|
label={
|
||||||
isReply
|
isReply
|
||||||
|
@ -606,53 +585,96 @@ export function CommentCreate(props: Props) {
|
||||||
? __('Commenting...')
|
? __('Commenting...')
|
||||||
: __('Comment --[button to submit something]--')
|
: __('Comment --[button to submit something]--')
|
||||||
}
|
}
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth
|
||||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{!supportDisabled && !claimIsMine && (
|
|
||||||
|
{/** Stickers/Support Buttons **/}
|
||||||
|
{!supportDisabled && !stickerSelector && (
|
||||||
<>
|
<>
|
||||||
|
{isReviewingStickerComment ? (
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
|
||||||
button="alt"
|
button="alt"
|
||||||
className="thatButton"
|
label={__('Different Sticker')}
|
||||||
icon={ICONS.LBC}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setReviewingStickerComment(false);
|
||||||
|
setIsSupportComment(false);
|
||||||
|
setStickerSelector(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
getActionButton(__('Stickers'), ICONS.TAG, () => {
|
||||||
|
setIsSupportComment(false);
|
||||||
|
setStickerSelector(true);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
{!claimIsMine &&
|
||||||
|
getActionButton(__('LBC'), ICONS.LBC, () => {
|
||||||
setIsSupportComment(true);
|
setIsSupportComment(true);
|
||||||
setActiveTab(TAB_LBC);
|
setActiveTab(TAB_LBC);
|
||||||
}}
|
})}
|
||||||
/>
|
{!claimIsMine &&
|
||||||
{/* @if TARGET='web' */}
|
stripeEnvironment &&
|
||||||
{stripeEnvironment && (
|
getActionButton(__('Cash'), ICONS.FINANCE, () => {
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
|
||||||
button="alt"
|
|
||||||
className="thisButton"
|
|
||||||
icon={ICONS.FINANCE}
|
|
||||||
onClick={() => {
|
|
||||||
setIsSupportComment(true);
|
setIsSupportComment(true);
|
||||||
setActiveTab(TAB_FIAT);
|
setActiveTab(TAB_FIAT);
|
||||||
}}
|
})}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* @endif */}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isReply && !minTip && (
|
|
||||||
|
{/* Cancel Button */}
|
||||||
|
{(isSupportComment ||
|
||||||
|
isReviewingSupportComment ||
|
||||||
|
stickerSelector ||
|
||||||
|
isReviewingStickerComment ||
|
||||||
|
(isReply && !minTip)) && (
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isSupportComment && isSubmitting}
|
||||||
button="link"
|
button="link"
|
||||||
label={__('Cancel')}
|
label={__('Cancel')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onCancelReplying) {
|
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();
|
onCancelReplying();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
{/* Help Text */}
|
||||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
{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>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,15 +4,11 @@ import { makeSelectCostInfoForUri, doFetchCostInfoForUri, makeSelectFetchingCost
|
||||||
import FilePrice from './view';
|
import FilePrice from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
fetching: makeSelectFetchingCostInfoForUri(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) => ({
|
export default connect(select, { doFetchCostInfoForUri })(FilePrice);
|
||||||
fetchCostInfo: (uri) => dispatch(doFetchCostInfoForUri(uri)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(FilePrice);
|
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import 'scss/component/_file-price.scss';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
showFullPrice: boolean,
|
|
||||||
costInfo: ?{ includesData: boolean, cost: number },
|
|
||||||
fetchCostInfo: string => void,
|
|
||||||
uri: string,
|
|
||||||
fetching: boolean,
|
|
||||||
claim: ?{},
|
claim: ?{},
|
||||||
claimWasPurchased: boolean,
|
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
|
claimWasPurchased: boolean,
|
||||||
|
costInfo?: ?{ includesData: boolean, cost: number },
|
||||||
|
fetching: boolean,
|
||||||
|
showFullPrice: boolean,
|
||||||
type?: string,
|
type?: string,
|
||||||
|
uri: string,
|
||||||
// below props are just passed to <CreditAmount />
|
// below props are just passed to <CreditAmount />
|
||||||
inheritStyle?: boolean,
|
customPrice: number,
|
||||||
showLBC?: boolean,
|
|
||||||
hideFree?: boolean, // hide the file price if it's free
|
hideFree?: boolean, // hide the file price if it's free
|
||||||
|
isFiat?: boolean,
|
||||||
|
showLBC?: boolean,
|
||||||
|
doFetchCostInfoForUri: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilePrice extends React.PureComponent<Props> {
|
class FilePrice extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = { showFullPrice: false };
|
||||||
showFullPrice: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchCost(this.props);
|
this.fetchCost(this.props);
|
||||||
|
@ -35,40 +35,44 @@ class FilePrice extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCost = (props: Props) => {
|
fetchCost = (props: Props) => {
|
||||||
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
|
const { costInfo, uri, fetching, claim, doFetchCostInfoForUri } = props;
|
||||||
|
|
||||||
if (costInfo === undefined && !fetching && claim) {
|
if (uri && costInfo === undefined && !fetching && claim) doFetchCostInfoForUri(uri);
|
||||||
fetchCostInfo(uri);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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)) {
|
if (!customPrice && (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree))) return null;
|
||||||
return null;
|
|
||||||
}
|
const className = classnames(claimWasPurchased ? 'filePrice__key' : 'filePrice', {
|
||||||
|
'filePrice--filepage': type === 'filepage',
|
||||||
|
'filePrice--modal': type === 'modal',
|
||||||
|
});
|
||||||
|
|
||||||
return claimWasPurchased ? (
|
return claimWasPurchased ? (
|
||||||
<span
|
<span className={className}>
|
||||||
className={classnames('file-price__key', {
|
|
||||||
'file-price__key--filepage': type === 'filepage',
|
|
||||||
'file-price__key--modal': type === 'modal',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon icon={ICONS.PURCHASED} size={type === 'filepage' ? 22 : undefined} />
|
<Icon icon={ICONS.PURCHASED} size={type === 'filepage' ? 22 : undefined} />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
className={classnames('file-price', {
|
amount={costInfo ? costInfo.cost : customPrice}
|
||||||
'file-price--filepage': type === 'filepage',
|
className={className}
|
||||||
'file-price--modal': type === 'modal',
|
isEstimate={!!costInfo && !costInfo.includesData}
|
||||||
})}
|
isFiat={isFiat}
|
||||||
showFree
|
showFree
|
||||||
showLBC={showLBC}
|
|
||||||
amount={costInfo.cost}
|
|
||||||
isEstimate={!costInfo.includesData}
|
|
||||||
showFullPrice={showFullPrice}
|
showFullPrice={showFullPrice}
|
||||||
|
showLBC={showLBC}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import classnames from 'classnames';
|
||||||
import CommentMenuList from 'component/commentMenuList';
|
import CommentMenuList from 'component/commentMenuList';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import { parseSticker } from 'util/comments';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -45,11 +47,13 @@ function LivestreamComment(props: Props) {
|
||||||
|
|
||||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||||
const { claimName } = parseURI(authorUri);
|
const { claimName } = parseURI(authorUri);
|
||||||
|
const stickerFromMessage = parseSticker(message);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={classnames('livestream-comment', {
|
className={classnames('livestream-comment', {
|
||||||
'livestream-comment--superchat': supportAmount > 0,
|
'livestream-comment--superchat': supportAmount > 0,
|
||||||
|
'livestream-comment--sticker': Boolean(stickerFromMessage),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{supportAmount > 0 && (
|
{supportAmount > 0 && (
|
||||||
|
@ -60,8 +64,12 @@ function LivestreamComment(props: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="livestream-comment__body">
|
<div className="livestream-comment__body">
|
||||||
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
|
{(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||||
<div className="livestream-comment__info">
|
<div
|
||||||
|
className={classnames('livestream-comment__info', {
|
||||||
|
'livestream-comment__info--sticker': Boolean(stickerFromMessage),
|
||||||
|
})}
|
||||||
|
>
|
||||||
{isGlobalMod && (
|
{isGlobalMod && (
|
||||||
<Tooltip label={__('Admin')}>
|
<Tooltip label={__('Admin')}>
|
||||||
<span className="comment__badge comment__badge--global-mod">
|
<span className="comment__badge comment__badge--global-mod">
|
||||||
|
@ -103,9 +111,15 @@ function LivestreamComment(props: Props) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{stickerFromMessage ? (
|
||||||
|
<div className="sticker__comment">
|
||||||
|
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="livestream-comment__text">
|
<div className="livestream-comment__text">
|
||||||
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
|
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ import CreditAmount from 'component/common/credit-amount';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import { parseSticker } from 'util/comments';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -47,7 +49,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
superChats: superChatsByTipAmount,
|
superChats: superChatsByTipAmount,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let superChatsFiatAmount, superChatsTotalAmount;
|
let superChatsFiatAmount, superChatsLBCAmount, superChatsTotalAmount, hasSuperChats;
|
||||||
|
|
||||||
const commentsRef = React.createRef();
|
const commentsRef = React.createRef();
|
||||||
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
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
|
// which kind of superchat to display, either
|
||||||
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
|
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
|
||||||
|
const stickerSuperChats =
|
||||||
|
superChatsByTipAmount && superChatsByTipAmount.filter(({ comment }) => Boolean(parseSticker(comment)));
|
||||||
|
|
||||||
const discussionElement = document.querySelector('.livestream__comments');
|
const discussionElement = document.querySelector('.livestream__comments');
|
||||||
|
|
||||||
|
@ -130,7 +134,9 @@ export default function LivestreamComments(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
superChatsFiatAmount = fiatAmount;
|
superChatsFiatAmount = fiatAmount;
|
||||||
superChatsTotalAmount = LBCAmount;
|
superChatsLBCAmount = LBCAmount;
|
||||||
|
superChatsTotalAmount = superChatsFiatAmount + superChatsLBCAmount;
|
||||||
|
hasSuperChats = (superChatsTotalAmount || 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let superChatsReversed;
|
let superChatsReversed;
|
||||||
|
@ -160,11 +166,16 @@ export default function LivestreamComments(props: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStickerUrl(comment: string) {
|
||||||
|
const stickerFromComment = parseSticker(comment);
|
||||||
|
return stickerFromComment && stickerFromComment.url;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card livestream__discussion">
|
<div className="card livestream__discussion">
|
||||||
<div className="card__header--between livestream-discussion__header">
|
<div className="card__header--between livestream-discussion__header">
|
||||||
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
||||||
{(superChatsTotalAmount || 0) > 0 && (
|
{hasSuperChats && (
|
||||||
<div className="recommended-content__toggles">
|
<div className="recommended-content__toggles">
|
||||||
{/* the superchats in chronological order button */}
|
{/* the superchats in chronological order button */}
|
||||||
<Button
|
<Button
|
||||||
|
@ -186,7 +197,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
})}
|
})}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<CreditAmount amount={superChatsTotalAmount || 0} size={8} /> /
|
<CreditAmount amount={superChatsLBCAmount || 0} size={8} /> /
|
||||||
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
|
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -207,17 +218,30 @@ export default function LivestreamComments(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={commentsRef} className="livestream__comments-wrapper">
|
<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__wrapper">
|
||||||
<div className="livestream-superchats__inner">
|
<div className="livestream-superchats__inner">
|
||||||
{superChatsByTipAmount.map((superChat: Comment) => (
|
{superChatsByTipAmount.map((superChat: Comment) => {
|
||||||
<Tooltip key={superChat.comment_id} label={superChat.comment}>
|
const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat);
|
||||||
|
|
||||||
|
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">
|
||||||
<div className="livestream-superchat__thumbnail">
|
<div className="livestream-superchat__thumbnail">
|
||||||
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="livestream-superchat__info">
|
<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 />
|
<UriIndicator uri={superChat.channel_url} link />
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
size={10}
|
size={10}
|
||||||
|
@ -226,9 +250,16 @@ export default function LivestreamComments(props: Props) {
|
||||||
isFiat={superChat.is_fiat}
|
isFiat={superChat.is_fiat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
|
||||||
|
<div className="livestream-superchat__info--image">
|
||||||
|
<OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
)}
|
||||||
))}
|
</div>
|
||||||
|
</div>
|
||||||
|
</SuperChatWrapper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,36 +5,26 @@ import {
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
selectFetchingMyChannels,
|
selectFetchingMyChannels,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
import { doHideModal } from 'redux/actions/app';
|
||||||
import { doSendTip } from 'redux/actions/wallet';
|
import { doSendTip, doSendCashTip } 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 { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { withRouter } from 'react-router';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import WalletSendTip from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
isPending: selectIsSendingSupport(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
title: makeSelectTitleForUri(props.uri)(state),
|
|
||||||
claim: makeSelectClaimForUri(props.uri, false)(state),
|
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
|
claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||||
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
|
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
|
||||||
incognito: selectIncognito(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) => ({
|
export default withRouter(connect(select, { doHideModal, doSendTip, doSendCashTip })(WalletSendTip));
|
||||||
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));
|
|
||||||
|
|
|
@ -1,405 +1,176 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { Form } from 'component/common/form';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { parseURI } from 'util/lbryURI';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
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 Card from 'component/common/card';
|
||||||
import classnames from 'classnames';
|
|
||||||
import ChannelSelector from 'component/channelSelector';
|
import ChannelSelector from 'component/channelSelector';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import React from 'react';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
const 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 TAB_BOOST = 'TabBoost';
|
const TAB_BOOST = 'TabBoost';
|
||||||
const TAB_FIAT = 'TabFiat';
|
const TAB_FIAT = 'TabFiat';
|
||||||
const TAB_LBC = 'TabLBC';
|
const TAB_LBC = 'TabLBC';
|
||||||
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
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 = {
|
type Props = {
|
||||||
uri: string,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
claimIsMine: boolean,
|
|
||||||
title: string,
|
|
||||||
claim: StreamClaim,
|
|
||||||
isPending: boolean,
|
|
||||||
isSupport: boolean,
|
|
||||||
sendSupport: (SupportParams, boolean) => void,
|
|
||||||
closeModal: () => void,
|
|
||||||
balance: number,
|
balance: number,
|
||||||
|
claim: StreamClaim,
|
||||||
|
claimIsMine: boolean,
|
||||||
fetchingChannels: boolean,
|
fetchingChannels: boolean,
|
||||||
|
incognito: boolean,
|
||||||
instantTipEnabled: boolean,
|
instantTipEnabled: boolean,
|
||||||
instantTipMax: { amount: number, currency: string },
|
instantTipMax: { amount: number, currency: string },
|
||||||
activeChannelClaim: ?ChannelClaim,
|
isPending: boolean,
|
||||||
incognito: boolean,
|
isSupport: boolean,
|
||||||
doToast: ({ message: string }) => void,
|
title: string,
|
||||||
isAuthenticated: boolean,
|
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) {
|
function WalletSendTip(props: Props) {
|
||||||
const {
|
const {
|
||||||
uri,
|
activeChannelClaim,
|
||||||
title,
|
|
||||||
isPending,
|
|
||||||
claimIsMine,
|
|
||||||
balance,
|
balance,
|
||||||
claim = {},
|
claim = {},
|
||||||
instantTipEnabled,
|
claimIsMine,
|
||||||
instantTipMax,
|
|
||||||
sendSupport,
|
|
||||||
closeModal,
|
|
||||||
fetchingChannels,
|
fetchingChannels,
|
||||||
incognito,
|
incognito,
|
||||||
activeChannelClaim,
|
instantTipEnabled,
|
||||||
doToast,
|
instantTipMax,
|
||||||
isAuthenticated,
|
isPending,
|
||||||
|
title,
|
||||||
|
uri,
|
||||||
|
doHideModal,
|
||||||
|
doSendCashTip,
|
||||||
|
doSendTip,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
/** REACT STATE **/
|
/** 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);
|
|
||||||
|
|
||||||
// only allow certain creators to receive tips
|
const [tipAmount, setTipAmount] = usePersistedState('comment-support:customTip', 1.0);
|
||||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
const [isOnConfirmationPage, setConfirmationPage] = React.useState(false);
|
||||||
|
|
||||||
// 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 [tipError, setTipError] = React.useState();
|
const [tipError, setTipError] = React.useState();
|
||||||
|
|
||||||
// denote which tab to show on the frontend
|
|
||||||
const [activeTab, setActiveTab] = usePersistedState(TAB_BOOST);
|
const [activeTab, setActiveTab] = usePersistedState(TAB_BOOST);
|
||||||
|
const [disableSubmitButton, setDisableSubmitButton] = React.useState();
|
||||||
|
|
||||||
// handle default active tab
|
/** CONSTS **/
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 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;
|
const { claim_id: claimId } = claim;
|
||||||
|
let channelName;
|
||||||
// channel name used in url
|
try {
|
||||||
const { channelName } = parseURI(uri);
|
({ channelName } = parseURI(uri));
|
||||||
|
} catch (e) {}
|
||||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
|
|
||||||
// setup variables for backend tip API
|
// setup variables for backend tip API
|
||||||
let channelClaimId, tipChannelName;
|
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||||
// if there is a signing channel it's on a file
|
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||||
if (claim.signing_channel) {
|
|
||||||
channelClaimId = claim.signing_channel.claim_id;
|
|
||||||
tipChannelName = claim.signing_channel.name;
|
|
||||||
|
|
||||||
// otherwise it's on the channel page
|
// icon to use or explainer text to show per tab
|
||||||
} else {
|
let explainerText = '',
|
||||||
channelClaimId = claim.claim_id;
|
confirmLabel = '';
|
||||||
tipChannelName = claim.name;
|
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 sourceClaimId = claim.claim_id;
|
/** FUNCTIONS **/
|
||||||
|
|
||||||
// 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() {
|
function getClaimTypeText() {
|
||||||
if (claim.value_type === 'stream') {
|
switch (claim.value_type) {
|
||||||
|
case 'stream':
|
||||||
return __('Content');
|
return __('Content');
|
||||||
} else if (claim.value_type === 'channel') {
|
case 'channel':
|
||||||
return __('Channel');
|
return __('Channel');
|
||||||
} else if (claim.value_type === 'repost') {
|
case 'repost':
|
||||||
return __('Repost');
|
return __('Repost');
|
||||||
} else if (claim.value_type === 'collection') {
|
case 'collection':
|
||||||
return __('List');
|
return __('List');
|
||||||
} else {
|
default:
|
||||||
return __('Claim');
|
return __('Claim');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const claimTypeText = getClaimTypeText();
|
|
||||||
|
|
||||||
// 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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// make call to the backend to send lbc or fiat
|
||||||
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
||||||
// send a tip
|
if (!isOnConfirmationPage && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||||
if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
setConfirmationPage(true);
|
||||||
setIsConfirming(true);
|
|
||||||
} else {
|
} else {
|
||||||
// send a boost
|
const supportParams: SupportParams = {
|
||||||
const supportParams: SupportParams = { amount: tipAmount, claim_id: claimId };
|
amount: tipAmount,
|
||||||
|
claim_id: claimId,
|
||||||
// include channel name if donation not anonymous
|
channel_id: activeChannelClaim && !incognito ? activeChannelClaim.claim_id : undefined,
|
||||||
if (activeChannelClaim && !incognito) {
|
};
|
||||||
supportParams.channel_id = activeChannelClaim.claim_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send tip/boost
|
// send tip/boost
|
||||||
sendSupport(supportParams, isSupport);
|
doSendTip(supportParams, isSupport);
|
||||||
closeModal();
|
doHideModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when the form button is clicked
|
// when the form button is clicked
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (tipAmount && claimId) {
|
if (!tipAmount || !claimId) return;
|
||||||
|
|
||||||
// send an instant tip (no need to go to an exchange first)
|
// send an instant tip (no need to go to an exchange first)
|
||||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||||
if (instantTipMax.currency === 'LBC') {
|
if (instantTipMax.currency === 'LBC') {
|
||||||
sendSupportOrConfirm(instantTipMax.amount);
|
sendSupportOrConfirm(instantTipMax.amount);
|
||||||
} else {
|
} else {
|
||||||
// Need to convert currency of instant purchase maximum before trying to send support
|
// Need to convert currency of instant purchase maximum before trying to send support
|
||||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
Lbryio.getExchangeRates().then(({ LBC_USD }) => sendSupportOrConfirm(instantTipMax.amount / LBC_USD));
|
||||||
sendSupportOrConfirm(instantTipMax.amount / LBC_USD);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// sending fiat tip
|
// sending fiat tip
|
||||||
} else if (activeTab === TAB_FIAT) {
|
} else if (activeTab === TAB_FIAT) {
|
||||||
if (!isConfirming) {
|
if (!isOnConfirmationPage) {
|
||||||
setIsConfirming(true);
|
setConfirmationPage(true);
|
||||||
} else if (isConfirming) {
|
} else {
|
||||||
let sendAnonymously = !activeChannelClaim || incognito;
|
const tipParams: TipParams = { tipAmount, tipChannelName, channelClaimId };
|
||||||
|
const userParams: UserParams = { activeChannelName, activeChannelId };
|
||||||
|
|
||||||
// hit backend to send tip
|
// hit backend to send tip
|
||||||
Lbryio.call(
|
doSendCashTip(tipParams, !activeChannelClaim || incognito, userParams, claimId, stripeEnvironment);
|
||||||
'customer',
|
doHideModal();
|
||||||
'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 (?)
|
// if it's a boost (?)
|
||||||
} else {
|
} else {
|
||||||
sendSupportOrConfirm();
|
sendSupportOrConfirm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
setCustomTipAmount(tipAmount);
|
|
||||||
}
|
|
||||||
// LBC tip input
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildButtonText() {
|
function buildButtonText() {
|
||||||
// test if frontend will show up as isNan
|
// test if frontend will show up as isNan
|
||||||
|
@ -407,10 +178,7 @@ function WalletSendTip(props: Props) {
|
||||||
// testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137
|
// testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137
|
||||||
// also sometimes it's returned as a string
|
// also sometimes it's returned as a string
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
if (tipAmount !== tipAmount || tipAmount === 'NaN') {
|
return tipAmount !== tipAmount || tipAmount === 'NaN';
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertToTwoDecimals(number) {
|
function convertToTwoDecimals(number) {
|
||||||
|
@ -423,129 +191,65 @@ function WalletSendTip(props: Props) {
|
||||||
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
|
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
|
||||||
|
|
||||||
// build button text based on tab
|
// build button text based on tab
|
||||||
if (activeTab === TAB_BOOST) {
|
switch (activeTab) {
|
||||||
return claimIsMine
|
case TAB_BOOST:
|
||||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
return titleText;
|
||||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
case TAB_FIAT:
|
||||||
} else if (activeTab === TAB_FIAT) {
|
|
||||||
return __('Send a $%displayAmount% Tip', { displayAmount });
|
return __('Send a $%displayAmount% Tip', { displayAmount });
|
||||||
} else if (activeTab === TAB_LBC) {
|
case TAB_LBC:
|
||||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dont allow user to click send button
|
/** RENDER **/
|
||||||
function shouldDisableAmountSelector(amount) {
|
|
||||||
return (
|
|
||||||
(amount > balance && activeTab !== TAB_FIAT) || (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// showed on confirm page above amount
|
const getTabButton = (tabIcon: string, tabLabel: string, tabName: string) => (
|
||||||
function setConfirmLabel() {
|
<Button
|
||||||
if (activeTab === TAB_LBC) {
|
key={tabName}
|
||||||
return __('Tipping Credit');
|
icon={tabIcon}
|
||||||
} else if (activeTab === TAB_FIAT) {
|
label={tabLabel}
|
||||||
return __('Tipping Fiat (USD)');
|
button="alt"
|
||||||
} else if (activeTab === TAB_BOOST) {
|
onClick={() => {
|
||||||
return __('Boosting');
|
const tipInputElement = document.getElementById('tip-input');
|
||||||
}
|
if (tipInputElement) tipInputElement.focus();
|
||||||
}
|
if (!isOnConfirmationPage) setActiveTab(tabName);
|
||||||
|
}}
|
||||||
|
className={classnames('button-toggle', { 'button-toggle--active': activeTab === tabName })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
{/* if there is no LBC balance, show user frontend to get credits */}
|
{/* 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 */}
|
{/* if there is lbc, the main tip/boost gui with the 3 tabs at the top */}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={<LbcSymbol postfix={titleText} size={22} />}
|
||||||
<LbcSymbol
|
|
||||||
postfix={
|
|
||||||
claimIsMine
|
|
||||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
|
||||||
: __('Support This %claimTypeText%', { claimTypeText })
|
|
||||||
}
|
|
||||||
size={22}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
subtitle={
|
subtitle={
|
||||||
<React.Fragment>
|
<>
|
||||||
{!claimIsMine && (
|
{!claimIsMine && (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{/* tip LBC tab button */}
|
{/* tip LBC tab button */}
|
||||||
<Button
|
{getTabButton(ICONS.LBC, __('Tip'), TAB_LBC)}
|
||||||
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 })}
|
|
||||||
/>
|
|
||||||
{/* tip fiat tab button */}
|
{/* tip fiat tab button */}
|
||||||
{/* @if TARGET='web' */}
|
{stripeEnvironment && getTabButton(ICONS.FINANCE, __('Tip'), TAB_FIAT)}
|
||||||
{stripeEnvironment && (
|
|
||||||
<Button
|
{/* support LBC tab button */}
|
||||||
key="tip-fiat"
|
{getTabButton(ICONS.TRENDING, __('Boost'), TAB_BOOST)}
|
||||||
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 })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* short explainer under the button */}
|
{/* short explainer under the button */}
|
||||||
<div className="section__subtitle">
|
<div className="section__subtitle">
|
||||||
{explainerText + ' '}
|
{explainerText}
|
||||||
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
||||||
{
|
<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />
|
||||||
<Button
|
|
||||||
label={__('Learn more')}
|
|
||||||
button="link"
|
|
||||||
href="https://odysee.com/@OdyseeHelp:b/Monetization-of-Content:3"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
// confirmation modal, allow user to confirm or cancel transaction
|
// confirmation modal, allow user to confirm or cancel transaction
|
||||||
isConfirming ? (
|
isOnConfirmationPage ? (
|
||||||
<>
|
<>
|
||||||
<div className="section section--padded card--inline confirm__wrapper">
|
<div className="section section--padded card--inline confirm__wrapper">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
|
@ -555,10 +259,10 @@ function WalletSendTip(props: Props) {
|
||||||
<div className="confirm__value">
|
<div className="confirm__value">
|
||||||
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
||||||
</div>
|
</div>
|
||||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
<div className="confirm__label">{confirmLabel}</div>
|
||||||
<div className="confirm__value">
|
<div className="confirm__value">
|
||||||
{activeTab === TAB_FIAT ? (
|
{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} />
|
<LbcSymbol postfix={tipAmount} size={22} />
|
||||||
)}
|
)}
|
||||||
|
@ -567,98 +271,23 @@ function WalletSendTip(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button autoFocus onClick={handleSubmit} button="primary" disabled={isPending} label={__('Confirm')} />
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && noBalance) ? (
|
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && balance === 0) ? (
|
||||||
<>
|
<>
|
||||||
<div className="section">
|
|
||||||
<ChannelSelector />
|
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* section to pick tip/boost amount */}
|
{/* section to pick tip/boost amount */}
|
||||||
<div className="section">
|
<WalletTipAmountSelector
|
||||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
setTipError={setTipError}
|
||||||
<Button
|
tipError={tipError}
|
||||||
key={amount}
|
claim={claim}
|
||||||
disabled={shouldDisableAmountSelector(amount)}
|
activeTab={activeTab === TAB_BOOST ? TAB_LBC : activeTab}
|
||||||
button="alt"
|
amount={tipAmount}
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
onChange={(amount) => setTipAmount(amount)}
|
||||||
'button-toggle--active': tipAmount === amount && !useCustomTip,
|
setDisableSubmitButton={setDisableSubmitButton}
|
||||||
'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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* send tip/boost button */}
|
{/* send tip/boost button */}
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
|
@ -667,35 +296,25 @@ function WalletSendTip(props: Props) {
|
||||||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||||
button="primary"
|
button="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={
|
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
|
||||||
fetchingChannels ||
|
|
||||||
isPending ||
|
|
||||||
tipError ||
|
|
||||||
!tipAmount ||
|
|
||||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
|
||||||
}
|
|
||||||
label={buildButtonText()}
|
label={buildButtonText()}
|
||||||
/>
|
/>
|
||||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||||
</div>
|
</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
|
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Supporting content requires %lbc%</I18nMessage>
|
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>
|
||||||
|
{__('Supporting content requires %lbc%')}
|
||||||
|
</I18nMessage>
|
||||||
}
|
}
|
||||||
subtitle={
|
subtitle={
|
||||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>
|
<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>
|
</I18nMessage>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
|
@ -712,7 +331,7 @@ function WalletSendTip(props: Props) {
|
||||||
label={__('Buy/Swap Credits')}
|
label={__('Buy/Swap Credits')}
|
||||||
navigate={`/$/${PAGES.BUY}`}
|
navigate={`/$/${PAGES.BUY}`}
|
||||||
/>
|
/>
|
||||||
<Button button="link" label={__('Nevermind')} onClick={closeModal} />
|
<Button button="link" label={__('Nevermind')} onClick={doHideModal} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { connect } from 'react-redux';
|
||||||
import { selectBalance } from 'redux/selectors/wallet';
|
import { selectBalance } from 'redux/selectors/wallet';
|
||||||
import WalletSpendableBalanceHelp from './view';
|
import WalletSpendableBalanceHelp from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({ balance: selectBalance(state) });
|
||||||
balance: selectBalance(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select)(WalletSpendableBalanceHelp);
|
export default connect(select)(WalletSpendableBalanceHelp);
|
||||||
|
|
|
@ -1,33 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = { balance: number, inline?: boolean };
|
||||||
balance: number,
|
|
||||||
inline?: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
function WalletSpendableBalanceHelp(props: Props) {
|
function WalletSpendableBalanceHelp(props: Props) {
|
||||||
const { balance, inline } = props;
|
const { balance, inline } = props;
|
||||||
|
|
||||||
if (!balance) {
|
const getMessage = (text: string) => (
|
||||||
return null;
|
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>{text}</I18nMessage>
|
||||||
}
|
);
|
||||||
|
|
||||||
return inline ? (
|
return !balance ? null : inline ? (
|
||||||
<span className="help--spendable">
|
<span className="help--spendable">{getMessage(__('%balance% available.'))}</span>
|
||||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
|
||||||
%balance% available.
|
|
||||||
</I18nMessage>
|
|
||||||
</span>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="help">
|
<div className="help">{getMessage(__('Your immediately spendable balance is %balance%.'))}</div>
|
||||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
|
||||||
Your immediately spendable balance is %balance%.
|
|
||||||
</I18nMessage>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectBalance } from 'redux/selectors/wallet';
|
import { selectBalance } from 'redux/selectors/wallet';
|
||||||
import WalletTipAmountSelector from './view';
|
import WalletTipAmountSelector from './view';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state) => ({ balance: selectBalance(state) });
|
||||||
balance: selectBalance(state),
|
|
||||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
|
||||||
// claim: makeSelectClaimForUri(props.uri)(state),
|
|
||||||
// claim: makeSelectClaimForUri(props.uri, false)(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select)(WalletTipAmountSelector);
|
export default connect(select)(WalletTipAmountSelector);
|
||||||
|
|
|
@ -1,76 +1,116 @@
|
||||||
// @flow
|
// @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 ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
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 classnames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||||
|
|
||||||
const TAB_FIAT = 'TabFiat';
|
const TAB_FIAT = 'TabFiat';
|
||||||
const TAB_LBC = 'TabLBC';
|
const TAB_LBC = 'TabLBC';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
balance: number,
|
|
||||||
amount: number,
|
|
||||||
onChange: (number) => void,
|
|
||||||
isAuthenticated: boolean,
|
|
||||||
claim: StreamClaim,
|
|
||||||
uri: string,
|
|
||||||
onTipErrorChange: (string) => void,
|
|
||||||
activeTab: string,
|
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) {
|
function WalletTipAmountSelector(props: Props) {
|
||||||
const { balance, amount, onChange, activeTab, claim, onTipErrorChange, shouldDisableReviewButton } = props;
|
const {
|
||||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
activeTab,
|
||||||
const [tipError, setTipError] = React.useState();
|
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 [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
|
// if it's fiat but there's no card saved OR the creator can't receive fiat tips
|
||||||
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
|
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
|
||||||
|
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
|
* whether tip amount selection/review functionality should be disabled
|
||||||
* @param [amount] LBC amount (optional)
|
* @param [amount] LBC amount (optional)
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function shouldDisableAmountSelector(amount) {
|
function shouldDisableAmountSelector(amount: number) {
|
||||||
// if it's LBC but the balance isn't enough, or fiat conditions met
|
// if it's LBC but the balance isn't enough, or fiat conditions met
|
||||||
// $FlowFixMe
|
// $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);
|
// parse number as float and sets it in the parent component
|
||||||
|
function handleCustomPriceChange(amount: number) {
|
||||||
// setup variables for tip API
|
const tipAmountValue = parseFloat(amount);
|
||||||
let channelClaimId, tipChannelName;
|
onChange(tipAmountValue);
|
||||||
// if there is a signing channel it's on a file
|
if (fiatConversion && exchangeRate && setConvertedAmount && convertedAmount !== tipAmountValue * exchangeRate) {
|
||||||
if (claim.signing_channel) {
|
setConvertedAmount(tipAmountValue * exchangeRate);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// check if creator has a payment method saved
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (stripeEnvironment) {
|
if (!stripeEnvironment) return;
|
||||||
|
|
||||||
Lbryio.call(
|
Lbryio.call(
|
||||||
'customer',
|
'customer',
|
||||||
'status',
|
'status',
|
||||||
|
@ -87,12 +127,11 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
|
|
||||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||||
});
|
});
|
||||||
}
|
}, [setHasSavedCard]);
|
||||||
}, [stripeEnvironment]);
|
|
||||||
|
|
||||||
//
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (stripeEnvironment) {
|
if (!stripeEnvironment) return;
|
||||||
|
|
||||||
Lbryio.call(
|
Lbryio.call(
|
||||||
'account',
|
'account',
|
||||||
'check',
|
'check',
|
||||||
|
@ -108,38 +147,32 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
setCanReceiveFiatTip(true);
|
setCanReceiveFiatTip(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(() => {});
|
||||||
// console.log(error);
|
}, [canReceiveFiatTip, channelClaimId, tipChannelName]);
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [stripeEnvironment]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// setHasSavedCard(false);
|
let regexp;
|
||||||
// setCanReceiveFiatTip(true);
|
|
||||||
|
|
||||||
let regexp,
|
|
||||||
tipError = '';
|
|
||||||
|
|
||||||
if (amount === 0) {
|
if (amount === 0) {
|
||||||
tipError = __('Amount must be a positive number');
|
setTipError(__('Amount cannot be zero.'));
|
||||||
} else if (!amount || typeof amount !== 'number') {
|
} else if (!amount || typeof amount !== 'number') {
|
||||||
tipError = __('Amount must be a number');
|
setTipError(__('Amount must be a number.'));
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// if it's not fiat, aka it's boost or lbc tip
|
// if it's not fiat, aka it's boost or lbc tip
|
||||||
else if (activeTab !== TAB_FIAT) {
|
if (activeTab !== TAB_FIAT) {
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||||
const validTipInput = regexp.test(String(amount));
|
const validTipInput = regexp.test(String(amount));
|
||||||
|
|
||||||
if (!validTipInput) {
|
if (!validTipInput) {
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
setTipError(__('Amount must have no more than 8 decimal places'));
|
||||||
} else if (amount === balance) {
|
} else if (amount === balance) {
|
||||||
tipError = __('Please decrease the amount to account for transaction fees');
|
setTipError(__('Please decrease the amount to account for transaction fees'));
|
||||||
} else if (amount > balance) {
|
} else if (amount > balance || balance === 0) {
|
||||||
tipError = __('Not enough Credits');
|
setTipError(__('Not enough Credits'));
|
||||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||||
tipError = __('Amount must be higher');
|
setTipError(__('Amount must be higher'));
|
||||||
|
} else {
|
||||||
|
setTipError(false);
|
||||||
}
|
}
|
||||||
// if tip fiat tab
|
// if tip fiat tab
|
||||||
} else {
|
} else {
|
||||||
|
@ -147,29 +180,24 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
const validTipInput = regexp.test(String(amount));
|
const validTipInput = regexp.test(String(amount));
|
||||||
|
|
||||||
if (!validTipInput) {
|
if (!validTipInput) {
|
||||||
tipError = __('Amount must have no more than 2 decimal places');
|
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||||
} else if (amount < 1) {
|
} else if (amount < 1) {
|
||||||
tipError = __('Amount must be at least one dollar');
|
setTipError(__('Amount must be at least one dollar'));
|
||||||
} else if (amount > 1000) {
|
} else if (amount > 1000) {
|
||||||
tipError = __('Amount cannot be over 1000 dollars');
|
setTipError(__('Amount cannot be over 1000 dollars'));
|
||||||
|
} else {
|
||||||
|
setTipError(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
}, [activeTab, amount, balance, setTipError]);
|
||||||
|
|
||||||
|
const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
{tipAmountsToDisplay.map((defaultAmount) => (
|
||||||
<Button
|
<Button
|
||||||
key={defaultAmount}
|
key={defaultAmount}
|
||||||
disabled={shouldDisableAmountSelector(defaultAmount)}
|
disabled={shouldDisableAmountSelector(defaultAmount)}
|
||||||
|
@ -186,9 +214,10 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
disabled={shouldDisableFiatSelectors}
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
'button-toggle--active': useCustomTip,
|
'button-toggle--active': useCustomTip,
|
||||||
})}
|
})}
|
||||||
|
@ -207,60 +236,26 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
{customTipAmount &&
|
||||||
<>
|
fiatConversion &&
|
||||||
<div className="help">
|
activeTab !== TAB_FIAT &&
|
||||||
<span className="help--spendable">
|
getHelpMessage(
|
||||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
__(
|
||||||
{__('Tip Creators')}
|
`This support is priced in $USD. ${
|
||||||
</span>
|
convertedAmount
|
||||||
</div>
|
? __(`The current exchange rate for the submitted amount is: $${convertToTwoDecimals(convertedAmount)}`)
|
||||||
</>
|
: ''
|
||||||
)}
|
}`
|
||||||
|
)
|
||||||
{/* 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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* custom number input form */}
|
{/* custom number input form */}
|
||||||
{useCustomTip && (
|
{useCustomTip && (
|
||||||
<div className="comment__tip-input">
|
<div className="walletTipSelector__input">
|
||||||
<FormField
|
<FormField
|
||||||
autoFocus
|
autoFocus={!isMobile}
|
||||||
name="tip-input"
|
name="tip-input"
|
||||||
disabled={shouldDisableAmountSelector()}
|
disabled={!customTipAmount && shouldDisableAmountSelector(0)}
|
||||||
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>
|
|
||||||
// </>
|
|
||||||
}
|
|
||||||
error={tipError}
|
error={tipError}
|
||||||
min="0"
|
min="0"
|
||||||
step="any"
|
step="any"
|
||||||
|
@ -274,35 +269,17 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
|
|
||||||
{/* lbc tab */}
|
{/* lbc tab */}
|
||||||
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
||||||
{/* fiat button but no card saved */}
|
{activeTab === TAB_FIAT &&
|
||||||
{!useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
(!hasCardSaved
|
||||||
|
? getHelpMessage(
|
||||||
<>
|
<>
|
||||||
<div className="help">
|
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||||
<span className="help--spendable">
|
{__(' To Tip Creators')}
|
||||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
|
||||||
{__('Tip Creators')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
: !canReceiveFiatTip
|
||||||
{/* has card saved but cant creator cant receive tips */}
|
? getHelpMessage(__('Only creators that verify cash accounts can receive tips'))
|
||||||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
: getHelpMessage(__('Send a tip directly from your attached card')))}
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
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 parent_id - What is this?
|
||||||
* @param uri
|
* @param uri
|
||||||
* @param livestream
|
* @param livestream
|
||||||
|
* @param sticker
|
||||||
* @param {string} [txid] Optional transaction id
|
* @param {string} [txid] Optional transaction id
|
||||||
* @param {string} [payment_intent_id] Optional transaction id
|
* @param {string} [payment_intent_id] Optional transaction id
|
||||||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||||
|
@ -566,7 +567,8 @@ export function doCommentCreate(
|
||||||
livestream?: boolean = false,
|
livestream?: boolean = false,
|
||||||
txid?: string,
|
txid?: string,
|
||||||
payment_intent_id?: string,
|
payment_intent_id?: string,
|
||||||
environment?: string
|
environment?: string,
|
||||||
|
sticker: boolean
|
||||||
) {
|
) {
|
||||||
return async (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -579,9 +581,7 @@ export function doCommentCreate(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({ type: ACTIONS.COMMENT_CREATE_STARTED });
|
||||||
type: ACTIONS.COMMENT_CREATE_STARTED,
|
|
||||||
});
|
|
||||||
|
|
||||||
let signatureData;
|
let signatureData;
|
||||||
if (activeChannelClaim) {
|
if (activeChannelClaim) {
|
||||||
|
@ -594,12 +594,8 @@ export function doCommentCreate(
|
||||||
}
|
}
|
||||||
|
|
||||||
// send a notification
|
// send a notification
|
||||||
if (parent_id) {
|
const notification = parent_id && makeSelectNotificationForCommentId(parent_id)(state);
|
||||||
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
if (notification && !notification.is_seen) dispatch(doSeeNotifications([notification.id]));
|
||||||
if (notification && !notification.is_seen) {
|
|
||||||
dispatch(doSeeNotifications([notification.id]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signatureData) {
|
if (!signatureData) {
|
||||||
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
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,
|
parent_id: parent_id,
|
||||||
signature: signatureData.signature,
|
signature: signatureData.signature,
|
||||||
signing_ts: signatureData.signing_ts,
|
signing_ts: signatureData.signing_ts,
|
||||||
|
sticker: sticker,
|
||||||
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
|
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
|
||||||
...(payment_intent_id ? { payment_intent_id } : {}), // add payment_intent_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
|
...(environment ? { environment } : {}), // add environment for stripe if it exists
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import Lbry from 'lbry';
|
import Lbry from 'lbry';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import {
|
import {
|
||||||
selectBalance,
|
selectBalance,
|
||||||
|
@ -12,7 +13,6 @@ import {
|
||||||
import { creditsToString } from 'util/format-credits';
|
import { creditsToString } from 'util/format-credits';
|
||||||
import { selectMyClaimsRaw, selectClaimsById } from 'redux/selectors/claims';
|
import { selectMyClaimsRaw, selectClaimsById } from 'redux/selectors/claims';
|
||||||
import { doFetchChannelListMine, doFetchClaimListMine, doClaimSearch } from 'redux/actions/claims';
|
import { doFetchChannelListMine, doFetchClaimListMine, doClaimSearch } from 'redux/actions/claims';
|
||||||
|
|
||||||
const FIFTEEN_SECONDS = 15000;
|
const FIFTEEN_SECONDS = 15000;
|
||||||
let walletBalancePromise = null;
|
let walletBalancePromise = null;
|
||||||
|
|
||||||
|
@ -705,3 +705,46 @@ export const doCheckPendingTxs = () => (dispatch, getState) => {
|
||||||
checkTxList();
|
checkTxList();
|
||||||
}, 30000);
|
}, 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;
|
$thumbnailWidth: 1.5rem;
|
||||||
$thumbnailWidthSmall: 1rem;
|
$thumbnailWidthSmall: 1rem;
|
||||||
|
|
||||||
.comment__create {
|
.content_comment {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentCreate {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -19,16 +23,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__create--reply {
|
.commentCreate--reply {
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content_comment {
|
.commentCreate--nestedReply {
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__create--nested-reply {
|
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
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;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-new__label-wrapper {
|
.commentCreate__labelWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
@ -49,6 +49,11 @@ $thumbnailWidthSmall: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.commentCreate__label {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
max-width: 10rem;
|
max-width: 10rem;
|
||||||
|
@ -56,27 +61,50 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-new__label {
|
.commentCreate__supportCommentPreview {
|
||||||
white-space: nowrap;
|
|
||||||
margin-right: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__sc-preview {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
}
|
|
||||||
|
|
||||||
.comment__sc-preview-amount {
|
.commentCreate__supportCommentPreviewAmount {
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
font-size: var(--font-large);
|
font-size: var(--font-large);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--min-amount-notice {
|
.commentCreate__minAmountNotice {
|
||||||
.icon {
|
.icon {
|
||||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
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);
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__tip-input {
|
|
||||||
margin: var(--spacing-s) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment--blocked {
|
.comment--blocked {
|
||||||
opacity: 0.5;
|
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 {
|
.livestream-comment--superchat {
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
|
||||||
+ .livestream-comment--superchat {
|
+ .livestream-comment--superchat {
|
||||||
margin-bottom: var(--spacing-xxs);
|
margin-bottom: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
@ -139,11 +141,15 @@ $recent-msg-button__height: 2rem;
|
||||||
|
|
||||||
.livestream-comment__body {
|
.livestream-comment__body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: flex-start;
|
||||||
flex: 2;
|
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.livestream-comment__info--sticker {
|
||||||
|
display: flex;
|
||||||
|
margin: var(--spacing-xxs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-thumbnail {
|
.channel-thumbnail {
|
||||||
@include handleChannelGif(2rem);
|
@include handleChannelGif(2rem);
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
|
@ -338,6 +344,28 @@ $recent-msg-button__height: 2rem;
|
||||||
font-size: var(--font-xsmall);
|
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 {
|
.livestream-superchat__banner {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
|
|
|
@ -15,7 +15,7 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__create,
|
.commentCreate,
|
||||||
.comment__content {
|
.comment__content {
|
||||||
margin: var(--spacing-m);
|
margin: var(--spacing-m);
|
||||||
margin-bottom: 0;
|
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 {
|
.purchase-stuff {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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
|
// @flow
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
|
||||||
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
|
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
|
// Mostly taken from Reddit's sorting functions
|
||||||
// https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx
|
// 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;
|
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