[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
5c4cfdd4d8
commit
403321f064
23 changed files with 1352 additions and 1073 deletions
|
@ -2319,5 +2319,53 @@
|
|||
":WHAT:": ":WHAT:",
|
||||
":WOODOO_DOLL:": ":WOODOO_DOLL:",
|
||||
"Global Emotes": "Global Emotes",
|
||||
":CAT:": ":CAT:",
|
||||
":FAIL:": ":FAIL:",
|
||||
":HYPE:": ":HYPE:",
|
||||
":PANTS_1:": ":PANTS_1:",
|
||||
":PANTS_2:": ":PANTS_2:",
|
||||
":PISS:": ":PISS:",
|
||||
":PREGNANT_MAN_ASIA:": ":PREGNANT_MAN_ASIA:",
|
||||
":PREGNANT_MAN_BLACK_HAIR:": ":PREGNANT_MAN_BLACK_HAIR:",
|
||||
":PREGNANT_MAN_BLACK_SKIN:": ":PREGNANT_MAN_BLACK_SKIN:",
|
||||
":PREGNANT_MAN_BLONDE:": ":PREGNANT_MAN_BLONDE:",
|
||||
":PREGNANT_MAN_RED_HAIR:": ":PREGNANT_MAN_RED_HAIR:",
|
||||
":PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT:": ":PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT:",
|
||||
":PREGNANT_WOMAN_BLACK_HAIR:": ":PREGNANT_WOMAN_BLACK_HAIR:",
|
||||
":PREGNANT_WOMAN_BLACK_SKIN:": ":PREGNANT_WOMAN_BLACK_SKIN:",
|
||||
":PREGNANT_WOMAN_BLONDE:": ":PREGNANT_WOMAN_BLONDE:",
|
||||
":PREGNANT_WOMAN_BROWN_HAIR:": ":PREGNANT_WOMAN_BROWN_HAIR:",
|
||||
":PREGNANT_WOMAN_RED_HAIR:": ":PREGNANT_WOMAN_RED_HAIR:",
|
||||
":ROCKET_SPACEMAN:": ":ROCKET_SPACEMAN:",
|
||||
":SICK_FLAME:": ":SICK_FLAME:",
|
||||
":SICK_SKULL:": ":SICK_SKULL:",
|
||||
":SLIME:": ":SLIME:",
|
||||
":SPHAGETTI_BATH:": ":SPHAGETTI_BATH:",
|
||||
":THUG_LIFE:": ":THUG_LIFE:",
|
||||
":WHUUT:": ":WHUUT:",
|
||||
":TIP_HAND_FLIP:": ":TIP_HAND_FLIP:",
|
||||
":TIP_HAND_FLIP_COIN:": ":TIP_HAND_FLIP_COIN:",
|
||||
":TIP_HAND_FLIP_LBC:": ":TIP_HAND_FLIP_LBC:",
|
||||
":COMET_TIP:": ":COMET_TIP:",
|
||||
":LBC_COMET_TIP:": ":LBC_COMET_TIP:",
|
||||
":SMALL_TIP:": ":SMALL_TIP:",
|
||||
":SILVER_ODYSEE_COIN:": ":SILVER_ODYSEE_COIN:",
|
||||
":SMALL_LBC_TIP:": ":SMALL_LBC_TIP:",
|
||||
":BITE_TIP:": ":BITE_TIP:",
|
||||
":BITE_TIP_CLOSEUP:": ":BITE_TIP_CLOSEUP:",
|
||||
":BITE_LBC_CLOSEUP:": ":BITE_LBC_CLOSEUP:",
|
||||
":MEDIUM_TIP:": ":MEDIUM_TIP:",
|
||||
":MEDIUM_LBC_TIP:": ":MEDIUM_LBC_TIP:",
|
||||
":LARGE_TIP:": ":LARGE_TIP:",
|
||||
":LARGE_LBC_TIP:": ":LARGE_LBC_TIP:",
|
||||
":BIG_TIP:": ":BIG_TIP:",
|
||||
":BIG_LBC_TIP:": ":BIG_LBC_TIP:",
|
||||
":FORTUNE_CHEST:": ":FORTUNE_CHEST:",
|
||||
":FORTUNE_CHEST_LBC:": ":FORTUNE_CHEST_LBC:",
|
||||
"Stickers": "Stickers",
|
||||
"Different Sticker": "Different Sticker",
|
||||
"LBC": "LBC",
|
||||
"Add a Card": "Add a Card",
|
||||
" To Tip Creators": " To Tip Creators",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ import CommentCreate from 'component/commentCreate';
|
|||
import CommentMenuList from 'component/commentMenuList';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import { parseSticker } from 'util/comments';
|
||||
|
||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||
|
||||
|
@ -130,6 +132,7 @@ function Comment(props: Props) {
|
|||
const totalLikesAndDislikes = likesCount + dislikesCount;
|
||||
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||
const stickerFromMessage = parseSticker(message);
|
||||
|
||||
let channelOwnerOfContent;
|
||||
try {
|
||||
|
@ -324,6 +327,10 @@ function Comment(props: Props) {
|
|||
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
||||
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
||||
</div>
|
||||
) : stickerFromMessage ? (
|
||||
<div className="sticker__comment">
|
||||
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||
</div>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<Expandable>
|
||||
<MarkdownPreview
|
||||
|
|
|
@ -6,13 +6,13 @@ import {
|
|||
selectFetchingMyChannels,
|
||||
makeSelectTagInClaimOrChannelForUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { doSendTip } from 'redux/actions/wallet';
|
||||
import { CommentCreate } from './view';
|
||||
import { DISABLE_SUPPORT_TAG } from 'constants/tags';
|
||||
import { doCommentCreate, doFetchCreatorSettings, doCommentById } from 'redux/actions/comments';
|
||||
import { doSendTip } from 'redux/actions/wallet';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectSettingsByChannelId } from 'redux/selectors/comments';
|
||||
import { CommentCreate } from './view';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { DISABLE_SUPPORT_TAG } from 'constants/tags';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = selectClaimForUri(state, props.uri);
|
||||
|
@ -28,8 +28,8 @@ const select = (state, props) => {
|
|||
};
|
||||
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
|
||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment)),
|
||||
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment, sticker) =>
|
||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment, sticker)),
|
||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||
|
|
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// @flow
|
||||
import 'scss/component/_sticker-selector.scss';
|
||||
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
|
||||
const buildStickerSideLink = (section: string, icon: string) => ({ section, icon });
|
||||
|
||||
const STICKER_SIDE_LINKS = [
|
||||
buildStickerSideLink(__('Free'), ICONS.TAG),
|
||||
buildStickerSideLink(__('Tips'), ICONS.FINANCE),
|
||||
// Future work may include Channel, Subscriptions, ...
|
||||
];
|
||||
|
||||
type Props = { claimIsMine: boolean, onSelect: (any) => void };
|
||||
|
||||
export default function StickerSelector(props: Props) {
|
||||
const { claimIsMine, onSelect } = props;
|
||||
|
||||
function scrollToStickerSection(section: string) {
|
||||
const listBodyEl = document.querySelector('.stickerSelector__listBody');
|
||||
const sectionToScroll = document.getElementById(section);
|
||||
|
||||
if (listBodyEl && sectionToScroll) {
|
||||
// $FlowFixMe
|
||||
listBodyEl.scrollTo({
|
||||
top: sectionToScroll.offsetTop - sectionToScroll.getBoundingClientRect().height * 2,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getListRow = (rowTitle: string, rowStickers: any) => (
|
||||
<div className="stickerSelector__listBody-row">
|
||||
<div id={rowTitle} className="stickerSelector__listBody-rowTitle">
|
||||
{rowTitle}
|
||||
</div>
|
||||
<div className="stickerSelector__listBody-rowItems">
|
||||
{rowStickers.map((sticker) => (
|
||||
<Button
|
||||
key={sticker.name}
|
||||
title={sticker.name}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => onSelect(sticker)}
|
||||
>
|
||||
<OptimizedImage src={sticker.url} waitLoad />
|
||||
{sticker.price && sticker.price > 0 && (
|
||||
<CreditAmount superChatLight amount={sticker.price} size={2} isFiat />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="stickerSelector">
|
||||
<div className="stickerSelector__header card__header--between">
|
||||
<div className="stickerSelector__headerTitle card__title-section--small">{__('Stickers')}</div>
|
||||
</div>
|
||||
|
||||
<div className="stickerSelector__list">
|
||||
<div className="stickerSelector__listBody">
|
||||
{getListRow(__('Free'), FREE_GLOBAL_STICKERS)}
|
||||
{!claimIsMine && getListRow(__('Tips'), PAID_GLOBAL_STICKERS)}
|
||||
</div>
|
||||
|
||||
<div className="navigation__wrapper">
|
||||
<ul className="navigation-links">
|
||||
{STICKER_SIDE_LINKS.map(
|
||||
(linkProps) =>
|
||||
((claimIsMine && linkProps.section !== 'Tips') || !claimIsMine) && (
|
||||
<li key={linkProps.section}>
|
||||
<Button
|
||||
label={__(linkProps.section)}
|
||||
title={__(linkProps.section)}
|
||||
icon={linkProps.icon}
|
||||
iconSize={1}
|
||||
className="navigation-link"
|
||||
onClick={() => scrollToStickerSection(linkProps.section)}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import 'scss/component/_comment-create.scss';
|
||||
import { buildValidSticker } from 'util/comments';
|
||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { useHistory } from 'react-router';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
|
@ -15,96 +15,108 @@ import classnames from 'classnames';
|
|||
import CreditAmount from 'component/common/credit-amount';
|
||||
import EmoteSelector from './emote-selector';
|
||||
import Empty from 'component/common/empty';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import Icon from 'component/common/icon';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import StickerSelector from './sticker-selector';
|
||||
import type { ElementRef } from 'react';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
const MENTION_DEBOUNCE_MS = 100;
|
||||
|
||||
// for sendCashTip REMOVE
|
||||
// type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
|
||||
// type UserParams = { activeChannelName: ?string, activeChannelId: ?string };
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: StreamClaim,
|
||||
hasChannels: boolean,
|
||||
isNested: boolean,
|
||||
isFetchingChannels: boolean,
|
||||
parentId: string,
|
||||
isReply: boolean,
|
||||
activeChannel: string,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
bottom: boolean,
|
||||
embed?: boolean,
|
||||
hasChannels: boolean, //
|
||||
claim: StreamClaim,
|
||||
claimIsMine: boolean,
|
||||
supportDisabled: boolean,
|
||||
isFetchingChannels: boolean,
|
||||
isNested: boolean,
|
||||
isReply: boolean,
|
||||
parentId: string,
|
||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||
shouldFetchComment: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
|
||||
onDoneReplying?: () => void,
|
||||
onCancelReplying?: () => void,
|
||||
toast: (string) => void,
|
||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||
supportDisabled: boolean,
|
||||
uri: string,
|
||||
createComment: (string, string, string, ?string, ?string, ?string, ?boolean) => Promise<any>,
|
||||
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
||||
setQuickReply: (any) => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
fetchComment: (commentId: string) => Promise<any>,
|
||||
onCancelReplying?: () => void,
|
||||
onDoneReplying?: () => void,
|
||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||
setQuickReply: (any) => void,
|
||||
toast: (string) => void,
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
claim,
|
||||
hasChannels,
|
||||
isNested,
|
||||
isFetchingChannels,
|
||||
isReply,
|
||||
parentId,
|
||||
activeChannelClaim,
|
||||
bottom,
|
||||
hasChannels,
|
||||
claim,
|
||||
claimIsMine,
|
||||
isFetchingChannels,
|
||||
isNested,
|
||||
isReply,
|
||||
parentId,
|
||||
settingsByChannelId,
|
||||
supportDisabled,
|
||||
shouldFetchComment,
|
||||
doToast,
|
||||
supportDisabled,
|
||||
uri,
|
||||
createComment,
|
||||
onDoneReplying,
|
||||
onCancelReplying,
|
||||
sendTip,
|
||||
doFetchCreatorSettings,
|
||||
setQuickReply,
|
||||
doToast,
|
||||
fetchComment,
|
||||
onCancelReplying,
|
||||
onDoneReplying,
|
||||
sendTip,
|
||||
setQuickReply,
|
||||
} = props;
|
||||
|
||||
const formFieldRef: ElementRef<any> = React.useRef();
|
||||
const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||
const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart;
|
||||
const buttonRef: ElementRef<any> = React.useRef();
|
||||
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const [isSubmitting, setSubmitting] = React.useState(false);
|
||||
const [commentFailure, setCommentFailure] = React.useState(false);
|
||||
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
|
||||
const [isSupportComment, setIsSupportComment] = React.useState();
|
||||
const [isReviewingSupportComment, setIsReviewingSupportComment] = React.useState();
|
||||
const [isReviewingSupportComment, setReviewingSupportComment] = React.useState();
|
||||
const [isReviewingStickerComment, setReviewingStickerComment] = React.useState();
|
||||
const [selectedSticker, setSelectedSticker] = React.useState();
|
||||
const [tipAmount, setTipAmount] = React.useState(1);
|
||||
const [convertedAmount, setConvertedAmount] = React.useState();
|
||||
const [commentValue, setCommentValue] = React.useState('');
|
||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const [stickerSelector, setStickerSelector] = React.useState();
|
||||
const [activeTab, setActiveTab] = React.useState('');
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
||||
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
||||
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
||||
|
||||
const selectedMentionIndex =
|
||||
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
||||
|
@ -124,8 +136,7 @@ export function CommentCreate(props: Props) {
|
|||
: '';
|
||||
|
||||
const claimId = claim && claim.claim_id;
|
||||
const signingChannel = (claim && claim.signing_channel) || claim;
|
||||
const channelUri = signingChannel && signingChannel.permanent_url;
|
||||
const channelUri = claim && (claim.signing_channel ? claim.signing_channel.permanent_url : claim.permanent_url);
|
||||
const charCount = commentValue ? commentValue.length : 0;
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend;
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
|
@ -138,31 +149,23 @@ export function CommentCreate(props: Props) {
|
|||
const minAmountRef = React.useRef(minAmount);
|
||||
minAmountRef.current = minAmount;
|
||||
|
||||
const MinAmountNotice = minAmount ? (
|
||||
<div className="help--notice comment--min-amount-notice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
<Icon
|
||||
customTooltipText={
|
||||
minTip
|
||||
? __('This channel requires a minimum tip for each comment.')
|
||||
: minSuper
|
||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||
: ''
|
||||
}
|
||||
className="icon--help"
|
||||
icon={ICONS.HELP}
|
||||
tooltip
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// **************************************************************************
|
||||
// Functions
|
||||
// **************************************************************************
|
||||
|
||||
function handleSelectSticker(sticker: any) {
|
||||
// $FlowFixMe
|
||||
setSelectedSticker(sticker);
|
||||
setReviewingStickerComment(true);
|
||||
setTipAmount(sticker.price || 0);
|
||||
setStickerSelector(false);
|
||||
|
||||
if (sticker.price && sticker.price > 0) {
|
||||
setActiveTab(TAB_FIAT);
|
||||
setIsSupportComment(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommentChange(event) {
|
||||
let commentValue;
|
||||
if (isReply) {
|
||||
|
@ -203,9 +206,7 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
|
||||
function handleSupportComment() {
|
||||
if (!activeChannelClaim) {
|
||||
return;
|
||||
}
|
||||
if (!activeChannelClaim) return;
|
||||
|
||||
if (!channelId) {
|
||||
doToast({
|
||||
|
@ -233,7 +234,7 @@ export function CommentCreate(props: Props) {
|
|||
message: __('The creator just updated the minimum setting. Please revise or double-check your tip amount.'),
|
||||
isError: true,
|
||||
});
|
||||
setIsReviewingSupportComment(false);
|
||||
setReviewingSupportComment(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -242,33 +243,18 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
|
||||
function doSubmitTip() {
|
||||
if (!activeChannelClaim) {
|
||||
return;
|
||||
}
|
||||
if (!activeChannelClaim) return;
|
||||
|
||||
const params = {
|
||||
amount: tipAmount,
|
||||
claim_id: claimId,
|
||||
channel_id: activeChannelClaim.claim_id,
|
||||
};
|
||||
|
||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
const params = { amount: tipAmount, claim_id: claimId, channel_id: activeChannelClaim.claim_id };
|
||||
// FIAT ONLY - REMOVE
|
||||
// const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
// const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
|
||||
// setup variables for tip API
|
||||
let channelClaimId, tipChannelName;
|
||||
// if there is a signing channel it's on a file
|
||||
if (claim.signing_channel) {
|
||||
channelClaimId = claim.signing_channel.claim_id;
|
||||
tipChannelName = claim.signing_channel.name;
|
||||
// const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
||||
// otherwise it's on the channel page
|
||||
} else {
|
||||
channelClaimId = claim.claim_id;
|
||||
tipChannelName = claim.name;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitting(true);
|
||||
|
||||
if (activeTab === TAB_LBC) {
|
||||
// call sendTip and then run the callback from the response
|
||||
|
@ -296,59 +282,24 @@ export function CommentCreate(props: Props) {
|
|||
},
|
||||
() => {
|
||||
// reset the frontend so people can send a new comment
|
||||
setIsSubmitting(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const sourceClaimId = claim.claim_id;
|
||||
const roundedAmount = Math.round(tipAmount * 100) / 100;
|
||||
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'tip',
|
||||
{
|
||||
// round to deal with floating point precision
|
||||
amount: Math.round(100 * roundedAmount), // convert from dollars to cents
|
||||
creator_channel_name: tipChannelName, // creator_channel_name
|
||||
creator_channel_claim_id: channelClaimId,
|
||||
tipper_channel_name: activeChannelName,
|
||||
tipper_channel_claim_id: activeChannelId,
|
||||
currency: 'USD',
|
||||
anonymous: false,
|
||||
source_claim_id: sourceClaimId,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((customerTipResponse) => {
|
||||
const paymentIntendId = customerTipResponse.payment_intent_id;
|
||||
|
||||
handleCreateComment(null, paymentIntendId, stripeEnvironment);
|
||||
|
||||
setCommentValue('');
|
||||
setIsReviewingSupportComment(false);
|
||||
setIsSupportComment(false);
|
||||
setCommentFailure(false);
|
||||
setIsSubmitting(false);
|
||||
|
||||
doToast({
|
||||
message: __("You sent $%formattedAmount% as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||
formattedAmount: roundedAmount.toFixed(2), // force show decimal places
|
||||
tipChannelName,
|
||||
}),
|
||||
});
|
||||
|
||||
// handleCreateComment(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
doToast({
|
||||
message:
|
||||
error.message !== 'payment intent failed to confirm'
|
||||
? error.message
|
||||
: 'Sorry, there was an error in processing your payment!',
|
||||
isError: true,
|
||||
});
|
||||
});
|
||||
// No cash tips
|
||||
// const tipParams: TipParams = { tipAmount: Math.round(tipAmount * 100) / 100, tipChannelName, channelClaimId };
|
||||
// const userParams: UserParams = { activeChannelName, activeChannelId };
|
||||
// sendCashTip(tipParams, userParams, claim.claim_id, stripeEnvironment, (customerTipResponse) => {
|
||||
// const { payment_intent_id } = customerTipResponse;
|
||||
//
|
||||
// handleCreateComment(null, payment_intent_id, stripeEnvironment);
|
||||
//
|
||||
// setCommentValue('');
|
||||
// setReviewingSupportComment(false);
|
||||
// setIsSupportComment(false);
|
||||
// setCommentFailure(false);
|
||||
// setSubmitting(false);
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -360,16 +311,17 @@ export function CommentCreate(props: Props) {
|
|||
*/
|
||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||
setShowEmotes(false);
|
||||
setIsSubmitting(true);
|
||||
setSubmitting(true);
|
||||
const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name);
|
||||
|
||||
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
|
||||
createComment(stickerValue || commentValue, claimId, parentId, txid, payment_intent_id, environment, !!stickerValue)
|
||||
.then((res) => {
|
||||
setIsSubmitting(false);
|
||||
setSubmitting(false);
|
||||
if (setQuickReply) setQuickReply(res);
|
||||
|
||||
if (res && res.signature) {
|
||||
setCommentValue('');
|
||||
setIsReviewingSupportComment(false);
|
||||
setReviewingSupportComment(false);
|
||||
setIsSupportComment(false);
|
||||
setCommentFailure(false);
|
||||
|
||||
|
@ -379,7 +331,7 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSubmitting(false);
|
||||
setSubmitting(false);
|
||||
setCommentFailure(true);
|
||||
|
||||
if (channelId) {
|
||||
|
@ -426,6 +378,10 @@ export function CommentCreate(props: Props) {
|
|||
// Render
|
||||
// **************************************************************************
|
||||
|
||||
const getActionButton = (title: string, icon: string, handleClick: () => void) => (
|
||||
<Button title={title} button="alt" icon={icon} onClick={handleClick} />
|
||||
);
|
||||
|
||||
if (channelSettings && !channelSettings.comments_enabled) {
|
||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||
}
|
||||
|
@ -447,24 +403,106 @@ export function CommentCreate(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (isReviewingSupportComment && activeChannelClaim) {
|
||||
return (
|
||||
<div className="comment__create">
|
||||
<div className="comment__sc-preview">
|
||||
return (
|
||||
<Form
|
||||
className={classnames('commentCreate', {
|
||||
'commentCreate--reply': isReply,
|
||||
'commentCreate--nestedReply': isNested,
|
||||
'commentCreate--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{/* Input Box/Preview Box */}
|
||||
{stickerSelector ? (
|
||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||
<div className="commentCreate__stickerPreview">
|
||||
<div className="commentCreate__stickerPreviewInfo">
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
</div>
|
||||
<div className="commentCreate__stickerPreviewImage">
|
||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad />
|
||||
</div>
|
||||
{selectedSticker.price && <FilePrice customPrice={selectedSticker.price} isFiat />}
|
||||
</div>
|
||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||
<div className="commentCreate__supportCommentPreview">
|
||||
<CreditAmount
|
||||
className="comment__sc-preview-amount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
amount={tipAmount}
|
||||
className="commentCreate__supportCommentPreviewAmount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
size={activeTab === TAB_LBC ? 18 : 2}
|
||||
/>
|
||||
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<div>
|
||||
<UriIndicator uri={activeChannelClaim.name} link />
|
||||
<div className="commentCreate__supportCommentBody">
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
<div>{commentValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section__actions--no-margin">
|
||||
) : (
|
||||
<>
|
||||
{showEmotes && (
|
||||
<EmoteSelector
|
||||
commentValue={commentValue}
|
||||
setCommentValue={setCommentValue}
|
||||
closeSelector={() => setShowEmotes(false)}
|
||||
/>
|
||||
)}
|
||||
{!advancedEditor && (
|
||||
<ChannelMentionSuggestions
|
||||
uri={uri}
|
||||
inputRef={formFieldInputRef}
|
||||
mentionTerm={channelMention}
|
||||
creatorUri={channelUri}
|
||||
customSelectAction={handleSelectMention}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
disabled={isFetchingChannels}
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
ref={formFieldRef}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
label={
|
||||
<span className="commentCreate__labelWrapper">
|
||||
<div className="commentCreate__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||
<SelectChannel tiny />
|
||||
</span>
|
||||
}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
value={commentValue}
|
||||
charCount={charCount}
|
||||
onChange={handleCommentChange}
|
||||
autoFocus={isReply}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(isSupportComment || (isReviewingStickerComment && selectedSticker && selectedSticker.price)) && (
|
||||
<WalletTipAmountSelector
|
||||
activeTab={activeTab}
|
||||
amount={tipAmount}
|
||||
claim={claim}
|
||||
convertedAmount={convertedAmount}
|
||||
customTipAmount={selectedSticker && selectedSticker.price}
|
||||
fiatConversion={selectedSticker && !!selectedSticker.price} // remove
|
||||
onChange={(amount) => setTipAmount(amount)}
|
||||
setConvertedAmount={setConvertedAmount}
|
||||
setDisableSubmitButton={setDisableReviewButton}
|
||||
setTipError={setTipError}
|
||||
tipError={tipError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Bottom Action Buttons */}
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
{/* Submit Button */}
|
||||
{isReviewingSupportComment ? (
|
||||
<Button
|
||||
autoFocus
|
||||
button="primary"
|
||||
|
@ -478,145 +516,146 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
onClick={handleSupportComment}
|
||||
/>
|
||||
) : isReviewingStickerComment && selectedSticker ? (
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => setIsReviewingSupportComment(false)}
|
||||
button="primary"
|
||||
label={__('Send')}
|
||||
disabled={
|
||||
(isSupportComment && (tipError || disableReviewButton)) ||
|
||||
(selectedSticker &&
|
||||
selectedSticker.price &&
|
||||
(activeTab === TAB_FIAT
|
||||
? tipAmount < selectedSticker.price
|
||||
: convertedAmount && convertedAmount < selectedSticker.price))
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSupportComment) {
|
||||
handleSupportComment();
|
||||
} else {
|
||||
handleCreateComment();
|
||||
}
|
||||
setSelectedSticker(null);
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setIsSupportComment(false);
|
||||
}}
|
||||
/>
|
||||
) : isSupportComment ? (
|
||||
<Button
|
||||
disabled={disabled || tipError || disableReviewButton || !minAmountMet}
|
||||
type="button"
|
||||
button="primary"
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} // only LBC
|
||||
label={__('Review')}
|
||||
onClick={() => setReviewingSupportComment(true)}
|
||||
requiresAuth
|
||||
/>
|
||||
{MinAmountNotice}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={classnames('comment__create', {
|
||||
'comment__create--reply': isReply,
|
||||
'comment__create--nested-reply': isNested,
|
||||
'comment__create--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{showEmotes && (
|
||||
<EmoteSelector
|
||||
commentValue={commentValue}
|
||||
setCommentValue={setCommentValue}
|
||||
closeSelector={() => setShowEmotes(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!advancedEditor && (
|
||||
<ChannelMentionSuggestions
|
||||
uri={uri}
|
||||
inputRef={formFieldInputRef}
|
||||
mentionTerm={channelMention}
|
||||
creatorUri={channelUri}
|
||||
customSelectAction={handleSelectMention}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
disabled={isFetchingChannels}
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
ref={formFieldRef}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
label={
|
||||
<span className="comment-new__label-wrapper">
|
||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||
<SelectChannel tiny />
|
||||
</span>
|
||||
}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
value={commentValue}
|
||||
charCount={charCount}
|
||||
onChange={handleCommentChange}
|
||||
autoFocus={isReply}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
{isSupportComment && (
|
||||
<WalletTipAmountSelector
|
||||
onTipErrorChange={setTipError}
|
||||
shouldDisableReviewButton={setShouldDisableReviewButton}
|
||||
claim={claim}
|
||||
activeTab={activeTab}
|
||||
amount={tipAmount}
|
||||
onChange={(amount) => setTipAmount(amount)}
|
||||
/>
|
||||
)}
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
{isSupportComment ? (
|
||||
<>
|
||||
<Button
|
||||
disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
|
||||
type="button"
|
||||
button="primary"
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||
label={__('Review')}
|
||||
onClick={() => setIsReviewingSupportComment(true)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => setIsSupportComment(false)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
(!minTip || claimIsMine) && (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled || stickerSelector}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
requiresAuth
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{/** Stickers/Support Buttons **/}
|
||||
{!supportDisabled && !stickerSelector && (
|
||||
<>
|
||||
{(!minTip || claimIsMine) && (
|
||||
{isReviewingStickerComment ? (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
/>
|
||||
)}
|
||||
{!supportDisabled && !claimIsMine && (
|
||||
<>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
button="alt"
|
||||
className="thatButton"
|
||||
icon={ICONS.LBC}
|
||||
onClick={() => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_LBC);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isReply && !minTip && (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
button="alt"
|
||||
label={__('Different Sticker')}
|
||||
onClick={() => {
|
||||
if (onCancelReplying) {
|
||||
onCancelReplying();
|
||||
}
|
||||
setReviewingStickerComment(false);
|
||||
setIsSupportComment(false);
|
||||
setStickerSelector(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
getActionButton(__('Stickers'), ICONS.TAG, () => {
|
||||
setIsSupportComment(false);
|
||||
setStickerSelector(true);
|
||||
})
|
||||
)}
|
||||
{!claimIsMine &&
|
||||
getActionButton(__('LBC'), ICONS.LBC, () => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_LBC);
|
||||
})}
|
||||
{!claimIsMine &&
|
||||
stripeEnvironment &&
|
||||
getActionButton(__('Cash'), ICONS.FINANCE, () => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_FIAT);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cancel Button */}
|
||||
{(isSupportComment ||
|
||||
isReviewingSupportComment ||
|
||||
stickerSelector ||
|
||||
isReviewingStickerComment ||
|
||||
(isReply && !minTip)) && (
|
||||
<Button
|
||||
disabled={isSupportComment && isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => {
|
||||
if (isSupportComment || isReviewingSupportComment) {
|
||||
if (!isReviewingSupportComment) setIsSupportComment(false);
|
||||
setReviewingSupportComment(false);
|
||||
if (selectedSticker && selectedSticker.price) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setSelectedSticker(null);
|
||||
}
|
||||
} else if (stickerSelector || isReviewingStickerComment) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
} else if (isReply && !minTip && onCancelReplying) {
|
||||
onCancelReplying();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Help Text */}
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
{MinAmountNotice}
|
||||
{!!minAmount && (
|
||||
<div className="help--notice commentCreate__minAmountNotice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
<Icon
|
||||
customTooltipText={
|
||||
minTip
|
||||
? __('This channel requires a minimum tip for each comment.')
|
||||
: minSuper
|
||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||
: ''
|
||||
}
|
||||
className="icon--help"
|
||||
icon={ICONS.HELP}
|
||||
tooltip
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
// @flow
|
||||
import 'scss/component/_file-price.scss';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import Icon from 'component/common/icon';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
showFullPrice: boolean,
|
||||
costInfo: ?{ includesData: boolean, cost: number },
|
||||
doFetchCostInfoForUri: (string) => void,
|
||||
uri: string,
|
||||
fetching: boolean,
|
||||
claim: ?{},
|
||||
claimWasPurchased: boolean,
|
||||
claimIsMine: boolean,
|
||||
claimWasPurchased: boolean,
|
||||
costInfo: ?{ includesData: boolean, cost: number },
|
||||
fetching: boolean,
|
||||
showFullPrice: boolean,
|
||||
type?: string,
|
||||
uri: string,
|
||||
// below props are just passed to <CreditAmount />
|
||||
inheritStyle?: boolean,
|
||||
showLBC?: boolean,
|
||||
customPrice: number,
|
||||
hideFree?: boolean, // hide the file price if it's free
|
||||
isFiat?: boolean,
|
||||
showLBC?: boolean,
|
||||
doFetchCostInfoForUri: (string) => void,
|
||||
};
|
||||
|
||||
class FilePrice extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
showFullPrice: false,
|
||||
};
|
||||
static defaultProps = { showFullPrice: false };
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchCost(this.props);
|
||||
|
@ -37,38 +37,42 @@ class FilePrice extends React.PureComponent<Props> {
|
|||
fetchCost = (props: Props) => {
|
||||
const { costInfo, doFetchCostInfoForUri, uri, fetching, claim } = props;
|
||||
|
||||
if (costInfo === undefined && !fetching && claim) {
|
||||
doFetchCostInfoForUri(uri);
|
||||
}
|
||||
if (uri && costInfo === undefined && !fetching && claim) doFetchCostInfoForUri(uri);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { costInfo, showFullPrice, showLBC, hideFree, claimWasPurchased, type, claimIsMine } = this.props;
|
||||
const {
|
||||
costInfo,
|
||||
showFullPrice,
|
||||
showLBC,
|
||||
isFiat, // this goes
|
||||
hideFree,
|
||||
claimWasPurchased,
|
||||
type,
|
||||
claimIsMine,
|
||||
customPrice,
|
||||
} = this.props;
|
||||
|
||||
if (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree)) {
|
||||
return null;
|
||||
}
|
||||
if (!customPrice && (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree))) return null;
|
||||
|
||||
const className = classnames(claimWasPurchased ? 'filePrice__key' : 'filePrice', {
|
||||
'filePrice--filepage': type === 'filepage',
|
||||
'filePrice--modal': type === 'modal',
|
||||
});
|
||||
|
||||
return claimWasPurchased ? (
|
||||
<span
|
||||
className={classnames('file-price__key', {
|
||||
'file-price__key--filepage': type === 'filepage',
|
||||
'file-price__key--modal': type === 'modal',
|
||||
})}
|
||||
>
|
||||
<span className={className}>
|
||||
<Icon icon={ICONS.PURCHASED} size={type === 'filepage' ? 22 : undefined} />
|
||||
</span>
|
||||
) : (
|
||||
<CreditAmount
|
||||
className={classnames('file-price', {
|
||||
'file-price--filepage': type === 'filepage',
|
||||
'file-price--modal': type === 'modal',
|
||||
})}
|
||||
amount={costInfo ? costInfo.cost : customPrice}
|
||||
className={className}
|
||||
isEstimate={!!costInfo && !costInfo.includesData}
|
||||
isFiat={isFiat} // this goes
|
||||
showFree
|
||||
showLBC={showLBC}
|
||||
amount={costInfo.cost}
|
||||
isEstimate={!costInfo.includesData}
|
||||
showFullPrice={showFullPrice}
|
||||
showLBC={showLBC}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,36 +5,26 @@ import {
|
|||
makeSelectClaimIsMine,
|
||||
selectFetchingMyChannels,
|
||||
} from 'redux/selectors/claims';
|
||||
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import { doSendTip } from 'redux/actions/wallet';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import WalletSendTip from './view';
|
||||
import { doOpenModal, doHideModal } from 'redux/actions/app';
|
||||
import { withRouter } from 'react-router';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
||||
import { withRouter } from 'react-router';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import WalletSendTip from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
isPending: selectIsSendingSupport(state),
|
||||
title: selectTitleForUri(state, props.uri),
|
||||
claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
balance: selectBalance(state),
|
||||
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
|
||||
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
|
||||
claim: makeSelectClaimForUri(props.uri, false)(state), // find this selectClaim
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
incognito: selectIncognito(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
|
||||
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
|
||||
isPending: selectIsSendingSupport(state),
|
||||
title: selectTitleForUri(state, props.uri),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
sendSupport: (params, isSupport) => dispatch(doSendTip(params, isSupport)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(WalletSendTip));
|
||||
export default withRouter(connect(select, { doHideModal, doSendTip })(WalletSendTip)); // doSendCashTip gone
|
||||
|
|
|
@ -1,219 +1,162 @@
|
|||
// @flow
|
||||
import { Form } from 'component/common/form';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import Card from 'component/common/card';
|
||||
import classnames from 'classnames';
|
||||
import ChannelSelector from 'component/channelSelector';
|
||||
import classnames from 'classnames';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import React from 'react';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
|
||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
|
||||
const TAB_BOOST = 'TabBoost';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
|
||||
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
||||
// REMOVE (fiat only)
|
||||
// type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
|
||||
// type UserParams = { activeChannelName: ?string, activeChannelId: ?string };
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
title: string,
|
||||
claim: StreamClaim,
|
||||
isPending: boolean,
|
||||
isSupport: boolean,
|
||||
sendSupport: (SupportParams, boolean) => void,
|
||||
closeModal: () => void,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
balance: number,
|
||||
claim: StreamClaim,
|
||||
claimIsMine: boolean,
|
||||
fetchingChannels: boolean,
|
||||
incognito: boolean,
|
||||
instantTipEnabled: boolean,
|
||||
instantTipMax: { amount: number, currency: string },
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
incognito: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
isAuthenticated: boolean,
|
||||
isPending: boolean,
|
||||
isSupport: boolean,
|
||||
title: string,
|
||||
uri: string,
|
||||
doHideModal: () => void,
|
||||
doSendTip: (SupportParams, boolean) => void,
|
||||
};
|
||||
|
||||
function WalletSendTip(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
title,
|
||||
isPending,
|
||||
claimIsMine,
|
||||
activeChannelClaim,
|
||||
balance,
|
||||
claim = {},
|
||||
instantTipEnabled,
|
||||
instantTipMax,
|
||||
sendSupport,
|
||||
closeModal,
|
||||
claimIsMine,
|
||||
fetchingChannels,
|
||||
incognito,
|
||||
activeChannelClaim,
|
||||
instantTipEnabled,
|
||||
instantTipMax,
|
||||
isPending,
|
||||
title,
|
||||
uri,
|
||||
doHideModal,
|
||||
doSendTip,
|
||||
} = props;
|
||||
|
||||
/** REACT STATE **/
|
||||
const [presetTipAmount, setPresetTipAmount] = usePersistedState('comment-support:presetTip', DEFAULT_TIP_AMOUNTS[0]);
|
||||
const [customTipAmount, setCustomTipAmount] = usePersistedState('comment-support:customTip', 1.0);
|
||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||
const [isConfirming, setIsConfirming] = React.useState(false);
|
||||
/** STATE **/
|
||||
|
||||
// show the tip error on the frontend
|
||||
const [tipAmount, setTipAmount] = usePersistedState('comment-support:customTip', 1.0);
|
||||
const [isOnConfirmationPage, setConfirmationPage] = React.useState(false);
|
||||
const [tipError, setTipError] = React.useState();
|
||||
|
||||
// denote which tab to show on the frontend
|
||||
const [activeTab, setActiveTab] = usePersistedState(TAB_BOOST);
|
||||
const [disableSubmitButton, setDisableSubmitButton] = React.useState();
|
||||
|
||||
// handle default active tab
|
||||
React.useEffect(() => {
|
||||
// force to boost tab if it's someone's own upload
|
||||
if (claimIsMine) {
|
||||
setActiveTab(TAB_BOOST);
|
||||
} else {
|
||||
// or set LBC tip as the default if none is set yet
|
||||
if (!activeTab || activeTab === 'undefined') {
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
/** CONSTS **/
|
||||
|
||||
// alphanumeric claim id
|
||||
const { claim_id: claimId } = claim;
|
||||
|
||||
// channel name used in url
|
||||
const { channelName } = parseURI(uri);
|
||||
|
||||
// focus tip element if it exists
|
||||
React.useEffect(() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// if user has no balance, used to show conditional frontend
|
||||
const noBalance = balance === 0;
|
||||
|
||||
// the tip amount, based on if a preset or custom tip amount is being used
|
||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
||||
|
||||
// get type of claim (stream/channel/repost/collection) for display on frontend
|
||||
function getClaimTypeText() {
|
||||
if (claim.value_type === 'stream') {
|
||||
return __('Content');
|
||||
} else if (claim.value_type === 'channel') {
|
||||
return __('Channel');
|
||||
} else if (claim.value_type === 'repost') {
|
||||
return __('Repost');
|
||||
} else if (claim.value_type === 'collection') {
|
||||
return __('List');
|
||||
} else {
|
||||
return __('Claim');
|
||||
}
|
||||
}
|
||||
const claimTypeText = getClaimTypeText();
|
||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||
const titleText = claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||
const { claim_id: claimId } = claim;
|
||||
let channelName;
|
||||
try {
|
||||
({ channelName } = parseURI(uri));
|
||||
} catch (e) {}
|
||||
// don't need this - fiat only, for reference REMOVE
|
||||
// const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
// const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
//
|
||||
// // setup variables for backend 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;
|
||||
|
||||
// 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_LBC) {
|
||||
iconToUse = ICONS.LBC;
|
||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
||||
let explainerText = '',
|
||||
confirmLabel = '';
|
||||
switch (activeTab) {
|
||||
case TAB_BOOST:
|
||||
explainerText = __(
|
||||
'This refundable boost will improve the discoverability of this %claimTypeText% while active. ',
|
||||
{ claimTypeText }
|
||||
);
|
||||
confirmLabel = __('Boosting');
|
||||
break;
|
||||
case TAB_FIAT:
|
||||
explainerText = __('Show this channel your appreciation by sending a donation in USD. ');
|
||||
confirmLabel = __('Tipping Fiat (USD)');
|
||||
break;
|
||||
case TAB_LBC:
|
||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits. ');
|
||||
confirmLabel = __('Tipping Credit');
|
||||
break;
|
||||
}
|
||||
|
||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||
/** FUNCTIONS **/
|
||||
|
||||
React.useEffect(() => {
|
||||
// Regex for number up to 8 decimal places
|
||||
let regexp;
|
||||
let tipError;
|
||||
|
||||
if (tipAmount === 0) {
|
||||
tipError = __('Amount must be a positive number');
|
||||
} else if (!tipAmount || typeof tipAmount !== 'number') {
|
||||
tipError = __('Amount must be a number');
|
||||
function getClaimTypeText() {
|
||||
switch (claim.value_type) {
|
||||
case 'stream':
|
||||
return __('Content');
|
||||
case 'channel':
|
||||
return __('Channel');
|
||||
case 'repost':
|
||||
return __('Repost');
|
||||
case 'collection':
|
||||
return __('List');
|
||||
default:
|
||||
return __('Claim');
|
||||
}
|
||||
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
else {
|
||||
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
|
||||
}
|
||||
|
||||
setTipError(tipError);
|
||||
}, [tipAmount, balance, setTipError, activeTab]);
|
||||
}
|
||||
|
||||
// make call to the backend to send lbc or fiat
|
||||
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
||||
// send a tip
|
||||
if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||
setIsConfirming(true);
|
||||
if (!isOnConfirmationPage && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||
setConfirmationPage(true);
|
||||
} else {
|
||||
// send a boost
|
||||
const supportParams: SupportParams = { amount: tipAmount, claim_id: claimId };
|
||||
|
||||
// include channel name if donation not anonymous
|
||||
if (activeChannelClaim && !incognito) {
|
||||
supportParams.channel_id = activeChannelClaim.claim_id;
|
||||
}
|
||||
const supportParams: SupportParams = {
|
||||
amount: tipAmount,
|
||||
claim_id: claimId,
|
||||
channel_id: activeChannelClaim && !incognito ? activeChannelClaim.claim_id : undefined,
|
||||
};
|
||||
|
||||
// send tip/boost
|
||||
sendSupport(supportParams, isSupport);
|
||||
closeModal();
|
||||
doSendTip(supportParams, isSupport);
|
||||
doHideModal();
|
||||
}
|
||||
}
|
||||
|
||||
// when the form button is clicked
|
||||
function handleSubmit() {
|
||||
if (tipAmount && claimId) {
|
||||
// send an instant tip (no need to go to an exchange first)
|
||||
if (instantTipEnabled) {
|
||||
if (instantTipMax.currency === 'LBC') {
|
||||
sendSupportOrConfirm(instantTipMax.amount);
|
||||
} else {
|
||||
// Need to convert currency of instant purchase maximum before trying to send support
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
||||
sendSupportOrConfirm(instantTipMax.amount / LBC_USD);
|
||||
});
|
||||
}
|
||||
// sending fiat tip
|
||||
if (!tipAmount || !claimId) return;
|
||||
|
||||
// send an instant tip (no need to go to an exchange first)
|
||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||
if (instantTipMax.currency === 'LBC') {
|
||||
sendSupportOrConfirm(instantTipMax.amount);
|
||||
} else {
|
||||
sendSupportOrConfirm();
|
||||
// Need to convert currency of instant purchase maximum before trying to send support
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => sendSupportOrConfirm(instantTipMax.amount / LBC_USD));
|
||||
}
|
||||
} else {
|
||||
sendSupportOrConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
||||
let tipAmountAsString = event.target.value;
|
||||
|
||||
let tipAmount = parseFloat(tipAmountAsString);
|
||||
setCustomTipAmount(tipAmount);
|
||||
}
|
||||
|
||||
function buildButtonText() {
|
||||
// test if frontend will show up as isNan
|
||||
function isNan(tipAmount) {
|
||||
|
@ -223,100 +166,72 @@ function WalletSendTip(props: Props) {
|
|||
return tipAmount !== tipAmount || tipAmount === 'NaN';
|
||||
}
|
||||
|
||||
function convertToTwoDecimals(number) {
|
||||
return (Math.round(number * 100) / 100).toFixed(2);
|
||||
}
|
||||
|
||||
const amountToShow = activeTab === TAB_FIAT ? convertToTwoDecimals(tipAmount) : tipAmount;
|
||||
|
||||
// if it's a valid number display it, otherwise do an empty string
|
||||
const displayAmount = !isNan(tipAmount) ? tipAmount : '';
|
||||
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
|
||||
|
||||
// build button text based on tab
|
||||
if (activeTab === TAB_BOOST) {
|
||||
return claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||
} else if (activeTab === TAB_LBC) {
|
||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||
switch (activeTab) {
|
||||
case TAB_BOOST:
|
||||
return titleText;
|
||||
// case TAB_FIAT:
|
||||
// return __('Send a $%displayAmount% Tip', { displayAmount });
|
||||
case TAB_LBC:
|
||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||
}
|
||||
}
|
||||
|
||||
// dont allow user to click send button
|
||||
function shouldDisableAmountSelector(amount) {
|
||||
return amount > balance;
|
||||
}
|
||||
/** RENDER **/
|
||||
|
||||
// showed on confirm page above amount
|
||||
function setConfirmLabel() {
|
||||
if (activeTab === TAB_LBC) {
|
||||
return __('Tipping Credit');
|
||||
} else if (activeTab === TAB_BOOST) {
|
||||
return __('Boosting');
|
||||
}
|
||||
}
|
||||
const getTabButton = (tabIcon: string, tabLabel: string, tabName: string) => (
|
||||
<Button
|
||||
key={tabName}
|
||||
icon={tabIcon}
|
||||
label={tabLabel}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) tipInputElement.focus();
|
||||
if (!isOnConfirmationPage) setActiveTab(tabName);
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === tabName })}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
{/* if there is no LBC balance, show user frontend to get credits */}
|
||||
{/* if there is lbc, the main tip/boost gui with the 3 tabs at the top */}
|
||||
<Card
|
||||
title={
|
||||
<LbcSymbol
|
||||
postfix={
|
||||
claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Support This %claimTypeText%', { claimTypeText })
|
||||
}
|
||||
size={22}
|
||||
/>
|
||||
}
|
||||
title={<LbcSymbol postfix={titleText} size={22} />}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
<>
|
||||
{!claimIsMine && (
|
||||
<div className="section">
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="tip"
|
||||
icon={ICONS.LBC}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_LBC })}
|
||||
/>
|
||||
{/* 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 })}
|
||||
/>
|
||||
{getTabButton(ICONS.LBC, __('Tip'), TAB_LBC)}
|
||||
|
||||
{/* support LBC tab button */}
|
||||
{getTabButton(ICONS.TRENDING, __('Boost'), TAB_BOOST)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* short explainer under the button */}
|
||||
<div className="section__subtitle">
|
||||
{explainerText + ' '}
|
||||
{explainerText}
|
||||
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
||||
{<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
// confirmation modal, allow user to confirm or cancel transaction
|
||||
isConfirming ? (
|
||||
isOnConfirmationPage ? (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
|
@ -326,7 +241,7 @@ function WalletSendTip(props: Props) {
|
|||
<div className="confirm__value">
|
||||
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
||||
</div>
|
||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
||||
<div className="confirm__label">{confirmLabel}</div>
|
||||
<div className="confirm__value">
|
||||
<LbcSymbol postfix={tipAmount} size={22} />
|
||||
</div>
|
||||
|
@ -334,85 +249,23 @@ function WalletSendTip(props: Props) {
|
|||
</div>
|
||||
<div className="section__actions">
|
||||
<Button autoFocus onClick={handleSubmit} button="primary" disabled={isPending} label={__('Confirm')} />
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setIsConfirming(false)} />
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setConfirmationPage(false)} />
|
||||
</div>
|
||||
</>
|
||||
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && noBalance) ? (
|
||||
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && balance === 0) ? (
|
||||
<>
|
||||
<div className="section">
|
||||
<ChannelSelector />
|
||||
</div>
|
||||
<ChannelSelector />
|
||||
|
||||
{/* section to pick tip/boost amount */}
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
||||
<Button
|
||||
key={amount}
|
||||
disabled={shouldDisableAmountSelector(amount)}
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': tipAmount === amount && !useCustomTip,
|
||||
'button-toggle--disabled': amount > balance,
|
||||
})}
|
||||
label={amount}
|
||||
icon={iconToUse}
|
||||
onClick={() => {
|
||||
setPresetTipAmount(amount);
|
||||
setUseCustomTip(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': useCustomTip, // set as active
|
||||
})}
|
||||
icon={iconToUse}
|
||||
label={__('Custom')}
|
||||
onClick={() => setUseCustomTip(true)}
|
||||
// disabled if it's receive fiat and there is no card or creator can't receive tips
|
||||
/>
|
||||
|
||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
|
||||
<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')}{' '}
|
||||
<I18nMessage
|
||||
tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} showLBC={false} /> }}
|
||||
>
|
||||
(%lbc_balance% Credits available)
|
||||
</I18nMessage>
|
||||
</React.Fragment>
|
||||
}
|
||||
error={tipError}
|
||||
min="0"
|
||||
step="any"
|
||||
type="number"
|
||||
style={{
|
||||
width: '160px',
|
||||
}}
|
||||
placeholder="1.23"
|
||||
value={customTipAmount}
|
||||
onChange={(event) => handleCustomPriceChange(event)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<WalletTipAmountSelector
|
||||
setTipError={setTipError}
|
||||
tipError={tipError}
|
||||
claim={claim}
|
||||
activeTab={TAB_LBC} // active tab
|
||||
amount={tipAmount}
|
||||
onChange={(amount) => setTipAmount(amount)}
|
||||
setDisableSubmitButton={setDisableSubmitButton}
|
||||
/>
|
||||
|
||||
{/* send tip/boost button */}
|
||||
<div className="section__actions">
|
||||
|
@ -421,23 +274,25 @@ function WalletSendTip(props: Props) {
|
|||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||
button="primary"
|
||||
type="submit"
|
||||
disabled={fetchingChannels || isPending || tipError || !tipAmount}
|
||||
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
|
||||
label={buildButtonText()}
|
||||
/>
|
||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||
</div>
|
||||
<WalletSpendableBalanceHelp />
|
||||
</>
|
||||
) : (
|
||||
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
||||
<Card
|
||||
title={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Supporting content requires %lbc%</I18nMessage>
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>
|
||||
{__('Supporting content requires %lbc%')}
|
||||
</I18nMessage>
|
||||
}
|
||||
subtitle={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>
|
||||
With %lbc%, you can send tips to your favorite creators, or help boost their content for more people
|
||||
to see.
|
||||
{__(
|
||||
'With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.'
|
||||
)}
|
||||
</I18nMessage>
|
||||
}
|
||||
actions={
|
||||
|
@ -454,7 +309,7 @@ function WalletSendTip(props: Props) {
|
|||
label={__('Buy/Swap Credits')}
|
||||
navigate={`/$/${PAGES.BUY}`}
|
||||
/>
|
||||
<Button button="link" label={__('Nevermind')} onClick={closeModal} />
|
||||
<Button button="link" label={__('Nevermind')} onClick={doHideModal} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -2,8 +2,6 @@ import { connect } from 'react-redux';
|
|||
import { selectBalance } from 'redux/selectors/wallet';
|
||||
import WalletSpendableBalanceHelp from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
const select = (state) => ({ balance: selectBalance(state) });
|
||||
|
||||
export default connect(select)(WalletSpendableBalanceHelp);
|
||||
|
|
|
@ -1,33 +1,21 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
inline?: boolean,
|
||||
};
|
||||
type Props = { balance: number, inline?: boolean };
|
||||
|
||||
function WalletSpendableBalanceHelp(props: Props) {
|
||||
const { balance, inline } = props;
|
||||
|
||||
if (!balance) {
|
||||
return null;
|
||||
}
|
||||
const getMessage = (text: string) => (
|
||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>{text}</I18nMessage>
|
||||
);
|
||||
|
||||
return inline ? (
|
||||
<span className="help--spendable">
|
||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
||||
%balance% available.
|
||||
</I18nMessage>
|
||||
</span>
|
||||
return !balance ? null : inline ? (
|
||||
<span className="help--spendable">{getMessage(__('%balance% available.'))}</span>
|
||||
) : (
|
||||
<div className="help">
|
||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
||||
Your immediately spendable balance is %balance%.
|
||||
</I18nMessage>
|
||||
</div>
|
||||
<div className="help">{getMessage(__('Your immediately spendable balance is %balance%.'))}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'redux/selectors/wallet';
|
||||
import WalletTipAmountSelector from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => ({
|
||||
balance: selectBalance(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
// claim: makeSelectClaimForUri(props.uri)(state),
|
||||
// claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||
});
|
||||
const select = (state) => ({ balance: selectBalance(state) });
|
||||
|
||||
export default connect(select)(WalletTipAmountSelector);
|
||||
|
|
|
@ -1,175 +1,205 @@
|
|||
// @flow
|
||||
import 'scss/component/_wallet-tip-selector.scss';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
amount: number,
|
||||
onChange: (number) => void,
|
||||
isAuthenticated: boolean,
|
||||
claim: StreamClaim,
|
||||
uri: string,
|
||||
onTipErrorChange: (string) => void,
|
||||
activeTab: string,
|
||||
shouldDisableReviewButton: (boolean) => void,
|
||||
amount: number,
|
||||
balance: number,
|
||||
claim: StreamClaim,
|
||||
convertedAmount?: number,
|
||||
customTipAmount?: number,
|
||||
fiatConversion?: boolean,
|
||||
tipError: boolean,
|
||||
tipError: string,
|
||||
uri: string,
|
||||
onChange: (number) => void,
|
||||
setConvertedAmount?: (number) => void,
|
||||
setDisableSubmitButton: (boolean) => void,
|
||||
setTipError: (any) => void,
|
||||
};
|
||||
|
||||
function WalletTipAmountSelector(props: Props) {
|
||||
const { balance, amount, onChange, activeTab, claim, onTipErrorChange, shouldDisableReviewButton } = props;
|
||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const {
|
||||
activeTab,
|
||||
amount,
|
||||
balance,
|
||||
claim,
|
||||
convertedAmount,
|
||||
customTipAmount,
|
||||
fiatConversion,
|
||||
tipError,
|
||||
onChange,
|
||||
setConvertedAmount,
|
||||
setDisableSubmitButton,
|
||||
setTipError,
|
||||
} = props;
|
||||
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
||||
const isMobile = useIsMobile();
|
||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', true);
|
||||
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
||||
const [exchangeRate, setExchangeRate] = React.useState();
|
||||
|
||||
const tipAmountsToDisplay =
|
||||
customTipAmount && fiatConversion && activeTab === TAB_FIAT ? [customTipAmount] : DEFAULT_TIP_AMOUNTS;
|
||||
|
||||
// if it's fiat but there's no card saved OR the creator can't receive fiat tips
|
||||
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
|
||||
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
|
||||
|
||||
// setup variables for tip API
|
||||
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
||||
/**
|
||||
* whether tip amount selection/review functionality should be disabled
|
||||
* @param [amount] LBC amount (optional)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldDisableAmountSelector(amount) {
|
||||
function shouldDisableAmountSelector(amount: number) {
|
||||
// if it's LBC but the balance isn't enough, or fiat conditions met
|
||||
// $FlowFixMe
|
||||
return (amount > balance && activeTab !== TAB_FIAT) || shouldDisableFiatSelectors;
|
||||
return (
|
||||
((amount > balance || balance === 0) && activeTab !== TAB_FIAT) ||
|
||||
shouldDisableFiatSelectors ||
|
||||
(customTipAmount && fiatConversion && activeTab !== TAB_FIAT && exchangeRate
|
||||
? amount * exchangeRate < customTipAmount
|
||||
: customTipAmount && amount < customTipAmount)
|
||||
);
|
||||
}
|
||||
|
||||
shouldDisableReviewButton(shouldDisableFiatSelectors);
|
||||
|
||||
// setup variables for tip API
|
||||
let channelClaimId, tipChannelName;
|
||||
// if there is a signing channel it's on a file
|
||||
if (claim.signing_channel) {
|
||||
channelClaimId = claim.signing_channel.claim_id;
|
||||
tipChannelName = claim.signing_channel.name;
|
||||
|
||||
// otherwise it's on the channel page
|
||||
} else {
|
||||
channelClaimId = claim.claim_id;
|
||||
tipChannelName = claim.name;
|
||||
}
|
||||
|
||||
// check if creator has a payment method saved
|
||||
React.useEffect(() => {
|
||||
if (stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerStatusResponse) => {
|
||||
const defaultPaymentMethodId =
|
||||
customerStatusResponse.Customer &&
|
||||
customerStatusResponse.Customer.invoice_settings &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||
|
||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
//
|
||||
React.useEffect(() => {
|
||||
if (stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
{
|
||||
channel_claim_id: channelClaimId,
|
||||
channel_name: tipChannelName,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((accountCheckResponse) => {
|
||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||
setCanReceiveFiatTip(true);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// setHasSavedCard(false);
|
||||
// setCanReceiveFiatTip(true);
|
||||
|
||||
let regexp,
|
||||
tipError = '';
|
||||
|
||||
if (amount === 0) {
|
||||
tipError = __('Amount must be a positive number');
|
||||
} else if (!amount || typeof amount !== 'number') {
|
||||
tipError = __('Amount must be a number');
|
||||
}
|
||||
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
else if (activeTab !== TAB_FIAT) {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
tipError = __('Amount must have no more than 8 decimal places');
|
||||
} else if (amount === balance) {
|
||||
tipError = __('Please decrease the amount to account for transaction fees');
|
||||
} else if (amount > balance) {
|
||||
tipError = __('Not enough Credits');
|
||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||
tipError = __('Amount must be higher');
|
||||
}
|
||||
// if tip fiat tab
|
||||
} else {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
tipError = __('Amount must have no more than 2 decimal places');
|
||||
} else if (amount < 1) {
|
||||
tipError = __('Amount must be at least one dollar');
|
||||
} else if (amount > 1000) {
|
||||
tipError = __('Amount cannot be over 1000 dollars');
|
||||
}
|
||||
}
|
||||
|
||||
setTipError(tipError);
|
||||
onTipErrorChange(tipError);
|
||||
}, [amount, balance, setTipError, activeTab]);
|
||||
|
||||
// parse number as float and sets it in the parent component
|
||||
function handleCustomPriceChange(amount: number) {
|
||||
const tipAmount = parseFloat(amount);
|
||||
|
||||
onChange(tipAmount);
|
||||
const tipAmountValue = parseFloat(amount);
|
||||
onChange(tipAmountValue);
|
||||
if (fiatConversion && exchangeRate && setConvertedAmount && convertedAmount !== tipAmountValue * exchangeRate) {
|
||||
setConvertedAmount(tipAmountValue * exchangeRate);
|
||||
}
|
||||
}
|
||||
|
||||
function convertToTwoDecimals(number: number) {
|
||||
return (Math.round(number * 100) / 100).toFixed(2);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!exchangeRate) {
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD));
|
||||
} else if ((!convertedAmount || convertedAmount !== amount * exchangeRate) && setConvertedAmount) {
|
||||
setConvertedAmount(amount * exchangeRate);
|
||||
}
|
||||
}, [amount, convertedAmount, exchangeRate, setConvertedAmount]);
|
||||
|
||||
// check if creator has a payment method saved
|
||||
// REMOVE
|
||||
React.useEffect(() => {
|
||||
if (!stripeEnvironment) return;
|
||||
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerStatusResponse) => {
|
||||
const defaultPaymentMethodId =
|
||||
customerStatusResponse.Customer &&
|
||||
customerStatusResponse.Customer.invoice_settings &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||
|
||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}, [setHasSavedCard]);
|
||||
|
||||
// REMOVE
|
||||
React.useEffect(() => {
|
||||
if (!stripeEnvironment) return;
|
||||
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
{
|
||||
channel_claim_id: channelClaimId,
|
||||
channel_name: tipChannelName,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((accountCheckResponse) => {
|
||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||
setCanReceiveFiatTip(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, channelClaimId, tipChannelName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let regexp;
|
||||
|
||||
if (amount === 0) {
|
||||
setTipError(__('Amount cannot be zero.'));
|
||||
} else if (!amount || typeof amount !== 'number') {
|
||||
setTipError(__('Amount must be a number.'));
|
||||
} else {
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
if (activeTab !== TAB_FIAT) {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
setTipError(__('Amount must have no more than 8 decimal places'));
|
||||
} else if (amount === balance) {
|
||||
setTipError(__('Please decrease the amount to account for transaction fees'));
|
||||
} else if (amount > balance || balance === 0) {
|
||||
setTipError(__('Not enough Credits'));
|
||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||
setTipError(__('Amount must be higher'));
|
||||
} else {
|
||||
setTipError(false);
|
||||
}
|
||||
// if tip fiat tab REMOVE
|
||||
} else {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||
const validTipInput = regexp.test(String(amount));
|
||||
|
||||
if (!validTipInput) {
|
||||
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||
} else if (amount < 1) {
|
||||
setTipError(__('Amount must be at least one dollar'));
|
||||
} else if (amount > 1000) {
|
||||
setTipError(__('Amount cannot be over 1000 dollars'));
|
||||
} else {
|
||||
setTipError(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [activeTab, amount, balance, setTipError]);
|
||||
|
||||
const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
||||
{tipAmountsToDisplay.map((defaultAmount) => (
|
||||
<Button
|
||||
key={defaultAmount}
|
||||
disabled={shouldDisableAmountSelector(defaultAmount)}
|
||||
|
@ -186,9 +216,10 @@ function WalletTipAmountSelector(props: Props) {
|
|||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
button="alt"
|
||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
||||
disabled={shouldDisableFiatSelectors}
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': useCustomTip,
|
||||
})}
|
||||
|
@ -207,60 +238,26 @@ function WalletTipAmountSelector(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||
{__('Tip Creators')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Send a tip directly from your attached card</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{customTipAmount &&
|
||||
fiatConversion &&
|
||||
activeTab !== TAB_FIAT &&
|
||||
getHelpMessage(
|
||||
__(
|
||||
`This support is priced in $USD. ${
|
||||
convertedAmount
|
||||
? __(`The current exchange rate for the submitted amount is: $${convertToTwoDecimals(convertedAmount)}`)
|
||||
: ''
|
||||
}`
|
||||
)
|
||||
)}
|
||||
|
||||
{/* custom number input form */}
|
||||
{useCustomTip && (
|
||||
<div className="comment__tip-input">
|
||||
<div className="walletTipSelector__input">
|
||||
<FormField
|
||||
autoFocus
|
||||
autoFocus={!isMobile}
|
||||
name="tip-input"
|
||||
disabled={shouldDisableAmountSelector()}
|
||||
label={
|
||||
activeTab === TAB_LBC ? (
|
||||
<React.Fragment>
|
||||
{__('Custom support amount')}{' '}
|
||||
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
|
||||
(%lbc_balance% available)
|
||||
</I18nMessage>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
// <>
|
||||
// <div className="">
|
||||
// <span className="help--spendable">Send a tip directly from your attached card</span>
|
||||
// </div>
|
||||
// </>
|
||||
}
|
||||
disabled={!customTipAmount && shouldDisableAmountSelector(0)}
|
||||
error={tipError}
|
||||
min="0"
|
||||
step="any"
|
||||
|
@ -274,35 +271,17 @@ function WalletTipAmountSelector(props: Props) {
|
|||
|
||||
{/* lbc tab */}
|
||||
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
||||
{/* fiat button but no card saved */}
|
||||
{!useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||
{__('Tip Creators')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* has card saved but cant creator cant receive tips */}
|
||||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Send a tip directly from your attached card</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{activeTab === TAB_FIAT &&
|
||||
(!hasCardSaved
|
||||
? getHelpMessage(
|
||||
<>
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||
{__(' To Tip Creators')}
|
||||
</>
|
||||
)
|
||||
: !canReceiveFiatTip
|
||||
? getHelpMessage(__('Only creators that verify cash accounts can receive tips'))
|
||||
: getHelpMessage(__('Send a tip directly from your attached card')))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
107
ui/constants/stickers.js
Normal file
107
ui/constants/stickers.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
// @flow
|
||||
|
||||
const buildCDNUrl = (path: string) => `https://static.odycdn.com/stickers/${path}`;
|
||||
|
||||
const buildSticker = (name: string, path: string, price?: number) => ({
|
||||
name: __(`:${name}:`),
|
||||
url: buildCDNUrl(path),
|
||||
price: price,
|
||||
});
|
||||
|
||||
const CAT_BORDER = 'CAT/PNG/cat_with_border.png';
|
||||
const FAIL_BORDER = 'FAIL/PNG/fail_with_border.png';
|
||||
const HYPE_BORDER = 'HYPE/PNG/hype_with_border.png';
|
||||
const PANTS_1_WITH_FRAME = 'PANTS/PNG/PANTS_1_with_frame.png';
|
||||
const PANTS_2_WITH_FRAME = 'PANTS/PNG/PANTS_2_with_frame.png';
|
||||
const PISS = 'PISS/PNG/piss_with_frame.png';
|
||||
const PREGNANT_MAN_ASIA_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_asia.png';
|
||||
const PREGNANT_MAN_BLACK_HAIR_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_black%20hair.png';
|
||||
const PREGNANT_MAN_BLACK_SKIN_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_black%20skin.png';
|
||||
const PREGNANT_MAN_BLONDE_WHITE_BORDER = 'pregnant%20man/png/Pregnant%20man_white%20border_blondie.png';
|
||||
const PREGNANT_MAN_RED_HAIR_WHITE_BORDER =
|
||||
'pregnant%20man/png/Pregnant%20man_white%20border_red%20hair%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.png';
|
||||
const PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT_WHITE_BORDER =
|
||||
'pregnant%20woman/png/Pregnant%20woman_white_border_black%20hair%20green%20shirt.png';
|
||||
const PREGNANT_WOMAN_BLACK_HAIR_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_black%20hair.png';
|
||||
const PREGNANT_WOMAN_BLACK_SKIN_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_black%20woman.png';
|
||||
const PREGNANT_WOMAN_BLONDE_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_blondie.png';
|
||||
const PREGNANT_WOMAN_BROWN_HAIR_WHITE_BORDER = 'pregnant%20woman/png/Pregnant%20woman_white_border_brown%20hair.png';
|
||||
const PREGNANT_WOMAN_RED_HAIR_WHITE_BORDER =
|
||||
'pregnant%20woman/png/Pregnant%20woman_white_border_red%20hair%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.png';
|
||||
const ROCKET_SPACEMAN_WITH_BORDER = 'ROCKET%20SPACEMAN/PNG/rocket-spaceman_with-border.png';
|
||||
const SALTY = 'SALTY/PNG/salty.png';
|
||||
const SICK_2_WITH_BORDER = 'SICK/PNG/sick2_with_border.png';
|
||||
const SICK_1_WITH_BORDERDARK_WITH_FRAME = 'SICK/PNG/with%20borderdark%20with%20frame.png';
|
||||
const SLIME_WITH_FRAME = 'SLIME/PNG/slime_with_frame.png';
|
||||
const SPHAGETTI_BATH_WITH_FRAME = 'SPHAGETTI%20BATH/PNG/sphagetti%20bath_with_frame.png';
|
||||
const THUG_LIFE_WITH_BORDER = 'THUG%20LIFE/PNG/thug_life_with_border_clean.png';
|
||||
const WHUUT_WITH_FRAME = 'WHUUT/PNG/whuut_with-frame.png';
|
||||
const COMET_TIP = 'TIPS/png/$%20comet%20tip%20with%20border.png';
|
||||
const BIG_LBC_TIP = 'TIPS/png/big_LBC_TIPV.png';
|
||||
const BIG_TIP = 'TIPS/png/with%20borderbig$tip.png';
|
||||
const BITE_TIP = 'TIPS/png/bite_$tip_with%20border.png';
|
||||
const BITE_TIP_CLOSEUP = 'TIPS/png/bite_$tip_closeup.png';
|
||||
const FORTUNE_CHEST_LBC = 'TIPS/png/with%20borderfortunechest_LBC_tip.png';
|
||||
const FORTUNE_CHEST = 'TIPS/png/with%20borderfortunechest$_tip.png';
|
||||
const LARGE_LBC_TIP = 'TIPS/png/with%20borderlarge_LBC_tip%20.png';
|
||||
const LARGE_TIP = 'TIPS/png/with%20borderlarge$tip.png';
|
||||
const BITE_LBC_CLOSEUP = 'TIPS/png/LBC%20bite.png';
|
||||
const LBC_COMET_TIP = 'TIPS/png/LBC%20comet%20tip%20with%20border.png';
|
||||
const MEDIUM_LBC_TIP = 'TIPS/png/with%20bordermedium_LBC_tip%20%20%20%20%20%20%20%20%20%20.png';
|
||||
const MEDIUM_TIP = 'TIPS/png/with%20bordermedium$_%20tip.png';
|
||||
const SILVER_ODYSEE_COIN = 'TIPS/png/with%20bordersilver_odysee_coinv.png';
|
||||
const SMALL_LBC_TIP = 'TIPS/png/with%20bordersmall_LBC_tip%20.png';
|
||||
const SMALL_TIP = 'TIPS/png/with%20bordersmall$_tip.png';
|
||||
const TIP_HAND_FLIP = 'TIPS/png/tip_hand_flip_$%20_with_border.png';
|
||||
const TIP_HAND_FLIP_COIN = 'TIPS/png/tip_hand_flip_coin_with_border.png';
|
||||
const TIP_HAND_FLIP_LBC = 'TIPS/png/tip_hand_flip_lbc_with_border.png';
|
||||
|
||||
export const FREE_GLOBAL_STICKERS = [
|
||||
buildSticker('CAT', CAT_BORDER),
|
||||
buildSticker('FAIL', FAIL_BORDER),
|
||||
buildSticker('HYPE', HYPE_BORDER),
|
||||
buildSticker('PANTS_1', PANTS_1_WITH_FRAME),
|
||||
buildSticker('PANTS_2', PANTS_2_WITH_FRAME),
|
||||
buildSticker('PISS', PISS),
|
||||
buildSticker('PREGNANT_MAN_ASIA', PREGNANT_MAN_ASIA_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_BLACK_HAIR', PREGNANT_MAN_BLACK_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_BLACK_SKIN', PREGNANT_MAN_BLACK_SKIN_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_BLONDE', PREGNANT_MAN_BLONDE_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_MAN_RED_HAIR', PREGNANT_MAN_RED_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT', PREGNANT_WOMAN_BLACK_HAIR_GREEN_SHIRT_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLACK_HAIR', PREGNANT_WOMAN_BLACK_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLACK_SKIN', PREGNANT_WOMAN_BLACK_SKIN_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BLONDE', PREGNANT_WOMAN_BLONDE_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_BROWN_HAIR', PREGNANT_WOMAN_BROWN_HAIR_WHITE_BORDER),
|
||||
buildSticker('PREGNANT_WOMAN_RED_HAIR', PREGNANT_WOMAN_RED_HAIR_WHITE_BORDER),
|
||||
buildSticker('ROCKET_SPACEMAN', ROCKET_SPACEMAN_WITH_BORDER),
|
||||
buildSticker('SALTY', SALTY),
|
||||
buildSticker('SICK_FLAME', SICK_2_WITH_BORDER),
|
||||
buildSticker('SICK_SKULL', SICK_1_WITH_BORDERDARK_WITH_FRAME),
|
||||
buildSticker('SLIME', SLIME_WITH_FRAME),
|
||||
buildSticker('SPHAGETTI_BATH', SPHAGETTI_BATH_WITH_FRAME),
|
||||
buildSticker('THUG_LIFE', THUG_LIFE_WITH_BORDER),
|
||||
buildSticker('WHUUT', WHUUT_WITH_FRAME),
|
||||
];
|
||||
|
||||
export const PAID_GLOBAL_STICKERS = [
|
||||
buildSticker('TIP_HAND_FLIP', TIP_HAND_FLIP, 1),
|
||||
buildSticker('TIP_HAND_FLIP_COIN', TIP_HAND_FLIP_COIN, 1),
|
||||
buildSticker('TIP_HAND_FLIP_LBC', TIP_HAND_FLIP_LBC, 1),
|
||||
buildSticker('COMET_TIP', COMET_TIP, 25),
|
||||
buildSticker('LBC_COMET_TIP', LBC_COMET_TIP, 25),
|
||||
buildSticker('SMALL_TIP', SMALL_TIP, 25),
|
||||
buildSticker('SILVER_ODYSEE_COIN', SILVER_ODYSEE_COIN, 25),
|
||||
buildSticker('SMALL_LBC_TIP', SMALL_LBC_TIP, 25),
|
||||
buildSticker('BITE_TIP', BITE_TIP, 50),
|
||||
buildSticker('BITE_TIP_CLOSEUP', BITE_TIP_CLOSEUP, 50),
|
||||
buildSticker('BITE_LBC_CLOSEUP', BITE_LBC_CLOSEUP, 50),
|
||||
buildSticker('MEDIUM_TIP', MEDIUM_TIP, 50),
|
||||
buildSticker('MEDIUM_LBC_TIP', MEDIUM_LBC_TIP, 50),
|
||||
buildSticker('LARGE_TIP', LARGE_TIP, 100),
|
||||
buildSticker('LARGE_LBC_TIP', LARGE_LBC_TIP, 100),
|
||||
buildSticker('BIG_TIP', BIG_TIP, 150),
|
||||
buildSticker('BIG_LBC_TIP', BIG_LBC_TIP, 150),
|
||||
buildSticker('FORTUNE_CHEST', FORTUNE_CHEST, 200),
|
||||
buildSticker('FORTUNE_CHEST_LBC', FORTUNE_CHEST_LBC, 200),
|
||||
];
|
|
@ -552,6 +552,7 @@ export function doCommentReact(commentId: string, type: string) {
|
|||
* @param claim_id - File claim id
|
||||
* @param parent_id - What is this?
|
||||
* @param uri
|
||||
* @param sticker
|
||||
* @param {string} [txid] Optional transaction id
|
||||
* @param {string} [payment_intent_id] Optional transaction id
|
||||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||
|
@ -561,10 +562,11 @@ export function doCommentCreate(
|
|||
comment: string = '',
|
||||
claim_id: string = '',
|
||||
parent_id?: string,
|
||||
uri: string,
|
||||
uri: string, // REMOVE ed livestream
|
||||
txid?: string,
|
||||
payment_intent_id?: string,
|
||||
environment?: string
|
||||
environment?: string,
|
||||
sticker: boolean
|
||||
) {
|
||||
return async (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
|
@ -577,9 +579,7 @@ export function doCommentCreate(
|
|||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_CREATE_STARTED,
|
||||
});
|
||||
dispatch({ type: ACTIONS.COMMENT_CREATE_STARTED });
|
||||
|
||||
let signatureData;
|
||||
if (activeChannelClaim) {
|
||||
|
@ -592,12 +592,8 @@ export function doCommentCreate(
|
|||
}
|
||||
|
||||
// send a notification
|
||||
if (parent_id) {
|
||||
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
||||
if (notification && !notification.is_seen) {
|
||||
dispatch(doSeeNotifications([notification.id]));
|
||||
}
|
||||
}
|
||||
const notification = parent_id && makeSelectNotificationForCommentId(parent_id)(state);
|
||||
if (notification && !notification.is_seen) dispatch(doSeeNotifications([notification.id]));
|
||||
|
||||
if (!signatureData) {
|
||||
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||
|
@ -613,6 +609,7 @@ export function doCommentCreate(
|
|||
parent_id: parent_id,
|
||||
signature: signatureData.signature,
|
||||
signing_ts: signatureData.signing_ts,
|
||||
sticker: sticker,
|
||||
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
|
||||
...(payment_intent_id ? { payment_intent_id } : {}), // add payment_intent_id if it exists
|
||||
...(environment ? { environment } : {}), // add environment for stripe if it exists
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import Lbry from 'lbry';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import {
|
||||
selectBalance,
|
||||
|
@ -12,7 +13,6 @@ import {
|
|||
import { creditsToString } from 'util/format-credits';
|
||||
import { selectMyClaimsRaw, selectClaimsById } from 'redux/selectors/claims';
|
||||
import { doFetchChannelListMine, doFetchClaimListMine, doClaimSearch } from 'redux/actions/claims';
|
||||
|
||||
const FIFTEEN_SECONDS = 15000;
|
||||
let walletBalancePromise = null;
|
||||
|
||||
|
@ -700,3 +700,47 @@ export const doCheckPendingTxs = () => (dispatch, getState) => {
|
|||
checkTxList();
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
// don't need hthis
|
||||
export const doSendCashTip = (tipParams, anonymous, userParams, claimId, stripeEnvironment, successCallback) => (
|
||||
dispatch
|
||||
) => {
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'tip',
|
||||
{
|
||||
// round to fix issues with floating point numbers
|
||||
amount: Math.round(100 * tipParams.tipAmount), // convert from dollars to cents
|
||||
creator_channel_name: tipParams.tipChannelName, // creator_channel_name
|
||||
creator_channel_claim_id: tipParams.channelClaimId,
|
||||
tipper_channel_name: anonymous ? '' : userParams.activeChannelName,
|
||||
tipper_channel_claim_id: anonymous ? '' : userParams.activeChannelId,
|
||||
currency: 'USD',
|
||||
anonymous: anonymous,
|
||||
source_claim_id: claimId,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((customerTipResponse) => {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __("You sent $%tipAmount% as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||
tipAmount: tipParams.tipAmount,
|
||||
tipChannelName: tipParams.tipChannelName,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
if (successCallback) successCallback(customerTipResponse);
|
||||
})
|
||||
.catch((error) => {
|
||||
// show error message from Stripe if one exists (being passed from backend by Beamer's API currently)
|
||||
dispatch(
|
||||
doToast({
|
||||
message: error.message || __('Sorry, there was an error in processing your payment!'),
|
||||
isError: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
$thumbnailWidth: 1.5rem;
|
||||
$thumbnailWidthSmall: 1rem;
|
||||
|
||||
.comment__create {
|
||||
.content_comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
|
@ -19,16 +23,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create--reply {
|
||||
.commentCreate--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content_comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment__create--nested-reply {
|
||||
.commentCreate--nestedReply {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
|
@ -37,11 +37,11 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create--bottom {
|
||||
.commentCreate--bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-new__label-wrapper {
|
||||
.commentCreate__labelWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
@ -49,6 +49,11 @@ $thumbnailWidthSmall: 1rem;
|
|||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.commentCreate__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
|
@ -56,27 +61,50 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment-new__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comment__sc-preview {
|
||||
.commentCreate__supportCommentPreview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
|
||||
.commentCreate__supportCommentPreviewAmount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__sc-preview-amount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
.comment--min-amount-notice {
|
||||
.commentCreate__minAmountNotice {
|
||||
.icon {
|
||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreview {
|
||||
@extend .commentCreate;
|
||||
display: flex;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
|
||||
.commentCreate__stickerPreviewInfo {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreviewImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
.filePrice {
|
||||
height: 1.5rem;
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -415,10 +415,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__tip-input {
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.comment--blocked {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
@ -479,3 +475,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;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create,
|
||||
.commentCreate,
|
||||
.comment__content {
|
||||
margin: var(--spacing-m);
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -7,155 +7,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-price {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-purchased-text);
|
||||
|
||||
.credit-amount,
|
||||
.icon--Key {
|
||||
position: relative;
|
||||
margin-left: var(--spacing-m);
|
||||
white-space: nowrap;
|
||||
color: var(--color-purchased-text);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
left: 0;
|
||||
width: 250%;
|
||||
height: 160%;
|
||||
transform: skew(15deg);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-purchased-alt);
|
||||
border: 2px solid var(--color-purchased);
|
||||
}
|
||||
}
|
||||
|
||||
.file-price__key {
|
||||
@extend .file-price;
|
||||
color: var(--color-gray-5);
|
||||
|
||||
.icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-purchased);
|
||||
height: 180%;
|
||||
}
|
||||
}
|
||||
|
||||
.file-price--filepage {
|
||||
font-size: var(--font-body);
|
||||
top: calc(var(--spacing-xxs) * -1);
|
||||
margin-left: var(--spacing-m);
|
||||
|
||||
.credit-amount {
|
||||
margin: 0 var(--spacing-m);
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
height: 250%;
|
||||
left: calc(var(--spacing-m) * -1);
|
||||
border-radius: 0;
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-width: 5px;
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
|
||||
&::before {
|
||||
height: 140%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-price__key--filepage {
|
||||
@extend .file-price--filepage;
|
||||
top: 0;
|
||||
|
||||
&::before {
|
||||
height: 300%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 var(--spacing-m);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
&::before {
|
||||
top: calc(-1 * var(--spacing-s));
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
top: calc(-1 * var(--spacing-s));
|
||||
margin: 0 var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-price--modal {
|
||||
border: 5px solid var(--color-purchased);
|
||||
|
||||
.credit-amount {
|
||||
margin: 0 var(--spacing-m);
|
||||
margin-left: var(--spacing-l);
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
}
|
||||
|
||||
.file-price--modal {
|
||||
font-size: var(--font-body);
|
||||
height: 4rem;
|
||||
background-color: var(--color-purchased-alt);
|
||||
border-radius: var(--border-radius);
|
||||
transform: skew(15deg);
|
||||
|
||||
.icon,
|
||||
.credit-amount {
|
||||
transform: skew(-15deg);
|
||||
}
|
||||
|
||||
.credit-amount {
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-price__key--modal {
|
||||
@extend .file-price--modal;
|
||||
top: var(--spacing-m);
|
||||
|
||||
.icon {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
left: calc(var(--spacing-xl) * 1.5);
|
||||
animation: moveKey 2.5s 1 ease-out;
|
||||
overflow: visible;
|
||||
stroke: var(--color-black);
|
||||
|
||||
g {
|
||||
animation: turnKey 2.5s 1 ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
transform: skew(15deg);
|
||||
animation: expand 2.5s 1 ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.purchase-stuff {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
82
ui/scss/component/_sticker-selector.scss
Normal file
82
ui/scss/component/_sticker-selector.scss
Normal file
|
@ -0,0 +1,82 @@
|
|||
@import '../init/vars';
|
||||
|
||||
.stickerSelector {
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
.stickerSelector__header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: var(--spacing-s);
|
||||
margin-bottom: 0;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xxs);
|
||||
|
||||
.stickerSelector__headerTitle {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__wrapper {
|
||||
height: unset;
|
||||
border-left: 1px solid var(--color-border);
|
||||
|
||||
.navigation-links {
|
||||
li {
|
||||
.button {
|
||||
padding: unset;
|
||||
|
||||
.button__content {
|
||||
justify-content: unset;
|
||||
flex-direction: unset;
|
||||
width: unset;
|
||||
|
||||
.button__label {
|
||||
font-size: var(--font-small);
|
||||
margin: 0 var(--spacing-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stickerSelector__list {
|
||||
display: flex;
|
||||
|
||||
.stickerSelector__listBody {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
max-height: 25vh;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.button--file-action {
|
||||
width: 5rem;
|
||||
height: 5.3rem;
|
||||
overflow: hidden;
|
||||
margin: unset;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.button__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.super-chat--light {
|
||||
position: absolute;
|
||||
display: inline;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-xsmall) {
|
||||
width: 4rem;
|
||||
height: 4.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
ui/scss/component/_wallet-tip-selector.scss
Normal file
3
ui/scss/component/_wallet-tip-selector.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.walletTipSelector__input {
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
// @flow
|
||||
import * as REACTION_TYPES from 'constants/reactions';
|
||||
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
|
||||
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||
import * as REACTION_TYPES from 'constants/reactions';
|
||||
|
||||
const ALL_VALID_STICKERS = [...FREE_GLOBAL_STICKERS, ...PAID_GLOBAL_STICKERS];
|
||||
const stickerRegex = /(<stkr>:[A-Z0-9_]+:<stkr>)/;
|
||||
|
||||
// Mostly taken from Reddit's sorting functions
|
||||
// https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx
|
||||
|
@ -88,3 +92,19 @@ export function sortComments(sortProps: SortProps): Array<Comment> {
|
|||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export const buildValidSticker = (sticker: string) => `<stkr>${sticker}<stkr>`;
|
||||
|
||||
export function parseSticker(comment: string) {
|
||||
const matchSticker = comment.match(stickerRegex);
|
||||
const stickerValue = matchSticker && matchSticker[0];
|
||||
const commentIsSticker = stickerValue && stickerValue.length === comment.length;
|
||||
|
||||
return (
|
||||
commentIsSticker &&
|
||||
ALL_VALID_STICKERS.find((sticker) => {
|
||||
// $FlowFixMe
|
||||
return sticker.name === stickerValue.replaceAll('<stkr>', '');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue