Bringing in emotes, stickers, and refactors from ody #7435
69 changed files with 4280 additions and 2324 deletions
3
flow-typed/search.js
vendored
3
flow-typed/search.js
vendored
|
@ -29,8 +29,10 @@ declare type SearchOptions = {
|
|||
declare type SearchState = {
|
||||
options: SearchOptions,
|
||||
resultsByQuery: {},
|
||||
results: Array<string>,
|
||||
hasReachedMaxResultsLength: {},
|
||||
searching: boolean,
|
||||
mentionQuery: string,
|
||||
};
|
||||
|
||||
declare type SearchSuccess = {
|
||||
|
@ -41,6 +43,7 @@ declare type SearchSuccess = {
|
|||
size: number,
|
||||
uris: Array<string>,
|
||||
recsys: string,
|
||||
query: string,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
"postinstall:warning": "echo '\n\nWARNING\n\nNot all node modules were installed because NODE_ENV is set to \"production\".\nThis should only be set after installing dependencies with \"yarn\". The app will not work.\n\n'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.6.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@mui/material": "^5.2.1",
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@ungap/from-entries": "^0.2.1",
|
||||
"auto-launch": "^5.0.5",
|
||||
|
|
|
@ -2235,9 +2235,17 @@
|
|||
"Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.": "Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.",
|
||||
"Content: Limit (GB)": "Content: Limit (GB)",
|
||||
"Network: Allow (GB)": "Network: Allow (GB)",
|
||||
"Failed to view lbry://@Destiny#6/destiny-crashes-conservative-panel-w#a, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@Destiny#6/destiny-crashes-conservative-panel-w#a, please try again. If this problem persists, visit https://lbry.com/faq/support for support.",
|
||||
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
||||
"Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.",
|
||||
"Admin": "Admin",
|
||||
"Stickers": "Stickers",
|
||||
"Different Sticker": "Different Sticker",
|
||||
"LBC": "LBC",
|
||||
"Add a Card": "Add a Card",
|
||||
" To Tip Creators": " To Tip Creators",
|
||||
"Nothing found": "Nothing found",
|
||||
"From Comments": "From Comments",
|
||||
"This support is priced in $USD.": "This support is priced in $USD.",
|
||||
"The current exchange rate for the submitted LBC amount is ~ $%exchange_amount%.": "The current exchange rate for the submitted LBC amount is ~ $%exchange_amount%.",
|
||||
"Amount of $%input_amount% LBC in USB is lower than price of $%price_amount%": "Amount of $%input_amount% LBC in USB is lower than price of $%price_amount%",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -68,8 +68,8 @@ type Props = {
|
|||
syncLoop: (?boolean) => void,
|
||||
currentModal: any,
|
||||
syncFatalError: boolean,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
myChannelUrls: ?Array<string>,
|
||||
activeChannelId: ?string,
|
||||
myChannelClaimIds: ?Array<string>,
|
||||
subscriptions: Array<Subscription>,
|
||||
setActiveChannelIfNotSet: () => void,
|
||||
setIncognito: (boolean) => void,
|
||||
|
@ -103,8 +103,8 @@ function App(props: Props) {
|
|||
syncLoop,
|
||||
currentModal,
|
||||
syncFatalError,
|
||||
myChannelUrls,
|
||||
activeChannelClaim,
|
||||
myChannelClaimIds,
|
||||
activeChannelId,
|
||||
setActiveChannelIfNotSet,
|
||||
setIncognito,
|
||||
fetchModBlockedList,
|
||||
|
@ -125,6 +125,7 @@ function App(props: Props) {
|
|||
const { pathname, search } = props.location;
|
||||
const [upgradeNagClosed, setUpgradeNagClosed] = useState(false);
|
||||
const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
|
||||
// const [retryingSync, setRetryingSync] = useState(false);
|
||||
const [sidebarOpen] = usePersistedState('sidebar', true);
|
||||
const showUpgradeButton =
|
||||
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
|
||||
|
@ -135,10 +136,10 @@ function App(props: Props) {
|
|||
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
|
||||
const userId = user && user.id;
|
||||
const useCustomScrollbar = !IS_MAC;
|
||||
const hasMyChannels = myChannelUrls && myChannelUrls.length > 0;
|
||||
const hasNoChannels = myChannelUrls && myChannelUrls.length === 0;
|
||||
const hasMyChannels = myChannelClaimIds && myChannelClaimIds.length > 0;
|
||||
const hasNoChannels = myChannelClaimIds && myChannelClaimIds.length === 0;
|
||||
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
||||
const hasActiveChannelClaim = activeChannelClaim !== undefined;
|
||||
const hasActiveChannelClaim = activeChannelId !== undefined;
|
||||
const isPersonalized = hasVerifiedEmail;
|
||||
const renderFiledrop = isAuthenticated;
|
||||
|
||||
|
@ -152,7 +153,7 @@ function App(props: Props) {
|
|||
if (!uploadCount) return;
|
||||
const handleBeforeUnload = (event) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = 'magic'; // without setting this to something it doesn't work
|
||||
event.returnValue = __('There are pending uploads.'); // without setting this to something it doesn't work
|
||||
};
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
|
@ -272,7 +273,6 @@ function App(props: Props) {
|
|||
}
|
||||
}, [previousRewardApproved, isRewardApproved]);
|
||||
|
||||
// @if TARGET='app'
|
||||
useEffect(() => {
|
||||
if (updatePreferences && getWalletSyncPref && readyForPrefs) {
|
||||
getWalletSyncPref()
|
||||
|
@ -282,7 +282,6 @@ function App(props: Props) {
|
|||
});
|
||||
}
|
||||
}, [updatePreferences, getWalletSyncPref, setReadyForSync, readyForPrefs, hasVerifiedEmail]);
|
||||
// @endif
|
||||
|
||||
// ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too.
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
|
||||
import ChannelMentionSuggestion from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: selectClaimForUri(state, props.uri),
|
||||
isResolvingUri: selectIsUriResolving(state, props.uri),
|
||||
});
|
||||
|
||||
export default connect(select)(ChannelMentionSuggestion);
|
|
@ -1,32 +0,0 @@
|
|||
// @flow
|
||||
import { ComboboxOption } from '@reach/combobox';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
claim: ?Claim,
|
||||
uri?: string,
|
||||
isResolvingUri: boolean,
|
||||
};
|
||||
|
||||
export default function ChannelMentionSuggestion(props: Props) {
|
||||
const { claim, uri, isResolvingUri } = props;
|
||||
|
||||
return !claim ? null : (
|
||||
<ComboboxOption value={uri}>
|
||||
{isResolvingUri ? (
|
||||
<div className="channel-mention__suggestion">
|
||||
<div className="media__thumb media__thumb--resolving" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="channel-mention__suggestion">
|
||||
<ChannelThumbnail xsmall uri={uri} />
|
||||
<span className="channel-mention__suggestion-label">
|
||||
<div className="channel-mention__suggestion-title">{(claim.value && claim.value.title) || claim.name}</div>
|
||||
<div className="channel-mention__suggestion-name">{claim.name}</div>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</ComboboxOption>
|
||||
);
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { withRouter } from 'react-router';
|
||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import { selectTopLevelCommentsForUri } from 'redux/selectors/comments';
|
||||
import ChannelMentionSuggestions from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const subscriptionUris = selectSubscriptions(state).map(({ uri }) => uri);
|
||||
const topLevelComments = selectTopLevelCommentsForUri(state, props.uri);
|
||||
|
||||
const commentorUris = [];
|
||||
// Avoid repeated commentors
|
||||
topLevelComments.map(({ channel_url }) => !commentorUris.includes(channel_url) && commentorUris.push(channel_url));
|
||||
|
||||
const getUnresolved = (uris) =>
|
||||
uris.map((uri) => !makeSelectClaimForUri(uri)(state) && uri).filter((uri) => uri !== false);
|
||||
const getCanonical = (uris) =>
|
||||
uris
|
||||
.map((uri) => makeSelectClaimForUri(uri)(state) && makeSelectClaimForUri(uri)(state).canonical_url)
|
||||
.filter((uri) => Boolean(uri));
|
||||
|
||||
return {
|
||||
commentorUris,
|
||||
subscriptionUris,
|
||||
unresolvedCommentors: getUnresolved(commentorUris),
|
||||
unresolvedSubscriptions: getUnresolved(subscriptionUris),
|
||||
canonicalCreator: getCanonical([props.creatorUri])[0],
|
||||
canonicalCommentors: getCanonical(commentorUris),
|
||||
canonicalSubscriptions: getCanonical(subscriptionUris),
|
||||
showMature: selectShowMatureContent(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(connect(select, { doResolveUris })(ChannelMentionSuggestions));
|
|
@ -1,277 +0,0 @@
|
|||
// @flow
|
||||
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox';
|
||||
import { Form } from 'component/common/form';
|
||||
import { parseURI, regexInvalidURI } from 'util/lbryURI';
|
||||
import { SEARCH_OPTIONS } from 'constants/search';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import ChannelMentionSuggestion from 'component/channelMentionSuggestion';
|
||||
import ChannelMentionTopSuggestion from 'component/channelMentionTopSuggestion';
|
||||
import React from 'react';
|
||||
import Spinner from 'component/spinner';
|
||||
import type { ElementRef } from 'react';
|
||||
import useLighthouse from 'effects/use-lighthouse';
|
||||
|
||||
const INPUT_DEBOUNCE_MS = 1000;
|
||||
const LIGHTHOUSE_MIN_CHARACTERS = 3;
|
||||
|
||||
type Props = {
|
||||
inputRef: any,
|
||||
mentionTerm: string,
|
||||
noTopSuggestion?: boolean,
|
||||
showMature: boolean,
|
||||
creatorUri: string,
|
||||
commentorUris: Array<string>,
|
||||
subscriptionUris: Array<string>,
|
||||
unresolvedSubscriptions: Array<string>,
|
||||
canonicalCreator: string,
|
||||
canonicalCommentors: Array<string>,
|
||||
canonicalSubscriptions: Array<string>,
|
||||
doResolveUris: (Array<string>) => void,
|
||||
customSelectAction?: (string, number) => void,
|
||||
};
|
||||
|
||||
export default function ChannelMentionSuggestions(props: Props) {
|
||||
const {
|
||||
unresolvedSubscriptions,
|
||||
canonicalCreator,
|
||||
creatorUri,
|
||||
inputRef,
|
||||
showMature,
|
||||
noTopSuggestion,
|
||||
mentionTerm,
|
||||
doResolveUris,
|
||||
customSelectAction,
|
||||
} = props;
|
||||
const comboboxInputRef: ElementRef<any> = React.useRef();
|
||||
const comboboxListRef: ElementRef<any> = React.useRef();
|
||||
|
||||
const mainEl = document.querySelector('.channel-mention__suggestions');
|
||||
|
||||
const [debouncedTerm, setDebouncedTerm] = React.useState('');
|
||||
const [mostSupported, setMostSupported] = React.useState('');
|
||||
const [canonicalResults, setCanonicalResults] = React.useState([]);
|
||||
|
||||
const isRefFocused = (ref) => ref && ref.current === document.activeElement;
|
||||
|
||||
const subscriptionUris = props.subscriptionUris.filter((uri) => uri !== creatorUri);
|
||||
const canonicalSubscriptions = props.canonicalSubscriptions.filter((uri) => uri !== canonicalCreator);
|
||||
const commentorUris = props.commentorUris.filter((uri) => uri !== creatorUri && !subscriptionUris.includes(uri));
|
||||
const canonicalCommentors = props.canonicalCommentors.filter(
|
||||
(uri) => uri !== canonicalCreator && !canonicalSubscriptions.includes(uri)
|
||||
);
|
||||
|
||||
const termToMatch = mentionTerm && mentionTerm.replace('@', '').toLowerCase();
|
||||
const allShownUris = [creatorUri, ...subscriptionUris, ...commentorUris];
|
||||
const allShownCanonical = [canonicalCreator, ...canonicalSubscriptions, ...canonicalCommentors];
|
||||
const possibleMatches = allShownUris.filter((uri) => {
|
||||
try {
|
||||
// yuck a try catch in a filter?
|
||||
const { channelName } = parseURI(uri);
|
||||
return channelName && channelName.toLowerCase().includes(termToMatch);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
const searchSize = 5;
|
||||
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
|
||||
const { results, loading } = useLighthouse(debouncedTerm, showMature, searchSize, additionalOptions, 0);
|
||||
const stringifiedResults = JSON.stringify(results);
|
||||
|
||||
const hasMinLength = mentionTerm && mentionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
|
||||
const isTyping = debouncedTerm !== mentionTerm;
|
||||
const showPlaceholder = isTyping || loading;
|
||||
|
||||
const isUriFromTermValid = !regexInvalidURI.test(mentionTerm.substring(1));
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(value, key) => {
|
||||
if (customSelectAction) {
|
||||
// Give them full results, as our resolved one might truncate the claimId.
|
||||
customSelectAction(value || (results && results.find((r) => r.startsWith(value))) || '', Number(key));
|
||||
}
|
||||
},
|
||||
[customSelectAction, results]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (isTyping) setDebouncedTerm(!hasMinLength ? '' : mentionTerm);
|
||||
}, INPUT_DEBOUNCE_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [hasMinLength, isTyping, mentionTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!mainEl) return;
|
||||
const header = document.querySelector('.header__navigation');
|
||||
|
||||
function handleReflow() {
|
||||
const boxAtTopOfPage = header && mainEl.getBoundingClientRect().top <= header.offsetHeight;
|
||||
const boxAtBottomOfPage = mainEl.getBoundingClientRect().bottom >= window.innerHeight;
|
||||
|
||||
if (boxAtTopOfPage) {
|
||||
mainEl.setAttribute('flow-bottom', '');
|
||||
}
|
||||
if (mainEl.getAttribute('flow-bottom') !== null && boxAtBottomOfPage) {
|
||||
mainEl.removeAttribute('flow-bottom');
|
||||
}
|
||||
}
|
||||
handleReflow();
|
||||
|
||||
window.addEventListener('scroll', handleReflow);
|
||||
return () => window.removeEventListener('scroll', handleReflow);
|
||||
}, [mainEl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!inputRef || !comboboxInputRef || !mentionTerm) return;
|
||||
|
||||
function handleKeyDown(event) {
|
||||
const { keyCode } = event;
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (keyCode === KEYCODES.UP || keyCode === KEYCODES.DOWN) {
|
||||
if (isRefFocused(comboboxInputRef)) {
|
||||
const selectedId = activeElement && activeElement.getAttribute('aria-activedescendant');
|
||||
const selectedItem = selectedId && document.querySelector(`li[id="${selectedId}"]`);
|
||||
if (selectedItem) selectedItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
} else {
|
||||
// $FlowFixMe
|
||||
comboboxInputRef.current.focus();
|
||||
}
|
||||
} else {
|
||||
if ((isRefFocused(comboboxInputRef) || isRefFocused(inputRef)) && keyCode === KEYCODES.TAB) {
|
||||
event.preventDefault();
|
||||
const activeValue = activeElement && activeElement.getAttribute('value');
|
||||
|
||||
if (activeValue) {
|
||||
handleSelect(activeValue, keyCode);
|
||||
} else if (possibleMatches.length) {
|
||||
// $FlowFixMe
|
||||
const suggest = allShownCanonical.find((matchUri) => possibleMatches.find((uri) => uri.includes(matchUri)));
|
||||
if (suggest) handleSelect(suggest, keyCode);
|
||||
} else if (results) {
|
||||
handleSelect(mentionTerm, keyCode);
|
||||
}
|
||||
}
|
||||
if (isRefFocused(comboboxInputRef)) {
|
||||
// $FlowFixMe
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [allShownCanonical, handleSelect, inputRef, mentionTerm, possibleMatches, results]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!stringifiedResults) return;
|
||||
|
||||
const arrayResults = JSON.parse(stringifiedResults);
|
||||
if (arrayResults && arrayResults.length > 0) {
|
||||
// $FlowFixMe
|
||||
doResolveUris(arrayResults).then((response) => {
|
||||
try {
|
||||
// $FlowFixMe
|
||||
const canonical_urls = Object.values(response).map(({ canonical_url }) => canonical_url);
|
||||
setCanonicalResults(canonical_urls);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
}, [doResolveUris, stringifiedResults]);
|
||||
|
||||
// Only resolve the subscriptions that match the mention term, instead of all
|
||||
React.useEffect(() => {
|
||||
if (isTyping) return;
|
||||
|
||||
const urisToResolve = [];
|
||||
subscriptionUris.map(
|
||||
(uri) =>
|
||||
hasMinLength &&
|
||||
possibleMatches.includes(uri) &&
|
||||
unresolvedSubscriptions.includes(uri) &&
|
||||
urisToResolve.push(uri)
|
||||
);
|
||||
|
||||
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
|
||||
}, [doResolveUris, hasMinLength, isTyping, possibleMatches, subscriptionUris, unresolvedSubscriptions]);
|
||||
|
||||
const suggestionsRow = (
|
||||
label: string,
|
||||
suggestions: Array<string>,
|
||||
canonical: Array<string>,
|
||||
hasSuggestionsBelow: boolean
|
||||
) => {
|
||||
if (mentionTerm.length > 1 && suggestions !== results) {
|
||||
suggestions = suggestions.filter((uri) => possibleMatches.includes(uri));
|
||||
} else if (suggestions === results) {
|
||||
suggestions = suggestions.filter((uri) => !allShownUris.includes(uri));
|
||||
}
|
||||
// $FlowFixMe
|
||||
suggestions = suggestions
|
||||
.map((matchUri) => canonical.find((uri) => matchUri.includes(uri)))
|
||||
.filter((uri) => Boolean(uri));
|
||||
|
||||
if (canonical === canonicalResults) {
|
||||
suggestions = suggestions.filter((uri) => uri !== mostSupported);
|
||||
}
|
||||
|
||||
return !suggestions.length ? null : (
|
||||
<>
|
||||
<div className="channel-mention__label">{label}</div>
|
||||
{suggestions.map((uri) => (
|
||||
<ChannelMentionSuggestion key={uri} uri={uri} />
|
||||
))}
|
||||
{hasSuggestionsBelow && <hr className="channel-mention__top-separator" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return isRefFocused(inputRef) || isRefFocused(comboboxInputRef) ? (
|
||||
<Form onSubmit={() => handleSelect(mentionTerm)}>
|
||||
<Combobox className="channel-mention" onSelect={handleSelect}>
|
||||
<ComboboxInput ref={comboboxInputRef} className="channel-mention__input--none" value={mentionTerm} />
|
||||
{mentionTerm && isUriFromTermValid && (
|
||||
<ComboboxPopover portal={false} className="channel-mention__suggestions">
|
||||
<ComboboxList ref={comboboxListRef}>
|
||||
{creatorUri &&
|
||||
suggestionsRow(
|
||||
__('Creator'),
|
||||
[creatorUri],
|
||||
[canonicalCreator],
|
||||
canonicalSubscriptions.length > 0 || commentorUris.length > 0 || !showPlaceholder
|
||||
)}
|
||||
{canonicalSubscriptions.length > 0 &&
|
||||
suggestionsRow(
|
||||
__('Following'),
|
||||
subscriptionUris,
|
||||
canonicalSubscriptions,
|
||||
commentorUris.length > 0 || !showPlaceholder
|
||||
)}
|
||||
{commentorUris.length > 0 &&
|
||||
suggestionsRow(__('From comments'), commentorUris, canonicalCommentors, !showPlaceholder)}
|
||||
|
||||
{hasMinLength &&
|
||||
(showPlaceholder ? (
|
||||
<Spinner type="small" />
|
||||
) : (
|
||||
results && (
|
||||
<>
|
||||
{!noTopSuggestion && (
|
||||
<ChannelMentionTopSuggestion
|
||||
query={debouncedTerm}
|
||||
shownUris={allShownCanonical}
|
||||
setMostSupported={(winningUri) => setMostSupported(winningUri)}
|
||||
/>
|
||||
)}
|
||||
{suggestionsRow(__('From search'), results, canonicalResults, false)}
|
||||
</>
|
||||
)
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
</Combobox>
|
||||
</Form>
|
||||
) : null;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectIsUriResolving } from 'redux/selectors/claims';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
||||
import ChannelMentionTopSuggestion from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const uriFromQuery = `lbry://${props.query}`;
|
||||
return {
|
||||
uriFromQuery,
|
||||
isResolvingUri: selectIsUriResolving(state, uriFromQuery),
|
||||
winningUri: makeSelectWinningUriForQuery(props.query)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, { doResolveUri })(ChannelMentionTopSuggestion);
|
|
@ -1,49 +0,0 @@
|
|||
// @flow
|
||||
import ChannelMentionSuggestion from 'component/channelMentionSuggestion';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
uriFromQuery: string,
|
||||
winningUri: string,
|
||||
isResolvingUri: boolean,
|
||||
shownUris: Array<string>,
|
||||
setMostSupported: (string) => void,
|
||||
doResolveUri: (string) => void,
|
||||
};
|
||||
|
||||
export default function ChannelMentionTopSuggestion(props: Props) {
|
||||
const { uriFromQuery, winningUri, isResolvingUri, shownUris, setMostSupported, doResolveUri } = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uriFromQuery) doResolveUri(uriFromQuery);
|
||||
}, [doResolveUri, uriFromQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (winningUri) setMostSupported(winningUri);
|
||||
}, [setMostSupported, winningUri]);
|
||||
|
||||
if (isResolvingUri) {
|
||||
return (
|
||||
<div className="channel-mention__winning-claim">
|
||||
<div className="channel-mention__label channel-mention__placeholder-label" />
|
||||
|
||||
<div className="channel-mention__suggestion channel-mention__placeholder-suggestion">
|
||||
<div className="channel-mention__placeholder-thumbnail" />
|
||||
<div className="channel-mention__placeholder-info" />
|
||||
</div>
|
||||
<hr className="channel-mention__top-separator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return !winningUri || shownUris.includes(winningUri) ? null : (
|
||||
<>
|
||||
<div className="channel-mention__label">
|
||||
<LbcSymbol prefix={__('Most Supported')} />
|
||||
</div>
|
||||
<ChannelMentionSuggestion uri={winningUri} />
|
||||
<hr className="channel-mention__top-separator" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 loading="lazy" />
|
||||
</div>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<Expandable>
|
||||
<MarkdownPreview
|
||||
|
|
66
ui/component/commentCreate/emote-selector.jsx
Normal file
66
ui/component/commentCreate/emote-selector.jsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
// @flow
|
||||
import 'scss/component/_emote-selector.scss';
|
||||
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import EMOJIS from 'emoji-dictionary';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
|
||||
const OLD_QUICK_EMOJIS = [
|
||||
EMOJIS.getUnicode('rocket'),
|
||||
EMOJIS.getUnicode('jeans'),
|
||||
EMOJIS.getUnicode('fire'),
|
||||
EMOJIS.getUnicode('heart'),
|
||||
EMOJIS.getUnicode('open_mouth'),
|
||||
];
|
||||
|
||||
type Props = { commentValue: string, setCommentValue: (string) => void, closeSelector: () => void };
|
||||
|
||||
export default function EmoteSelector(props: Props) {
|
||||
const { commentValue, setCommentValue, closeSelector } = props;
|
||||
|
||||
function addEmoteToComment(emote: string) {
|
||||
setCommentValue(
|
||||
commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="emoteSelector">
|
||||
<Button button="close" icon={ICONS.REMOVE} onClick={closeSelector} />
|
||||
|
||||
<div className="emoteSelector__list">
|
||||
<div className="emoteSelector__listRow">
|
||||
<div className="emoteSelector__listRowItems">
|
||||
{OLD_QUICK_EMOJIS.map((emoji) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
label={emoji}
|
||||
title={`:${EMOJIS.getName(emoji)}:`}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => addEmoteToComment(emoji)}
|
||||
/>
|
||||
))}
|
||||
{EMOTES.map((emote) => {
|
||||
const emoteName = emote.name.toLowerCase();
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={emoteName}
|
||||
title={emoteName}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => addEmoteToComment(emoteName)}
|
||||
>
|
||||
<OptimizedImage src={emote.url} waitLoad />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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,12 +28,12 @@ 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)),
|
||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment, sticker) =>
|
||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment, sticker)),
|
||||
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
fetchComment: (commentId) => dispatch(doCommentById(commentId, false)),
|
||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentCreate);
|
||||
|
|
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// @flow
|
||||
import 'scss/component/_sticker-selector.scss';
|
||||
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
|
||||
const buildStickerSideLink = (section: string, icon: string) => ({ section, icon });
|
||||
|
||||
const STICKER_SIDE_LINKS = [
|
||||
buildStickerSideLink(__('Free'), ICONS.TAG),
|
||||
buildStickerSideLink(__('Tips'), ICONS.FINANCE),
|
||||
// Future work may include Channel, Subscriptions, ...
|
||||
];
|
||||
|
||||
type Props = { claimIsMine: boolean, onSelect: (any) => void };
|
||||
|
||||
export default function StickerSelector(props: Props) {
|
||||
const { claimIsMine, onSelect } = props;
|
||||
|
||||
function scrollToStickerSection(section: string) {
|
||||
const listBodyEl = document.querySelector('.stickerSelector__listBody');
|
||||
const sectionToScroll = document.getElementById(section);
|
||||
|
||||
if (listBodyEl && sectionToScroll) {
|
||||
// $FlowFixMe
|
||||
listBodyEl.scrollTo({
|
||||
top: sectionToScroll.offsetTop - sectionToScroll.getBoundingClientRect().height * 2,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getListRow = (rowTitle: string, rowStickers: any) => (
|
||||
<div className="stickerSelector__listBody-row">
|
||||
<div id={rowTitle} className="stickerSelector__listBody-rowTitle">
|
||||
{rowTitle}
|
||||
</div>
|
||||
<div className="stickerSelector__listBody-rowItems">
|
||||
{rowStickers.map((sticker) => (
|
||||
<Button
|
||||
key={sticker.name}
|
||||
title={sticker.name}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => onSelect(sticker)}
|
||||
>
|
||||
<OptimizedImage src={sticker.url} waitLoad loading="lazy" />
|
||||
{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,4 +1,8 @@
|
|||
// @flow
|
||||
|
||||
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';
|
||||
|
@ -8,158 +12,142 @@ import * as ICONS from 'constants/icons';
|
|||
import * as KEYCODES from 'constants/keycodes';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import Button from 'component/button';
|
||||
import ChannelMentionSuggestions from 'component/channelMentionSuggestions';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
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,
|
||||
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 [activeTab, setActiveTab] = React.useState('');
|
||||
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 selectedMentionIndex =
|
||||
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
||||
? commentValue.indexOf('@', selectionIndex)
|
||||
: commentValue.lastIndexOf('@', selectionIndex);
|
||||
const modifierIndex = commentValue.indexOf(':', selectedMentionIndex);
|
||||
const spaceIndex = commentValue.indexOf(' ', selectedMentionIndex);
|
||||
const mentionLengthIndex =
|
||||
modifierIndex >= 0 && (spaceIndex === -1 || modifierIndex < spaceIndex)
|
||||
? modifierIndex
|
||||
: spaceIndex >= 0 && (modifierIndex === -1 || spaceIndex < modifierIndex)
|
||||
? spaceIndex
|
||||
: commentValue.length;
|
||||
const channelMention =
|
||||
selectedMentionIndex >= 0 && selectionIndex <= mentionLengthIndex
|
||||
? commentValue.substring(selectedMentionIndex, mentionLengthIndex)
|
||||
: '';
|
||||
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
||||
const [exchangeRate, setExchangeRate] = React.useState();
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
|
||||
|
||||
const claimId = claim && claim.claim_id;
|
||||
const signingChannel = (claim && claim.signing_channel) || claim;
|
||||
const channelUri = signingChannel && signingChannel.permanent_url;
|
||||
const charCount = commentValue ? commentValue.length : 0;
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend;
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length;
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
|
||||
const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0;
|
||||
const minAmount = minTip || minSuper || 0;
|
||||
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
||||
const stickerPrice = selectedSticker && selectedSticker.price;
|
||||
|
||||
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_LBC);
|
||||
setIsSupportComment(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommentChange(event) {
|
||||
let commentValue;
|
||||
if (isReply) {
|
||||
|
@ -171,19 +159,6 @@ export function CommentCreate(props: Props) {
|
|||
setCommentValue(commentValue);
|
||||
}
|
||||
|
||||
function handleSelectMention(mentionValue, key) {
|
||||
let newMentionValue = mentionValue.replace('lbry://', '');
|
||||
if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':');
|
||||
|
||||
setCommentValue(
|
||||
commentValue.substring(0, selectedMentionIndex) +
|
||||
`${newMentionValue}` +
|
||||
(commentValue.length > mentionLengthIndex + 1
|
||||
? commentValue.substring(mentionLengthIndex, commentValue.length)
|
||||
: ' ')
|
||||
);
|
||||
}
|
||||
|
||||
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||
e.preventDefault();
|
||||
|
@ -199,16 +174,8 @@ export function CommentCreate(props: Props) {
|
|||
window.removeEventListener('keydown', altEnterListener);
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (activeChannelClaim && commentValue.length) {
|
||||
handleCreateComment();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSupportComment() {
|
||||
if (!activeChannelClaim) {
|
||||
return;
|
||||
}
|
||||
if (!activeChannelClaim) return;
|
||||
|
||||
if (!channelId) {
|
||||
doToast({
|
||||
|
@ -236,7 +203,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;
|
||||
}
|
||||
|
||||
|
@ -245,33 +212,18 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
|
||||
function doSubmitTip() {
|
||||
if (!activeChannelClaim) {
|
||||
return;
|
||||
}
|
||||
if (!activeChannelClaim || isSubmitting) return;
|
||||
|
||||
const params = {
|
||||
amount: tipAmount,
|
||||
claim_id: claimId,
|
||||
channel_id: activeChannelClaim.claim_id,
|
||||
};
|
||||
setSubmitting(true);
|
||||
|
||||
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;
|
||||
|
||||
// otherwise it's on the channel page
|
||||
} else {
|
||||
channelClaimId = claim.claim_id;
|
||||
tipChannelName = claim.name;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
// const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
||||
if (activeTab === TAB_LBC) {
|
||||
// call sendTip and then run the callback from the response
|
||||
|
@ -286,72 +238,34 @@ export function CommentCreate(props: Props) {
|
|||
}, 1500);
|
||||
|
||||
doToast({
|
||||
message: __(
|
||||
"You sent %tipAmount% LBRY Credits as a tip to %tipChannelName%, I'm sure they appreciate it!",
|
||||
{
|
||||
message: __("You sent %tipAmount% Credits as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||
tipAmount: tipAmount, // force show decimal places
|
||||
tipChannelName,
|
||||
}
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
setSuccessTip({ txid, tipAmount });
|
||||
},
|
||||
() => {
|
||||
// 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 - REMOVE
|
||||
// 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);
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,16 +276,21 @@ export function CommentCreate(props: Props) {
|
|||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||
*/
|
||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||
setIsSubmitting(true);
|
||||
if (isSubmitting) return;
|
||||
|
||||
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
|
||||
setShowEmotes(false);
|
||||
setSubmitting(true);
|
||||
|
||||
const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name);
|
||||
|
||||
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);
|
||||
if (!stickerValue) setCommentValue('');
|
||||
setReviewingSupportComment(false);
|
||||
setIsSupportComment(false);
|
||||
setCommentFailure(false);
|
||||
|
||||
|
@ -381,7 +300,7 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSubmitting(false);
|
||||
setSubmitting(false);
|
||||
setCommentFailure(true);
|
||||
|
||||
if (channelId) {
|
||||
|
@ -412,22 +331,72 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
}, [fetchComment, shouldFetchComment, parentId]);
|
||||
|
||||
// Debounce for disabling the submit button when mentioning a user with Enter
|
||||
// so that the comment isn't sent at the same time
|
||||
// Stickers: Get LBC-USD exchange rate if hasn't yet and selected a paid sticker
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (pauseQuickSend) {
|
||||
setPauseQuickSend(false);
|
||||
}
|
||||
}, MENTION_DEBOUNCE_MS);
|
||||
if (stickerPrice && !exchangeRate) Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD));
|
||||
}, [exchangeRate, stickerPrice]);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [pauseQuickSend]);
|
||||
// Stickers: Check if creator has a tip account saved (on selector so that if a paid sticker is selected,
|
||||
// it defaults to LBC tip instead of USD)
|
||||
React.useEffect(() => {
|
||||
if (!stripeEnvironment || !stickerSelector || canReceiveFiatTip !== undefined) return;
|
||||
|
||||
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
{
|
||||
channel_claim_id: channelClaimId,
|
||||
channel_name: tipChannelName,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((accountCheckResponse) => {
|
||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||
setCanReceiveFiatTip(true);
|
||||
} else {
|
||||
setCanReceiveFiatTip(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||
|
||||
// LIVESTREAM ONLY - REMOVE
|
||||
// Handle keyboard shortcut comment creation
|
||||
// React.useEffect(() => {
|
||||
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||
//
|
||||
// if (inputRef && inputRef.current === document.activeElement) {
|
||||
// // $FlowFixMe
|
||||
// const isTyping = e.target.attributes['term'];
|
||||
//
|
||||
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||
// e.preventDefault();
|
||||
// buttonRef.current.click();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// window.addEventListener('keydown', altEnterListener);
|
||||
//
|
||||
// // removes the listener so it doesn't cause problems elsewhere in the app
|
||||
// return () => {
|
||||
// window.removeEventListener('keydown', altEnterListener);
|
||||
// };
|
||||
// }, [isLivestream]);
|
||||
|
||||
// **************************************************************************
|
||||
// Render
|
||||
// **************************************************************************
|
||||
|
||||
const getActionButton = (title: string, label?: string, icon: string, handleClick: () => void) => (
|
||||
<Button title={title} label={label} button="alt" icon={icon} onClick={handleClick} />
|
||||
);
|
||||
|
||||
if (channelSettings && !channelSettings.comments_enabled) {
|
||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||
}
|
||||
|
@ -449,24 +418,107 @@ export function CommentCreate(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (isReviewingSupportComment && activeChannelClaim) {
|
||||
return (
|
||||
<div className="comment__create">
|
||||
<div className="comment__sc-preview">
|
||||
<Form
|
||||
onSubmit={() => {}}
|
||||
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 loading="lazy" />
|
||||
</div>
|
||||
{/* figure out lbc sticker prices */}
|
||||
{selectedSticker.price && exchangeRate && (
|
||||
<FilePrice
|
||||
customPrices={{ priceFiat: selectedSticker.price, priceLBC: selectedSticker.price / exchangeRate }}
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
autoFocus={isReply}
|
||||
charCount={charCount}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
disabled={isFetchingChannels}
|
||||
label={
|
||||
<div className="commentCreate__labelWrapper">
|
||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||
<SelectChannel tiny />
|
||||
</div>
|
||||
}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
ref={formFieldRef}
|
||||
onChange={handleCommentChange}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
value={commentValue}
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(isSupportComment || (isReviewingStickerComment && stickerPrice)) && (
|
||||
<WalletTipAmountSelector
|
||||
activeTab={activeTab}
|
||||
amount={tipAmount}
|
||||
claim={claim}
|
||||
convertedAmount={convertedAmount}
|
||||
customTipAmount={stickerPrice}
|
||||
exchangeRate={exchangeRate}
|
||||
fiatConversion={selectedSticker && !!selectedSticker.price} // REMOVE / figure out
|
||||
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"
|
||||
|
@ -480,95 +532,38 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
onClick={handleSupportComment}
|
||||
/>
|
||||
) : isReviewingStickerComment && selectedSticker ? (
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => setIsReviewingSupportComment(false)}
|
||||
/>
|
||||
{MinAmountNotice}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
button="primary"
|
||||
label={__('Send')}
|
||||
disabled={isSupportComment && (tipError || disableReviewButton)}
|
||||
onClick={() => {
|
||||
if (isSupportComment) {
|
||||
handleSupportComment();
|
||||
} else {
|
||||
handleCreateComment();
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className={classnames('comment__create', {
|
||||
'comment__create--reply': isReply,
|
||||
'comment__create--nested-reply': isNested,
|
||||
'comment__create--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{!advancedEditor && (
|
||||
<ChannelMentionSuggestions
|
||||
uri={uri}
|
||||
inputRef={formFieldInputRef}
|
||||
mentionTerm={channelMention}
|
||||
creatorUri={channelUri}
|
||||
customSelectAction={handleSelectMention}
|
||||
setSelectedSticker(null);
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setIsSupportComment(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<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)}
|
||||
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 ? (
|
||||
<>
|
||||
) : isSupportComment ? (
|
||||
<Button
|
||||
disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
|
||||
disabled={disabled || tipError || disableReviewButton || !minAmountMet}
|
||||
type="button"
|
||||
button="primary"
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} // only LBC
|
||||
label={__('Review')}
|
||||
onClick={() => setIsReviewingSupportComment(true)}
|
||||
onClick={() => setReviewingSupportComment(true)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => setIsSupportComment(false)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{(!minTip || claimIsMine) && (
|
||||
(!minTip || claimIsMine) && (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
disabled={disabled || stickerSelector}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
|
@ -579,37 +574,106 @@ export function CommentCreate(props: Props) {
|
|||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{!supportDisabled && !claimIsMine && (
|
||||
|
||||
{/** Stickers/Support Buttons **/}
|
||||
{!supportDisabled && !stickerSelector && (
|
||||
<>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
button="alt"
|
||||
className="thatButton"
|
||||
icon={ICONS.LBC}
|
||||
onClick={() => {
|
||||
{getActionButton(
|
||||
__('Stickers'),
|
||||
isReviewingStickerComment ? __('Different Sticker') : undefined,
|
||||
ICONS.STICKER,
|
||||
() => {
|
||||
if (isReviewingStickerComment) setReviewingStickerComment(false);
|
||||
setIsSupportComment(false);
|
||||
setStickerSelector(true);
|
||||
}
|
||||
)}
|
||||
{/* below buttons are unnecessary - REMOVE */}
|
||||
{!claimIsMine && (
|
||||
<>
|
||||
{(!isSupportComment || activeTab !== TAB_LBC) &&
|
||||
getActionButton(
|
||||
__('Credits'),
|
||||
isSupportComment ? __('Switch to Credits') : undefined,
|
||||
ICONS.LBC,
|
||||
() => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_LBC);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
|
||||
{stripeEnvironment &&
|
||||
(!isSupportComment || activeTab !== TAB_FIAT) &&
|
||||
getActionButton(
|
||||
__('Cash'),
|
||||
isSupportComment ? __('Switch to Cash') : undefined,
|
||||
ICONS.FINANCE,
|
||||
() => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_FIAT);
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isReply && !minTip && (
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cancel Button */}
|
||||
{(isSupportComment ||
|
||||
isReviewingSupportComment ||
|
||||
stickerSelector ||
|
||||
isReviewingStickerComment ||
|
||||
(isReply && !minTip)) && (
|
||||
<Button
|
||||
disabled={isSupportComment && isSubmitting}
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
onClick={() => {
|
||||
if (onCancelReplying) {
|
||||
if (isSupportComment || isReviewingSupportComment) {
|
||||
if (!isReviewingSupportComment) setIsSupportComment(false);
|
||||
setReviewingSupportComment(false);
|
||||
if (stickerPrice) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setSelectedSticker(null);
|
||||
}
|
||||
} else if (stickerSelector || isReviewingStickerComment) {
|
||||
setReviewingStickerComment(false);
|
||||
setStickerSelector(false);
|
||||
setSelectedSticker(null);
|
||||
} 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,63 +1,65 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { formatCredits, formatFullPrice } from 'util/format-credits';
|
||||
import classnames from 'classnames';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import { formatCredits, formatFullPrice } from 'util/format-credits';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
amount: number,
|
||||
amount?: number,
|
||||
className?: string,
|
||||
customAmounts?: { amountFiat: number, amountLBC: number },
|
||||
fee?: boolean,
|
||||
isEstimate?: boolean,
|
||||
isFiat?: boolean,
|
||||
noFormat?: boolean,
|
||||
precision: number,
|
||||
showFree: boolean,
|
||||
showFullPrice: boolean,
|
||||
showPlus: boolean,
|
||||
isEstimate?: boolean,
|
||||
showLBC?: boolean,
|
||||
fee?: boolean,
|
||||
className?: string,
|
||||
noFormat?: boolean,
|
||||
showPlus: boolean,
|
||||
size?: number,
|
||||
superChat?: boolean,
|
||||
superChatLight?: boolean,
|
||||
isFiat?: boolean,
|
||||
};
|
||||
|
||||
class CreditAmount extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
noFormat: false,
|
||||
precision: 2,
|
||||
showFree: false,
|
||||
showFullPrice: false,
|
||||
showPlus: false,
|
||||
showLBC: true,
|
||||
noFormat: false,
|
||||
showPlus: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
amount,
|
||||
precision,
|
||||
showFullPrice,
|
||||
showFree,
|
||||
showPlus,
|
||||
isEstimate,
|
||||
fee,
|
||||
showLBC,
|
||||
className,
|
||||
customAmounts,
|
||||
fee,
|
||||
isEstimate,
|
||||
isFiat,
|
||||
noFormat,
|
||||
precision,
|
||||
showFree,
|
||||
showFullPrice,
|
||||
showLBC,
|
||||
showPlus,
|
||||
size,
|
||||
superChat,
|
||||
superChatLight,
|
||||
isFiat,
|
||||
} = this.props;
|
||||
const minimumRenderableAmount = 10 ** (-1 * precision);
|
||||
|
||||
// return null, otherwise it will try and convert undefined to a string
|
||||
if (amount === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (amount === undefined && customAmounts === undefined) return null;
|
||||
|
||||
function getAmountText(amount: number, isFiat?: boolean) {
|
||||
const fullPrice = formatFullPrice(amount, 2);
|
||||
const isFree = parseFloat(amount) === 0;
|
||||
|
||||
let formattedAmount;
|
||||
|
||||
if (showFullPrice) {
|
||||
formattedAmount = fullPrice;
|
||||
} else {
|
||||
|
@ -67,11 +69,10 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
: formatCredits(amount, precision, true);
|
||||
}
|
||||
|
||||
let amountText;
|
||||
if (showFree && isFree) {
|
||||
amountText = __('Free');
|
||||
return __('Free');
|
||||
} else {
|
||||
amountText = noFormat ? amount : formattedAmount;
|
||||
let amountText = noFormat ? amount : formattedAmount;
|
||||
|
||||
if (showPlus && amount > 0) {
|
||||
amountText = `+${amountText}`;
|
||||
|
@ -86,17 +87,26 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
if (fee) {
|
||||
amountText = __('%amount% fee', { amount: amountText });
|
||||
}
|
||||
|
||||
return amountText;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
title={fullPrice}
|
||||
title={amount ? formatFullPrice(amount, 2) : ''}
|
||||
className={classnames(className, {
|
||||
'super-chat': superChat,
|
||||
'super-chat--light': superChatLight,
|
||||
})}
|
||||
>
|
||||
<span className="credit-amount">{amountText}</span>
|
||||
{customAmounts
|
||||
? Object.values(customAmounts).map((amount, index) => (
|
||||
<span key={String(amount)} className="credit-amount">
|
||||
{getAmountText(Number(amount), !index)}
|
||||
</span>
|
||||
))
|
||||
: amount && <span className="credit-amount">{getAmountText(amount, isFiat)}</span>}
|
||||
|
||||
{isEstimate ? (
|
||||
<span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}>
|
||||
|
|
|
@ -1,60 +1,53 @@
|
|||
// @flow
|
||||
import type { ElementRef, Node } from 'react';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import Button from 'component/button';
|
||||
import emoji from 'emoji-dictionary';
|
||||
|
||||
const QUICK_EMOJIS = [
|
||||
emoji.getUnicode('rocket'),
|
||||
emoji.getUnicode('jeans'),
|
||||
emoji.getUnicode('fire'),
|
||||
emoji.getUnicode('heart'),
|
||||
emoji.getUnicode('open_mouth'),
|
||||
];
|
||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||
import type { ElementRef, Node } from 'react';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
label?: string | Node,
|
||||
render?: () => React$Node,
|
||||
prefix?: string,
|
||||
postfix?: string,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
type?: string,
|
||||
onChange?: (any) => any,
|
||||
defaultValue?: string | number,
|
||||
placeholder?: string | number,
|
||||
children?: React$Node,
|
||||
stretch?: boolean,
|
||||
affixClass?: string, // class applied to prefix/postfix label
|
||||
autoFocus?: boolean,
|
||||
labelOnLeft: boolean,
|
||||
inputButton?: React$Node,
|
||||
blockWrap: boolean,
|
||||
charCount?: number,
|
||||
textAreaMaxLength?: number,
|
||||
range?: number,
|
||||
min?: number,
|
||||
max?: number,
|
||||
quickActionLabel?: string,
|
||||
quickActionHandler?: (any) => any,
|
||||
children?: React$Node,
|
||||
defaultValue?: string | number,
|
||||
disabled?: boolean,
|
||||
onChange: (any) => void,
|
||||
value?: string | number,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
hideSuggestions?: boolean,
|
||||
inputButton?: React$Node,
|
||||
isLivestream?: boolean,
|
||||
label?: string | Node,
|
||||
labelOnLeft: boolean,
|
||||
max?: number,
|
||||
min?: number,
|
||||
name: string,
|
||||
noEmojis?: boolean,
|
||||
placeholder?: string | number,
|
||||
postfix?: string,
|
||||
prefix?: string,
|
||||
quickActionLabel?: string,
|
||||
range?: number,
|
||||
readOnly?: boolean,
|
||||
stretch?: boolean,
|
||||
textAreaMaxLength?: number,
|
||||
type?: string,
|
||||
value?: string | number,
|
||||
onChange?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
quickActionHandler?: (any) => any,
|
||||
render?: () => React$Node,
|
||||
};
|
||||
|
||||
export class FormField extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
labelOnLeft: false,
|
||||
blockWrap: true,
|
||||
};
|
||||
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||
|
||||
input: { current: ElementRef<any> };
|
||||
|
||||
|
@ -67,36 +60,48 @@ export class FormField extends React.PureComponent<Props> {
|
|||
const { autoFocus } = this.props;
|
||||
const input = this.input.current;
|
||||
|
||||
if (input && autoFocus) {
|
||||
input.focus();
|
||||
}
|
||||
if (input && autoFocus) input.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
render,
|
||||
label,
|
||||
prefix,
|
||||
postfix,
|
||||
error,
|
||||
helper,
|
||||
name,
|
||||
type,
|
||||
children,
|
||||
stretch,
|
||||
affixClass,
|
||||
autoFocus,
|
||||
inputButton,
|
||||
labelOnLeft,
|
||||
blockWrap,
|
||||
charCount,
|
||||
textAreaMaxLength,
|
||||
quickActionLabel,
|
||||
quickActionHandler,
|
||||
children,
|
||||
error,
|
||||
helper,
|
||||
hideSuggestions,
|
||||
inputButton,
|
||||
isLivestream,
|
||||
label,
|
||||
labelOnLeft,
|
||||
name,
|
||||
noEmojis,
|
||||
postfix,
|
||||
prefix,
|
||||
quickActionLabel,
|
||||
stretch,
|
||||
textAreaMaxLength,
|
||||
type,
|
||||
openEmoteMenu,
|
||||
quickActionHandler,
|
||||
render,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
|
||||
const errorMessage = typeof error === 'object' ? error.message : error;
|
||||
|
||||
// Ideally, the character count should (and can) be appended to the
|
||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||
// to pass the current value to it's callback, nor query the current
|
||||
// text length from the callback. So, we'll use our own widget.
|
||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
|
||||
const Wrapper = blockWrap
|
||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||
|
@ -108,32 +113,15 @@ export class FormField extends React.PureComponent<Props> {
|
|||
</div>
|
||||
) : null;
|
||||
|
||||
let input;
|
||||
if (type) {
|
||||
if (type === 'radio') {
|
||||
input = (
|
||||
<Wrapper>
|
||||
<input id={name} type="radio" {...inputProps} />
|
||||
const inputSimple = (type: string) => (
|
||||
<>
|
||||
<input id={name} type={type} {...inputProps} />
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
} else if (type === 'checkbox') {
|
||||
input = (
|
||||
<div className="checkbox">
|
||||
<input id={name} type="checkbox" {...inputProps} />
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'range') {
|
||||
input = (
|
||||
<div>
|
||||
<input id={name} type="range" {...inputProps} />
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'select') {
|
||||
input = (
|
||||
<fieldset-section>
|
||||
|
||||
const inputSelect = (selectClass: string) => (
|
||||
<fieldset-section class={selectClass}>
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||
)}
|
||||
|
@ -142,21 +130,21 @@ export class FormField extends React.PureComponent<Props> {
|
|||
</select>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else if (type === 'select-tiny') {
|
||||
input = (
|
||||
<fieldset-section class="select--slim">
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||
)}
|
||||
<select id={name} {...inputProps}>
|
||||
{children}
|
||||
</select>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else if (type === 'markdown') {
|
||||
const handleEvents = {
|
||||
contextmenu: openEditorMenu,
|
||||
};
|
||||
|
||||
const input = () => {
|
||||
switch (type) {
|
||||
case 'radio':
|
||||
return <Wrapper>{inputSimple('radio')}</Wrapper>;
|
||||
case 'checkbox':
|
||||
return <div className="checkbox">{inputSimple('checkbox')}</div>;
|
||||
case 'range':
|
||||
return <div>{inputSimple('range')}</div>;
|
||||
case 'select':
|
||||
return inputSelect('');
|
||||
case 'select-tiny':
|
||||
return inputSelect('select--slim');
|
||||
case 'markdown':
|
||||
const handleEvents = { contextmenu: openEditorMenu };
|
||||
|
||||
const getInstance = (editor) => {
|
||||
// SimpleMDE max char check
|
||||
|
@ -164,9 +152,9 @@ export class FormField extends React.PureComponent<Props> {
|
|||
if (textAreaMaxLength && changes.update) {
|
||||
var str = changes.text.join('\n');
|
||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||
if (delta <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (delta <= 0) return;
|
||||
|
||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||
if (delta > 0) {
|
||||
str = str.substr(0, str.length - delta);
|
||||
|
@ -208,22 +196,11 @@ export class FormField extends React.PureComponent<Props> {
|
|||
}, 25);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Do nothing (revert to original behavior)
|
||||
}
|
||||
} catch (e) {} // Do nothing (revert to original behavior)
|
||||
});
|
||||
};
|
||||
|
||||
// Ideally, the character count should (and can) be appended to the
|
||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||
// to pass the current value to it's callback, nor query the current
|
||||
// text length from the callback. So, we'll use our own widget.
|
||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
|
||||
input = (
|
||||
return (
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
<fieldset-section>
|
||||
<div className="form-field__two-column">
|
||||
|
@ -251,21 +228,17 @@ export class FormField extends React.PureComponent<Props> {
|
|||
</fieldset-section>
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'textarea') {
|
||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
input = (
|
||||
case 'textarea':
|
||||
return (
|
||||
<fieldset-section>
|
||||
{(label || quickAction) && (
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
|
@ -273,30 +246,33 @@ export class FormField extends React.PureComponent<Props> {
|
|||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && (
|
||||
<div className="form-field__quick-emojis">
|
||||
{QUICK_EMOJIS.map((emoji) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
disabled={inputProps.disabled}
|
||||
type="button"
|
||||
className="button--emoji"
|
||||
label={emoji}
|
||||
onClick={() => {
|
||||
inputProps.onChange({
|
||||
target: { value: inputProps.value ? `${inputProps.value} ${emoji}` : emoji },
|
||||
});
|
||||
}}
|
||||
) : (
|
||||
<TextareaWithSuggestions
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
inputRef={this.input}
|
||||
isLivestream={isLivestream}
|
||||
{...inputProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && openEmoteMenu && (
|
||||
<Button
|
||||
type="alt"
|
||||
className="button--file-action"
|
||||
title="Emotes"
|
||||
onClick={openEmoteMenu}
|
||||
icon={ICONS.EMOJI}
|
||||
iconSize={20}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{countInfo}
|
||||
</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else {
|
||||
default:
|
||||
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
||||
const inner = inputButton ? (
|
||||
<input-submit>
|
||||
|
@ -307,8 +283,7 @@ export class FormField extends React.PureComponent<Props> {
|
|||
inputElement
|
||||
);
|
||||
|
||||
input = (
|
||||
<React.Fragment>
|
||||
return (
|
||||
<fieldset-section>
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>
|
||||
|
@ -318,17 +293,15 @@ export class FormField extends React.PureComponent<Props> {
|
|||
{prefix && <label htmlFor={name}>{prefix}</label>}
|
||||
{inner}
|
||||
</fieldset-section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{input}
|
||||
|
||||
<>
|
||||
{type && input()}
|
||||
{helper && <div className="form-field__help">{helper}</div>}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2014,4 +2014,22 @@ export const icons = {
|
|||
<line x1="19" y1="20.5" x2="20" y2="20.5" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.EMOJI]: buildIcon(
|
||||
<g>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
||||
<line x1="9" y1="9" x2="9.01" y2="9" />
|
||||
<line x1="15" y1="9" x2="15.01" y2="9" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.STICKER]: buildIcon(
|
||||
<g>
|
||||
<path d="M7.13,9a.38.38,0,1,1-.38.38A.38.38,0,0,1,7.13,9" />
|
||||
<path d="M5.51,15.42A7.34,7.34,0,0,0,12,19.34a7.83,7.83,0,0,0,.92-.06" />
|
||||
<path d="M23.24,11.52A11.25,11.25,0,1,0,12,23.25h.5" />
|
||||
<path d="M14.45,9.66a2.31,2.31,0,0,1,3.91,0" />
|
||||
<line x1="23.24" y1="11.52" x2="12.5" y2="23.24" />
|
||||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -10,13 +10,22 @@ import remarkFrontMatter from 'remark-frontmatter';
|
|||
import reactRenderer from 'remark-react';
|
||||
import MarkdownLink from 'component/markdownLink';
|
||||
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
||||
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
||||
import { formattedLinks, inlineLinks } from 'util/remark-lbry';
|
||||
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
|
||||
import { formattedEmote, inlineEmote } from 'util/remark-emote';
|
||||
import ZoomableImage from 'component/zoomableImage';
|
||||
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { parse } from 'node-html-parser';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
|
||||
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
||||
|
||||
function isEmote(title, src) {
|
||||
return title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons');
|
||||
}
|
||||
|
||||
type SimpleTextProps = {
|
||||
children?: React.Node,
|
||||
};
|
||||
|
@ -42,6 +51,7 @@ type MarkdownProps = {
|
|||
className?: string,
|
||||
parentCommentId?: string,
|
||||
isMarkdownPost?: boolean,
|
||||
disableTimestamps?: boolean,
|
||||
stakedLevel?: number,
|
||||
};
|
||||
|
||||
|
@ -93,10 +103,15 @@ const SimpleLink = (props: SimpleLinkProps) => {
|
|||
|
||||
const SimpleImageLink = (props: ImageLinkProps) => {
|
||||
const { src, title, alt, helpText } = props;
|
||||
|
||||
if (!src) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEmote(title, src)) {
|
||||
return <OptimizedImage src={src} title={title} className="emote" waitLoad loading="lazy" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
button="link"
|
||||
|
@ -132,9 +147,20 @@ function isStakeEnoughForPreview(stakedLevel) {
|
|||
// ****************************************************************************
|
||||
|
||||
const MarkdownPreview = (props: MarkdownProps) => {
|
||||
const { content, strip, simpleLinks, noDataStore, className, parentCommentId, isMarkdownPost, stakedLevel } = props;
|
||||
const {
|
||||
content,
|
||||
strip,
|
||||
simpleLinks,
|
||||
noDataStore,
|
||||
className,
|
||||
parentCommentId,
|
||||
isMarkdownPost,
|
||||
disableTimestamps,
|
||||
stakedLevel,
|
||||
} = props;
|
||||
|
||||
const strippedContent = content
|
||||
? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => {
|
||||
? content.replace(REPLACE_REGEX, (iframeHtml) => {
|
||||
// Let the browser try to create an iframe to see if the markup is valid
|
||||
let lbrySrc;
|
||||
try {
|
||||
|
@ -152,6 +178,10 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
})
|
||||
: '';
|
||||
|
||||
const initialQuote = strippedContent.split(' ').find((word) => word.length > 0 || word.charAt(0) === '>');
|
||||
let stripQuote;
|
||||
if (initialQuote && initialQuote.charAt(0) === '>') stripQuote = true;
|
||||
|
||||
const remarkOptions: Object = {
|
||||
sanitize: schema,
|
||||
fragment: React.Fragment,
|
||||
|
@ -169,9 +199,12 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
),
|
||||
// Workaraund of remarkOptions.Fragment
|
||||
div: React.Fragment,
|
||||
img: isStakeEnoughForPreview(stakedLevel)
|
||||
? ZoomableImage
|
||||
: (imgProps) => <SimpleImageLink src={imgProps.src} alt={imgProps.alt} title={imgProps.title} />,
|
||||
img: (imgProps) =>
|
||||
isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src) ? (
|
||||
ZoomableImage
|
||||
) : (
|
||||
<SimpleImageLink src={imgProps.src} alt={imgProps.alt} title={imgProps.title} />
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -183,10 +216,22 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
};
|
||||
|
||||
// Strip all content and just render text
|
||||
if (strip) {
|
||||
if (strip || stripQuote) {
|
||||
// Remove new lines and extra space
|
||||
remarkOptions.remarkReactComponents.p = SimpleText;
|
||||
return (
|
||||
return stripQuote ? (
|
||||
<span dir="auto" className="markdown-preview">
|
||||
<blockquote>
|
||||
{
|
||||
remark()
|
||||
.use(remarkStrip)
|
||||
.use(remarkFrontMatter, ['yaml'])
|
||||
.use(reactRenderer, remarkOptions)
|
||||
.processSync(content).contents
|
||||
}
|
||||
</blockquote>
|
||||
</span>
|
||||
) : (
|
||||
<span dir="auto" className="markdown-preview">
|
||||
{
|
||||
remark()
|
||||
|
@ -206,11 +251,13 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
.use(remarkAttr, remarkAttrOpts)
|
||||
// Remark plugins for lbry urls
|
||||
// Note: The order is important
|
||||
.use(formatedLinks)
|
||||
.use(formattedLinks)
|
||||
.use(inlineLinks)
|
||||
.use(isMarkdownPost ? null : inlineTimestamp)
|
||||
.use(isMarkdownPost ? null : formattedTimestamp)
|
||||
.use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
|
||||
.use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
|
||||
// Emojis
|
||||
.use(inlineEmote)
|
||||
.use(formattedEmote)
|
||||
.use(remarkEmoji)
|
||||
// Render new lines without needing spaces.
|
||||
.use(remarkBreaks)
|
||||
|
|
|
@ -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,
|
||||
customPrices?: { priceFiat: number, priceLBC: 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,45 @@ 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,
|
||||
customPrices,
|
||||
} = this.props;
|
||||
|
||||
if (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree)) {
|
||||
return null;
|
||||
}
|
||||
if (!customPrices && (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 : undefined}
|
||||
customAmounts={
|
||||
customPrices ? { amountFiat: customPrices.priceFiat, amountLBC: customPrices.priceLBC } : undefined
|
||||
}
|
||||
className={className}
|
||||
isEstimate={!!costInfo && !costInfo.includesData}
|
||||
isFiat={isFiat} // this goes
|
||||
showFree
|
||||
showLBC={showLBC}
|
||||
amount={costInfo.cost}
|
||||
isEstimate={!costInfo.includesData}
|
||||
showFullPrice={showFullPrice}
|
||||
showLBC={showLBC}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import { connect } from 'react-redux';
|
|||
import { doReadNotifications, doDeleteNotification } from 'redux/actions/notifications';
|
||||
import Notification from './view';
|
||||
|
||||
export default connect(null, {
|
||||
doReadNotifications,
|
||||
doDeleteNotification,
|
||||
})(Notification);
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
readNotification: () => dispatch(doReadNotifications([ownProps.notification.id])),
|
||||
deleteNotification: () => dispatch(doDeleteNotification(ownProps.notification.id)),
|
||||
});
|
||||
|
||||
export default connect(null, perform)(Notification);
|
||||
|
|
|
@ -1,64 +1,63 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
|
||||
import { parseSticker } from 'util/comments';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { RULE } from 'constants/notifications';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'component/common/icon';
|
||||
import DateTime from 'component/dateTime';
|
||||
import { useHistory } from 'react-router';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { useHistory } from 'react-router';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
import NotificationContentChannelMenu from 'component/notificationContentChannelMenu';
|
||||
import LbcMessage from 'component/common/lbc-message';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import CommentReactions from 'component/commentReactions';
|
||||
import classnames from 'classnames';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
import CommentReactions from 'component/commentReactions';
|
||||
import CommentsReplies from 'component/commentsReplies';
|
||||
import DateTime from 'component/dateTime';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
import Icon from 'component/common/icon';
|
||||
import LbcMessage from 'component/common/lbc-message';
|
||||
import NotificationContentChannelMenu from 'component/notificationContentChannelMenu';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
|
||||
type Props = {
|
||||
notification: WebNotification,
|
||||
menuButton: boolean,
|
||||
children: any,
|
||||
doReadNotifications: ([number]) => void,
|
||||
doDeleteNotification: (number) => void,
|
||||
notification: WebNotification,
|
||||
deleteNotification: () => void,
|
||||
readNotification: () => void,
|
||||
};
|
||||
|
||||
export default function Notification(props: Props) {
|
||||
const { notification, menuButton = false, doReadNotifications, doDeleteNotification } = props;
|
||||
const { menuButton = false, notification, readNotification, deleteNotification } = props;
|
||||
|
||||
const { notification_rule, notification_parameters, is_read } = notification;
|
||||
|
||||
const { push } = useHistory();
|
||||
const { notification_rule, notification_parameters, is_read, id } = notification;
|
||||
const [isReplying, setReplying] = React.useState(false);
|
||||
const [quickReply, setQuickReply] = React.useState();
|
||||
|
||||
// ?
|
||||
const isIgnoredNotification = notification_rule === RULE.NEW_LIVESTREAM;
|
||||
if (isIgnoredNotification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCommentNotification =
|
||||
notification_rule === RULE.COMMENT ||
|
||||
notification_rule === RULE.COMMENT_REPLY ||
|
||||
notification_rule === RULE.CREATOR_COMMENT;
|
||||
const commentText = isCommentNotification && notification_parameters.dynamic.comment;
|
||||
const stickerFromComment = isCommentNotification && commentText && parseSticker(commentText);
|
||||
const notificationTarget = getNotificationTarget();
|
||||
|
||||
let notificationTarget;
|
||||
switch (notification_rule) {
|
||||
default:
|
||||
notificationTarget = notification_parameters.device.target;
|
||||
}
|
||||
|
||||
const creatorIcon = (channelUrl) => {
|
||||
return (
|
||||
const creatorIcon = (channelUrl) => (
|
||||
<UriIndicator uri={channelUrl} link>
|
||||
<ChannelThumbnail small uri={channelUrl} />
|
||||
</UriIndicator>
|
||||
);
|
||||
};
|
||||
let channelUrl;
|
||||
let icon;
|
||||
switch (notification_rule) {
|
||||
|
@ -94,8 +93,7 @@ export default function Notification(props: Props) {
|
|||
let channelName;
|
||||
if (channelUrl) {
|
||||
try {
|
||||
const { claimName } = parseURI(channelUrl);
|
||||
channelName = claimName;
|
||||
({ claimName: channelName } = parseURI(channelUrl));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
@ -123,25 +121,28 @@ export default function Notification(props: Props) {
|
|||
|
||||
try {
|
||||
const { isChannel } = parseURI(notificationTarget);
|
||||
if (isChannel) {
|
||||
urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
|
||||
}
|
||||
if (isChannel) urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
|
||||
} catch (e) {}
|
||||
|
||||
notificationLink += `?${urlParams.toString()}`;
|
||||
const navLinkProps = {
|
||||
to: notificationLink,
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
};
|
||||
const navLinkProps = { to: notificationLink, onClick: (e) => e.stopPropagation() };
|
||||
|
||||
function getNotificationTarget() {
|
||||
// switch (notification_rule) {
|
||||
// case RULE.DAILY_WATCH_AVAILABLE:
|
||||
// case RULE.DAILY_WATCH_REMIND:
|
||||
// return `/$/${PAGES.CHANNELS_FOLLOWING}`;
|
||||
// case RULE.MISSED_OUT:
|
||||
// case RULE.REWARDS_APPROVAL_PROMPT:
|
||||
// return `/$/${PAGES.REWARDS_VERIFY}?redirect=/$/${PAGES.REWARDS}`;
|
||||
// default:
|
||||
return notification_parameters.device.target;
|
||||
// }
|
||||
}
|
||||
|
||||
function handleNotificationClick() {
|
||||
if (!is_read) {
|
||||
doReadNotifications([id]);
|
||||
}
|
||||
|
||||
if (menuButton && notificationLink) {
|
||||
push(notificationLink);
|
||||
}
|
||||
if (!is_read) readNotification();
|
||||
if (menuButton && notificationLink) push(notificationLink);
|
||||
}
|
||||
|
||||
const Wrapper = menuButton
|
||||
|
@ -166,45 +167,40 @@ export default function Notification(props: Props) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('notification__wrapper', {
|
||||
'notification__wrapper--unread': !is_read,
|
||||
})}
|
||||
>
|
||||
<div className={classnames('notification__wrapper', { 'notification__wrapper--unread': !is_read })}>
|
||||
<Wrapper>
|
||||
<div className="notification__icon">{icon}</div>
|
||||
|
||||
<div className="notification__content-wrapper">
|
||||
<div className="notificationContent__wrapper">
|
||||
<div className="notification__content">
|
||||
<div className="notification__text-wrapper">
|
||||
{!isCommentNotification && <div className="notification__title">{title}</div>}
|
||||
|
||||
{isCommentNotification && commentText ? (
|
||||
<>
|
||||
<div className="notificationText__wrapper">
|
||||
<div className="notification__title">{title}</div>
|
||||
<div title={commentText} className="notification__text">
|
||||
{commentText}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
{!commentText ? (
|
||||
<div
|
||||
title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')}
|
||||
className="notification__text"
|
||||
>
|
||||
<LbcMessage>{notification_parameters.device.text}</LbcMessage>
|
||||
</div>
|
||||
</>
|
||||
) : stickerFromComment ? (
|
||||
<div className="sticker__comment">
|
||||
<OptimizedImage src={stickerFromComment.url} waitLoad loading="lazy" />
|
||||
</div>
|
||||
) : (
|
||||
<div title={commentText} className="notification__text">
|
||||
{commentText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{notification_rule === RULE.NEW_CONTENT && (
|
||||
<FileThumbnail uri={notification_parameters.device.target} className="notification__content-thumbnail" />
|
||||
<FileThumbnail uri={notification_parameters.device.target} className="notificationContent__thumbnail" />
|
||||
)}
|
||||
{notification_rule === RULE.NEW_LIVESTREAM && (
|
||||
<FileThumbnail
|
||||
thumbnail={notification_parameters.device.image_url}
|
||||
className="notification__content-thumbnail"
|
||||
className="notificationContent__thumbnail"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -212,10 +208,10 @@ export default function Notification(props: Props) {
|
|||
<div className="notification__extra">
|
||||
{!is_read && (
|
||||
<Button
|
||||
className="notification__mark-seen"
|
||||
className="notification__markSeen"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
doReadNotifications([id]);
|
||||
readNotification();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -228,7 +224,7 @@ export default function Notification(props: Props) {
|
|||
<div className="notification__menu">
|
||||
<Menu>
|
||||
<MenuButton
|
||||
className={'menu__button notification__menu-button'}
|
||||
className="menu__button notification__menuButton"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -237,7 +233,7 @@ export default function Notification(props: Props) {
|
|||
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
||||
</MenuButton>
|
||||
<MenuList className="menu__list">
|
||||
<MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}>
|
||||
<MenuItem className="menu__link" onSelect={() => deleteNotification()}>
|
||||
<Icon aria-hidden icon={ICONS.DELETE} />
|
||||
{__('Delete')}
|
||||
</MenuItem>
|
||||
|
|
|
@ -10,10 +10,11 @@ function scaleToDevicePixelRatio(value: number, window: any) {
|
|||
type Props = {
|
||||
src: string,
|
||||
objectFit?: string,
|
||||
waitLoad?: boolean,
|
||||
};
|
||||
|
||||
function OptimizedImage(props: Props) {
|
||||
const { objectFit, src, ...imgProps } = props;
|
||||
const { objectFit, src, waitLoad, ...imgProps } = props;
|
||||
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
||||
const ref = React.useRef<any>();
|
||||
|
||||
|
@ -101,8 +102,12 @@ function OptimizedImage(props: Props) {
|
|||
<img
|
||||
ref={ref}
|
||||
{...imgProps}
|
||||
style={{ visibility: waitLoad ? 'hidden' : 'visible' }}
|
||||
src={optimizedSrc}
|
||||
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)}
|
||||
onLoad={() => {
|
||||
if (waitLoad) ref.current.style.visibility = 'visible';
|
||||
adjustOptimizationIfNeeded(ref.current, objectFit, src);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||
import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||
import { selectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||
import RecommendedContent from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||
const { claim_id: claimId } = claim;
|
||||
const recommendedContentUris = makeSelectRecommendedContentForUri(props.uri)(state);
|
||||
const recommendedContentUris = selectRecommendedContentForUri(state, props.uri);
|
||||
const nextRecommendedUri = recommendedContentUris && recommendedContentUris[0];
|
||||
|
||||
return {
|
||||
|
|
9
ui/component/textareaSuggestionsItem/index.js
Normal file
9
ui/component/textareaSuggestionsItem/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||
import TextareaSuggestionsItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: props.uri && selectClaimForUri(state, props.uri),
|
||||
});
|
||||
|
||||
export default connect(select)(TextareaSuggestionsItem);
|
46
ui/component/textareaSuggestionsItem/view.jsx
Normal file
46
ui/component/textareaSuggestionsItem/view.jsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
// @flow
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
claim?: Claim,
|
||||
emote?: any,
|
||||
uri?: string,
|
||||
};
|
||||
|
||||
export default function TextareaSuggestionsItem(props: Props) {
|
||||
const { claim, emote, uri, ...autocompleteProps } = props;
|
||||
|
||||
if (emote) {
|
||||
const { name: value, url, unicode } = emote;
|
||||
|
||||
return (
|
||||
<div {...autocompleteProps} dispatch={undefined}>
|
||||
{unicode ? <div className="emote">{unicode}</div> : <img className="emote" src={url} />}
|
||||
|
||||
<div className="textareaSuggestion__label">
|
||||
<span className="textareaSuggestion__title textareaSuggestion__value textareaSuggestion__value--emote">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (claim) {
|
||||
const value = claim.canonical_url.replace('lbry://', '').replace('#', ':');
|
||||
|
||||
return (
|
||||
<div {...autocompleteProps} dispatch={undefined}>
|
||||
<ChannelThumbnail xsmall uri={uri} />
|
||||
|
||||
<div className="textareaSuggestion__label">
|
||||
<span className="textareaSuggestion__title">{(claim.value && claim.value.title) || value}</span>
|
||||
<span className="textareaSuggestion__value">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
46
ui/component/textareaWithSuggestions/index.js
Normal file
46
ui/component/textareaWithSuggestions/index.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import { doSetMentionSearchResults } from 'redux/actions/search';
|
||||
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
||||
// import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
|
||||
import { selectChannelMentionData } from 'redux/selectors/comments';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { withRouter } from 'react-router';
|
||||
import TextareaWithSuggestions from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { pathname } = props.location;
|
||||
const uri = `lbry:/${pathname.replaceAll(':', '#')}`;
|
||||
|
||||
// const maxComments = props.isLivestream ? MAX_LIVESTREAM_COMMENTS : -1;
|
||||
const maxComments = -1;
|
||||
const data = selectChannelMentionData(state, uri, maxComments);
|
||||
const {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions,
|
||||
commentorUris,
|
||||
hasNewResolvedResults,
|
||||
query,
|
||||
} = data;
|
||||
|
||||
return {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions,
|
||||
canonicalTop: makeSelectWinningUriForQuery(query)(state),
|
||||
commentorUris,
|
||||
hasNewResolvedResults,
|
||||
searchQuery: query,
|
||||
showMature: selectShowMatureContent(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
||||
doSetMentionSearchResults: (query, uris) => dispatch(doSetMentionSearchResults(query, uris)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(TextareaWithSuggestions));
|
417
ui/component/textareaWithSuggestions/view.jsx
Normal file
417
ui/component/textareaWithSuggestions/view.jsx
Normal file
|
@ -0,0 +1,417 @@
|
|||
// @flow
|
||||
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||
import { matchSorter } from 'match-sorter';
|
||||
import { SEARCH_OPTIONS } from 'constants/search';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import EMOJIS from 'emoji-dictionary';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import React from 'react';
|
||||
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import useLighthouse from 'effects/use-lighthouse';
|
||||
import useThrottle from 'effects/use-throttle';
|
||||
|
||||
const SUGGESTION_REGEX = new RegExp(
|
||||
'(?<Mention>(?:^| |\n)@[^\\s=&#$@%?:;/\\"<>%{}|^~[]*(?::[\\w]+)?)|(?<Emote>(?:^| |\n):[\\w+-]*:?)',
|
||||
'gm'
|
||||
);
|
||||
|
||||
/** Regex Explained step-by-step:
|
||||
*
|
||||
* 1) (?<Name>....) = naming a match into a possible group (either Mention or Emote)
|
||||
* 2) (?:^| |\n) = only allow for: sentence beginning, space or newline before the match (no words or symbols)
|
||||
* 3) [^\s=&#$@%?:;/\\"<>%{}|^~[]* = anything, except the characters inside
|
||||
* 4) (?::[\w]+)? = A mention can be matched with a ':' as a claim modifier with words or digits after as ID digits,
|
||||
* or else it's everything before the ':' (will then match the winning uri for the mention behind since has no canonical ID)
|
||||
* 5) :\w*:? = the emote Regex, possible to be matched with a ':' at the end to consider previously typed emotes
|
||||
*
|
||||
*/
|
||||
|
||||
const SEARCH_SIZE = 10;
|
||||
const LIGHTHOUSE_MIN_CHARACTERS = 3;
|
||||
const INPUT_DEBOUNCE_MS = 1000;
|
||||
|
||||
const EMOJI_MIN_CHARACTERS = 2;
|
||||
|
||||
type Props = {
|
||||
canonicalCommentors?: Array<string>,
|
||||
canonicalCreatorUri?: string,
|
||||
canonicalSearch?: Array<string>,
|
||||
canonicalSubscriptions?: Array<string>,
|
||||
canonicalTop?: string,
|
||||
className?: string,
|
||||
commentorUris?: Array<string>,
|
||||
disabled?: boolean,
|
||||
hasNewResolvedResults?: boolean,
|
||||
id: string,
|
||||
inputRef: any,
|
||||
isLivestream?: boolean,
|
||||
maxLength?: number,
|
||||
placeholder?: string,
|
||||
searchQuery?: string,
|
||||
showMature: boolean,
|
||||
type?: string,
|
||||
uri?: string,
|
||||
value: any,
|
||||
doResolveUris: (Array<string>) => void,
|
||||
doSetMentionSearchResults: (string, Array<string>) => void,
|
||||
onBlur: (any) => any,
|
||||
onChange: (any) => any,
|
||||
onFocus: (any) => any,
|
||||
};
|
||||
|
||||
export default function TextareaWithSuggestions(props: Props) {
|
||||
const {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions: canonicalSubs,
|
||||
canonicalTop,
|
||||
className,
|
||||
commentorUris,
|
||||
disabled,
|
||||
hasNewResolvedResults,
|
||||
id,
|
||||
inputRef,
|
||||
isLivestream,
|
||||
maxLength,
|
||||
placeholder,
|
||||
searchQuery,
|
||||
showMature,
|
||||
type,
|
||||
value: messageValue,
|
||||
doResolveUris,
|
||||
doSetMentionSearchResults,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
} = props;
|
||||
|
||||
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
|
||||
|
||||
const [suggestionValue, setSuggestionValue] = React.useState(undefined);
|
||||
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
|
||||
const [shouldClose, setClose] = React.useState();
|
||||
const [debouncedTerm, setDebouncedTerm] = React.useState('');
|
||||
// const [mostSupported, setMostSupported] = React.useState('');
|
||||
|
||||
const suggestionTerm = suggestionValue && suggestionValue.term;
|
||||
const isEmote = suggestionValue && suggestionValue.isEmote;
|
||||
const isMention = suggestionValue && !suggestionValue.isEmote;
|
||||
const invalidTerm = suggestionTerm && isMention && suggestionTerm.charAt(1) === ':';
|
||||
|
||||
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
|
||||
const { results, loading } = useLighthouse(debouncedTerm, showMature, SEARCH_SIZE, additionalOptions, 0);
|
||||
const stringifiedResults = JSON.stringify(results);
|
||||
|
||||
const hasMinLength = suggestionTerm && isMention && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
|
||||
const isTyping = isMention && debouncedTerm !== suggestionTerm;
|
||||
const showPlaceholder =
|
||||
isMention && !invalidTerm && (isTyping || loading || (results && results.length > 0 && !hasNewResolvedResults));
|
||||
|
||||
const shouldFilter = (uri, previous) => uri !== canonicalCreatorUri && (!previous || !previous.includes(uri));
|
||||
const filteredCommentors = canonicalCommentors && canonicalCommentors.filter((uri) => shouldFilter(uri));
|
||||
const filteredSubs = canonicalSubs && canonicalSubs.filter((uri) => shouldFilter(uri, filteredCommentors));
|
||||
const filteredTop =
|
||||
canonicalTop &&
|
||||
shouldFilter(canonicalTop, filteredSubs) &&
|
||||
shouldFilter(canonicalTop, filteredCommentors) &&
|
||||
canonicalTop;
|
||||
const filteredSearch =
|
||||
canonicalSearch &&
|
||||
canonicalSearch.filter(
|
||||
(uri) => shouldFilter(uri, filteredSubs) && shouldFilter(uri, filteredCommentors) && uri !== filteredTop
|
||||
);
|
||||
|
||||
let emoteNames;
|
||||
let emojiNames;
|
||||
const allOptions = [];
|
||||
if (isEmote) {
|
||||
emoteNames = EMOTES.map(({ name }) => name.toLowerCase());
|
||||
const hasMinEmojiLength = suggestionTerm && suggestionTerm.length > EMOJI_MIN_CHARACTERS;
|
||||
// Filter because our emotes are priority from default emojis, like :eggplant:
|
||||
emojiNames = hasMinEmojiLength ? EMOJIS.names.filter((name) => !emoteNames.includes(`:${name}:`)) : [];
|
||||
const emotesAndEmojis = [...emoteNames, ...emojiNames];
|
||||
|
||||
allOptions.push(...emotesAndEmojis);
|
||||
} else {
|
||||
if (canonicalCreatorUri) allOptions.push(canonicalCreatorUri);
|
||||
if (filteredSubs) allOptions.push(...filteredSubs);
|
||||
if (filteredCommentors) allOptions.push(...filteredCommentors);
|
||||
if (filteredTop) allOptions.push(filteredTop);
|
||||
if (filteredSearch) allOptions.push(...filteredSearch);
|
||||
}
|
||||
|
||||
const allOptionsGrouped =
|
||||
allOptions.length > 0
|
||||
? allOptions.map((option) => {
|
||||
const groupName = isEmote
|
||||
? (emoteNames.includes(option) && __('Emotes')) || (emojiNames.includes(option) && __('Emojis'))
|
||||
: (canonicalCreatorUri === option && __('Creator')) ||
|
||||
(filteredSubs && filteredSubs.includes(option) && __('Following')) ||
|
||||
(filteredCommentors && filteredCommentors.includes(option) && __('From Comments')) ||
|
||||
(filteredTop && filteredTop === option && 'Top') ||
|
||||
(filteredSearch && filteredSearch.includes(option) && __('From Search'));
|
||||
|
||||
let emoteLabel;
|
||||
if (isEmote) {
|
||||
// $FlowFixMe
|
||||
emoteLabel = `:${option.replaceAll(':', '')}:`;
|
||||
}
|
||||
|
||||
return {
|
||||
label: emoteLabel || option.replace('lbry://', '').replace('#', ':'),
|
||||
group: groupName,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const allMatches =
|
||||
useSuggestionMatch(
|
||||
suggestionTerm || '',
|
||||
allOptionsGrouped.map(({ label }) => label)
|
||||
) || [];
|
||||
|
||||
/** --------- **/
|
||||
/** Functions **/
|
||||
/** --------- **/
|
||||
|
||||
function handleInputChange(value: string) {
|
||||
onChange({ target: { value } });
|
||||
|
||||
const cursorIndex = inputRef && inputRef.current && inputRef.current.selectionStart;
|
||||
|
||||
const suggestionMatches = value.match(SUGGESTION_REGEX);
|
||||
|
||||
if (!suggestionMatches) {
|
||||
if (suggestionValue) setSuggestionValue(null);
|
||||
return; // Exit here and avoid unnecessary behavior
|
||||
}
|
||||
|
||||
const exec = SUGGESTION_REGEX.exec(value);
|
||||
const groups = exec && exec.groups;
|
||||
const groupValue = groups && Object.keys(groups).find((group) => groups[group]);
|
||||
|
||||
const previousLastIndexes = [];
|
||||
let isEmote = groupValue && groupValue === 'Emote';
|
||||
let currentSuggestionIndex = exec && exec.index;
|
||||
let currentLastIndex = exec && SUGGESTION_REGEX.lastIndex;
|
||||
let currentSuggestionValue =
|
||||
cursorIndex >= currentSuggestionIndex &&
|
||||
cursorIndex <= currentLastIndex &&
|
||||
suggestionMatches &&
|
||||
suggestionMatches[0];
|
||||
|
||||
if (suggestionMatches && suggestionMatches.length > 1) {
|
||||
currentSuggestionValue = suggestionMatches.find((match, index) => {
|
||||
const previousLastIndex = previousLastIndexes[index - 1] || 0;
|
||||
const valueWithoutPrevious = value.substring(previousLastIndex);
|
||||
|
||||
const tempRe = new RegExp(SUGGESTION_REGEX);
|
||||
const tempExec = tempRe.exec(valueWithoutPrevious);
|
||||
const groups = tempExec && tempExec.groups;
|
||||
const groupValue = groups && Object.keys(groups).find((group) => groups[group]);
|
||||
|
||||
if (tempExec) {
|
||||
isEmote = groupValue && groupValue === 'Emote';
|
||||
currentSuggestionIndex = previousLastIndex + tempExec.index;
|
||||
currentLastIndex = previousLastIndex + tempRe.lastIndex;
|
||||
previousLastIndexes.push(currentLastIndex);
|
||||
}
|
||||
|
||||
// the current mention term will be the one on the text cursor's range,
|
||||
// in case of there being more in the same comment message
|
||||
if (previousLastIndexes) {
|
||||
return cursorIndex >= currentSuggestionIndex && cursorIndex <= currentLastIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (currentSuggestionValue) {
|
||||
const token = isEmote ? ':' : '@';
|
||||
const tokenIndex = currentSuggestionValue.indexOf(token);
|
||||
|
||||
// $FlowFixMe
|
||||
setSuggestionValue({
|
||||
beforeTerm: currentSuggestionValue.substring(0, tokenIndex), // in case of a space or newline
|
||||
term: currentSuggestionValue.substring(tokenIndex),
|
||||
index: currentSuggestionIndex,
|
||||
lastIndex: currentLastIndex,
|
||||
isEmote,
|
||||
});
|
||||
} else if (suggestionValue) {
|
||||
setSuggestionValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(selectedValue: string) => {
|
||||
if (!suggestionValue) return;
|
||||
|
||||
const elem = inputRef && inputRef.current;
|
||||
const newCursorPos = suggestionValue.beforeTerm.length + suggestionValue.index + selectedValue.length + 1;
|
||||
|
||||
const contentBegin = messageValue.substring(0, suggestionValue.index);
|
||||
const replaceValue = suggestionValue.beforeTerm + selectedValue;
|
||||
const contentEnd =
|
||||
messageValue.length > suggestionValue.lastIndex
|
||||
? messageValue.substring(suggestionValue.lastIndex, messageValue.length)
|
||||
: ' ';
|
||||
|
||||
const newValue = contentBegin + replaceValue + contentEnd;
|
||||
|
||||
onChange({ target: { value: newValue } });
|
||||
setSuggestionValue(null);
|
||||
elem.focus();
|
||||
elem.setSelectionRange(newCursorPos, newCursorPos);
|
||||
},
|
||||
[messageValue, inputRef, onChange, suggestionValue]
|
||||
);
|
||||
|
||||
/** ------- **/
|
||||
/** Effects **/
|
||||
/** ------- **/
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isMention) return;
|
||||
|
||||
if (isTyping && suggestionTerm && !invalidTerm) {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedTerm(!hasMinLength ? '' : suggestionTerm);
|
||||
}, INPUT_DEBOUNCE_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [hasMinLength, invalidTerm, isMention, isTyping, suggestionTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!stringifiedResults) return;
|
||||
|
||||
const arrayResults = JSON.parse(stringifiedResults);
|
||||
if (debouncedTerm && arrayResults && arrayResults.length > 0) {
|
||||
doResolveUris([debouncedTerm, ...arrayResults]);
|
||||
doSetMentionSearchResults(debouncedTerm, arrayResults);
|
||||
}
|
||||
}, [debouncedTerm, doResolveUris, doSetMentionSearchResults, stringifiedResults, suggestionTerm]);
|
||||
|
||||
// Disable sending on Enter on Livestream chat
|
||||
React.useEffect(() => {
|
||||
if (!isLivestream) return;
|
||||
|
||||
if (suggestionTerm && inputRef) {
|
||||
inputRef.current.setAttribute('term', suggestionTerm);
|
||||
} else {
|
||||
inputRef.current.removeAttribute('term');
|
||||
}
|
||||
}, [inputRef, isLivestream, suggestionTerm]);
|
||||
|
||||
// Only resolve commentors on Livestreams when first trying to mention/looking for it
|
||||
React.useEffect(() => {
|
||||
if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris);
|
||||
}, [commentorUris, doResolveUris, isLivestream, suggestionTerm]);
|
||||
|
||||
// Allow selecting with TAB key
|
||||
React.useEffect(() => {
|
||||
if (!suggestionTerm) return; // only if there is a term, or else can't tab to navigate page
|
||||
|
||||
function handleKeyDown(e: SyntheticKeyboardEvent<*>) {
|
||||
const { keyCode } = e;
|
||||
|
||||
if (highlightedSuggestion && keyCode === KEYCODES.TAB) {
|
||||
e.preventDefault();
|
||||
handleSelect(highlightedSuggestion.label);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleSelect, highlightedSuggestion, suggestionTerm]);
|
||||
|
||||
/** ------ **/
|
||||
/** Render **/
|
||||
/** ------ **/
|
||||
|
||||
const renderGroup = (groupName: string, children: any) => (
|
||||
<div key={groupName} className="textareaSuggestions__group">
|
||||
<label className="textareaSuggestions__label">
|
||||
{groupName === 'Top' ? (
|
||||
<LbcSymbol prefix={__('Winning Search for %matching_term%', { matching_term: searchQuery })} />
|
||||
) : suggestionTerm && suggestionTerm.length > 1 ? (
|
||||
__('%group_name% matching %matching_term%', { group_name: groupName, matching_term: suggestionTerm })
|
||||
) : (
|
||||
groupName
|
||||
)}
|
||||
</label>
|
||||
{children}
|
||||
<hr className="textareaSuggestions__topSeparator" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderInput = (params: any) => {
|
||||
const { InputProps, disabled, fullWidth, id, inputProps: autocompleteInputProps } = params;
|
||||
const inputProps = { ...autocompleteInputProps, ...inputDefaultProps };
|
||||
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
|
||||
|
||||
return <TextField inputRef={inputRef} multiline select={false} {...autocompleteProps} />;
|
||||
};
|
||||
|
||||
const renderOption = (optionProps: any, label: string) => {
|
||||
const emoteFound = isEmote && EMOTES.find(({ name }) => name.toLowerCase() === label);
|
||||
const emoteValue = emoteFound ? { name: label, url: emoteFound.url } : undefined;
|
||||
const emojiFound = isEmote && EMOJIS.getUnicode(label);
|
||||
const emojiValue = emojiFound ? { name: label, unicode: emojiFound } : undefined;
|
||||
|
||||
return <TextareaSuggestionsItem key={label} uri={label} emote={emoteValue || emojiValue} {...optionProps} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
PopperComponent={AutocompletePopper}
|
||||
autoHighlight
|
||||
disableClearable
|
||||
filterOptions={(options) => options.filter(({ label }) => allMatches.includes(label))}
|
||||
freeSolo
|
||||
fullWidth
|
||||
getOptionLabel={(option) => option.label || ''}
|
||||
groupBy={(option) => option.group}
|
||||
id={id}
|
||||
inputValue={messageValue}
|
||||
loading={allMatches.length === 0 || showPlaceholder}
|
||||
loadingText={showPlaceholder ? <BusyIndicator message={__('Searching...')} /> : __('Nothing found')}
|
||||
onBlur={() => onBlur && onBlur()}
|
||||
/* Different from onInputChange, onChange is only used for the selected value,
|
||||
so here it is acting simply as a selection handler (see it as onSelect) */
|
||||
onChange={(event, value) => handleSelect(value.label)}
|
||||
onClose={(event, reason) => reason !== 'selectOption' && setClose(true)}
|
||||
onFocus={() => onFocus && onFocus()}
|
||||
onHighlightChange={(event, option) => setHighlightedSuggestion(option)}
|
||||
onInputChange={(event, value, reason) => reason === 'input' && handleInputChange(value)}
|
||||
onOpen={() => suggestionTerm && setClose(false)}
|
||||
/* 'open' is for the popper box component, set to check for a valid term
|
||||
or else it will be displayed all the time as empty (no options) */
|
||||
open={!!suggestionTerm && !shouldClose}
|
||||
options={allOptionsGrouped}
|
||||
renderGroup={({ group, children }) => renderGroup(group, children)}
|
||||
renderInput={(params) => renderInput(params)}
|
||||
renderOption={(optionProps, option) => renderOption(optionProps, option.label)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AutocompletePopper(props: any) {
|
||||
return <Popper {...props} placement="top" />;
|
||||
}
|
||||
|
||||
function useSuggestionMatch(term: string, list: Array<string>) {
|
||||
const throttledTerm = useThrottle(term);
|
||||
|
||||
return React.useMemo(() => {
|
||||
return !throttledTerm || throttledTerm.trim() === ''
|
||||
? undefined
|
||||
: matchSorter(list, term, { keys: [(item) => item] });
|
||||
}, [list, term, throttledTerm]);
|
||||
}
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { selectVolume, selectMute } from 'redux/selectors/app';
|
||||
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||
import { makeSelectContentPositionForUri, makeSelectIsPlayerFloating, selectPlayingUri } from 'redux/selectors/content';
|
||||
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
||||
import { selectRecommendedContentForUri } from 'redux/selectors/search';
|
||||
import VideoViewer from './view';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
|
@ -41,7 +41,7 @@ const select = (state, props) => {
|
|||
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||
previousListUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||
} else {
|
||||
const recommendedContent = makeSelectRecommendedContentForUri(uri)(state);
|
||||
const recommendedContent = selectRecommendedContentForUri(state, uri);
|
||||
nextRecommendedUri = recommendedContent && recommendedContent[0];
|
||||
}
|
||||
|
||||
|
|
|
@ -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,218 +1,161 @@
|
|||
// @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 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;
|
||||
|
||||
// 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();
|
||||
// icon to use or explainer text to show per tab
|
||||
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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// if user has no balance, used to show conditional frontend
|
||||
const noBalance = balance === 0;
|
||||
/** FUNCTIONS **/
|
||||
|
||||
// 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') {
|
||||
switch (claim.value_type) {
|
||||
case 'stream':
|
||||
return __('Content');
|
||||
} else if (claim.value_type === 'channel') {
|
||||
case 'channel':
|
||||
return __('Channel');
|
||||
} else if (claim.value_type === 'repost') {
|
||||
case 'repost':
|
||||
return __('Repost');
|
||||
} else if (claim.value_type === 'collection') {
|
||||
case 'collection':
|
||||
return __('List');
|
||||
} else {
|
||||
default:
|
||||
return __('Claim');
|
||||
}
|
||||
}
|
||||
const claimTypeText = getClaimTypeText();
|
||||
|
||||
// icon to use or explainer text to show per tab
|
||||
let iconToUse;
|
||||
let explainerText = '';
|
||||
if (activeTab === TAB_BOOST) {
|
||||
iconToUse = ICONS.LBC;
|
||||
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {
|
||||
claimTypeText,
|
||||
});
|
||||
} else if (activeTab === TAB_LBC) {
|
||||
iconToUse = ICONS.LBC;
|
||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
||||
}
|
||||
|
||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||
|
||||
React.useEffect(() => {
|
||||
// Regex for number up to 8 decimal places
|
||||
let regexp;
|
||||
let tipError;
|
||||
|
||||
if (tipAmount === 0) {
|
||||
tipError = __('Amount must be a positive number');
|
||||
} else if (!tipAmount || typeof tipAmount !== 'number') {
|
||||
tipError = __('Amount must be a number');
|
||||
}
|
||||
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
else {
|
||||
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) {
|
||||
if (!tipAmount || !claimId) return;
|
||||
|
||||
// send an instant tip (no need to go to an exchange first)
|
||||
if (instantTipEnabled) {
|
||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||
if (instantTipMax.currency === 'LBC') {
|
||||
sendSupportOrConfirm(instantTipMax.amount);
|
||||
} else {
|
||||
// Need to convert currency of instant purchase maximum before trying to send support
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
||||
sendSupportOrConfirm(instantTipMax.amount / LBC_USD);
|
||||
});
|
||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => sendSupportOrConfirm(instantTipMax.amount / LBC_USD));
|
||||
}
|
||||
// sending fiat tip
|
||||
} 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
|
||||
|
@ -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) {
|
||||
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>
|
||||
|
||||
{/* 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);
|
||||
}}
|
||||
<WalletTipAmountSelector
|
||||
setTipError={setTipError}
|
||||
tipError={tipError}
|
||||
claim={claim}
|
||||
activeTab={TAB_LBC} // active tab
|
||||
amount={tipAmount}
|
||||
onChange={(amount) => setTipAmount(amount)}
|
||||
setDisableSubmitButton={setDisableSubmitButton}
|
||||
/>
|
||||
))}
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 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,76 +1,119 @@
|
|||
// @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,
|
||||
exchangeRate?: any,
|
||||
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,
|
||||
exchangeRate,
|
||||
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 convertToTwoDecimalsOrMore = (number: number, decimals: number = 2) =>
|
||||
Number((Math.round(number * 10 ** decimals) / 10 ** decimals).toFixed(decimals));
|
||||
|
||||
const tipAmountsToDisplay =
|
||||
customTipAmount && fiatConversion && activeTab === TAB_FIAT
|
||||
? [customTipAmount]
|
||||
: customTipAmount && exchangeRate
|
||||
? [convertToTwoDecimalsOrMore(customTipAmount / exchangeRate)]
|
||||
: 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
|
||||
? convertToTwoDecimalsOrMore(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;
|
||||
// parse number as float and sets it in the parent component
|
||||
function handleCustomPriceChange(amount: number) {
|
||||
const tipAmountValue = parseFloat(amount);
|
||||
onChange(tipAmountValue);
|
||||
if (fiatConversion && exchangeRate && setConvertedAmount && convertedAmount !== tipAmountValue * exchangeRate) {
|
||||
setConvertedAmount(tipAmountValue * exchangeRate);
|
||||
}
|
||||
}
|
||||
|
||||
// check if creator has a payment method saved
|
||||
React.useEffect(() => {
|
||||
if (stripeEnvironment) {
|
||||
if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
|
||||
setConvertedAmount(amount * exchangeRate);
|
||||
}
|
||||
}, [amount, convertedAmount, exchangeRate, setConvertedAmount]);
|
||||
|
||||
// check if user has a payment method saved
|
||||
// REMOVE
|
||||
React.useEffect(() => {
|
||||
if (!stripeEnvironment) return;
|
||||
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
|
@ -87,12 +130,12 @@ function WalletTipAmountSelector(props: Props) {
|
|||
|
||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
}, [setHasSavedCard]);
|
||||
|
||||
//
|
||||
// check if creator has a tip account saved REMOVE
|
||||
React.useEffect(() => {
|
||||
if (stripeEnvironment) {
|
||||
if (!stripeEnvironment) return;
|
||||
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
|
@ -108,74 +151,90 @@ function WalletTipAmountSelector(props: Props) {
|
|||
setCanReceiveFiatTip(true);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, channelClaimId, tipChannelName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// setHasSavedCard(false);
|
||||
// setCanReceiveFiatTip(true);
|
||||
|
||||
let regexp,
|
||||
tipError = '';
|
||||
let regexp;
|
||||
|
||||
if (amount === 0) {
|
||||
tipError = __('Amount must be a positive number');
|
||||
setTipError(__('Amount cannot be zero.'));
|
||||
} else if (!amount || typeof amount !== 'number') {
|
||||
tipError = __('Amount must be a number');
|
||||
}
|
||||
|
||||
setTipError(__('Amount must be a number.'));
|
||||
} else {
|
||||
// if it's not fiat, aka it's boost or lbc tip
|
||||
else if (activeTab !== TAB_FIAT) {
|
||||
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');
|
||||
setTipError(__('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');
|
||||
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) {
|
||||
tipError = __('Amount must be higher');
|
||||
setTipError(__('Amount must be higher'));
|
||||
} else if (
|
||||
convertedAmount &&
|
||||
exchangeRate &&
|
||||
customTipAmount &&
|
||||
amount < convertToTwoDecimalsOrMore(customTipAmount / exchangeRate)
|
||||
) {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||
const validCustomTipInput = regexp.test(String(amount));
|
||||
|
||||
if (validCustomTipInput) {
|
||||
setTipError(
|
||||
__('Amount of $%input_amount% LBC in USB is lower than price of $%price_amount%', {
|
||||
input_amount: convertToTwoDecimalsOrMore(convertedAmount, 4),
|
||||
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||
}
|
||||
// if tip fiat tab
|
||||
} else {
|
||||
setTipError(false);
|
||||
}
|
||||
// if tip fiat tab REMOVE
|
||||
} 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');
|
||||
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||
} else if (amount < 1) {
|
||||
tipError = __('Amount must be at least one dollar');
|
||||
setTipError(__('Amount must be at least one dollar'));
|
||||
} else if (amount > 1000) {
|
||||
tipError = __('Amount cannot be over 1000 dollars');
|
||||
setTipError(__('Amount cannot be over 1000 dollars'));
|
||||
} else if (customTipAmount && amount < customTipAmount) {
|
||||
setTipError(
|
||||
__('Amount is lower than price of $%price_amount%', {
|
||||
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setTipError(false);
|
||||
}
|
||||
}
|
||||
|
||||
setTipError(tipError);
|
||||
onTipErrorChange(tipError);
|
||||
}, [amount, balance, setTipError, activeTab]);
|
||||
|
||||
// parse number as float and sets it in the parent component
|
||||
function handleCustomPriceChange(amount: number) {
|
||||
const tipAmount = parseFloat(amount);
|
||||
|
||||
onChange(tipAmount);
|
||||
}
|
||||
}, [activeTab, amount, balance, convertedAmount, customTipAmount, exchangeRate, setTipError]);
|
||||
|
||||
const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
||||
{tipAmountsToDisplay &&
|
||||
tipAmountsToDisplay.map((defaultAmount) => (
|
||||
<Button
|
||||
key={defaultAmount}
|
||||
disabled={shouldDisableAmountSelector(defaultAmount)}
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': defaultAmount === amount && !useCustomTip,
|
||||
'button-toggle--active':
|
||||
convertToTwoDecimalsOrMore(defaultAmount) === convertToTwoDecimalsOrMore(amount) && !useCustomTip,
|
||||
'button-toggle--disabled': amount > balance,
|
||||
})}
|
||||
label={defaultAmount}
|
||||
|
@ -186,9 +245,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 +267,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 LBC amount is ~ $%exchange_amount%.', {
|
||||
exchange_amount: convertToTwoDecimalsOrMore(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 +300,17 @@ function WalletTipAmountSelector(props: Props) {
|
|||
|
||||
{/* lbc tab */}
|
||||
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
||||
{/* fiat button but no card saved */}
|
||||
{!useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
{activeTab === TAB_FIAT &&
|
||||
(!hasCardSaved
|
||||
? getHelpMessage(
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||
{__('Tip Creators')}
|
||||
</span>
|
||||
</div>
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||
{' ' + __('To Tip Creators')}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
)}
|
||||
)
|
||||
: !canReceiveFiatTip
|
||||
? getHelpMessage(__('Only creators that verify cash accounts can receive tips'))
|
||||
: getHelpMessage(__('Send a tip directly from your attached card')))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -232,6 +232,7 @@ export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
|
|||
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
||||
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
|
||||
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
||||
export const SET_MENTION_SEARCH_RESULTS = 'SET_MENTION_SEARCH_RESULTS';
|
||||
|
||||
// Settings
|
||||
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
||||
|
|
96
ui/constants/emotes.js
Normal file
96
ui/constants/emotes.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
// @flow
|
||||
|
||||
const buildCDNUrl = (path: string) => `https://static.odycdn.com/emoticons/${path}`;
|
||||
|
||||
const buildEmote = (name: string, path: string) => ({
|
||||
name: `:${name}:`,
|
||||
url: buildCDNUrl(path),
|
||||
});
|
||||
|
||||
const getEmotes = (px: string, multiplier: string) => [
|
||||
buildEmote('ALIEN', `${px}/Alien${multiplier}.png`),
|
||||
buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`),
|
||||
buildEmote('ANGRY_2', `${px}/angry%202${multiplier}.png`),
|
||||
buildEmote('ANGRY_3', `${px}/angry%203${multiplier}.png`),
|
||||
buildEmote('ANGRY_4', `${px}/angry%204${multiplier}.png`),
|
||||
buildEmote('BLIND', `${px}/blind${multiplier}.png`),
|
||||
buildEmote('BLOCK', `${px}/block${multiplier}.png`),
|
||||
buildEmote('BOMB', `${px}/bomb${multiplier}.png`),
|
||||
buildEmote('BRAIN_CHIP', `${px}/Brain%20chip${multiplier}.png`),
|
||||
buildEmote('CONFIRM', `${px}/CONFIRM${multiplier}.png`),
|
||||
buildEmote('CONFUSED_1', `${px}/confused${multiplier}-1.png`),
|
||||
buildEmote('CONFUSED_2', `${px}/confused${multiplier}.png`),
|
||||
buildEmote('COOKING_SOMETHING_NICE', `${px}/cooking%20something%20nice${multiplier}.png`),
|
||||
buildEmote('CRY_1', `${px}/cry${multiplier}.png`),
|
||||
buildEmote('CRY_2', `${px}/cry%202${multiplier}.png`),
|
||||
buildEmote('CRY_3', `${px}/cry%203${multiplier}.png`),
|
||||
buildEmote('CRY_4', `${px}/cry%204${multiplier}.png`),
|
||||
buildEmote('CRY_5', `${px}/cry%205${multiplier}.png`),
|
||||
buildEmote('DONUT', `${px}/donut${multiplier}.png`),
|
||||
buildEmote('EGGPLANT_WITH_CONDOM', `${px}/eggplant%20with%20condom${multiplier}.png`),
|
||||
buildEmote('EGGPLANT', `${px}/eggplant${multiplier}.png`),
|
||||
buildEmote('FIRE_UP', `${px}/fire%20up${multiplier}.png`),
|
||||
buildEmote('FLAT_EARTH', `${px}/Flat%20earth${multiplier}.png`),
|
||||
buildEmote('FLYING_SAUCER', `${px}/Flying%20saucer${multiplier}.png`),
|
||||
buildEmote('HEART_CHOPPER', `${px}/heart%20chopper${multiplier}.png`),
|
||||
buildEmote('HYPER_TROLL', `${px}/HyperTroll${multiplier}.png`),
|
||||
buildEmote('ICE_CREAM', `${px}/ice%20cream${multiplier}.png`),
|
||||
buildEmote('IDK', `${px}/IDK${multiplier}.png`),
|
||||
buildEmote('ILLUMINATI_1', `${px}/Illuminati${multiplier}-1.png`),
|
||||
buildEmote('ILLUMINATI_2', `${px}/Illuminati${multiplier}.png`),
|
||||
buildEmote('KISS_1', `${px}/kiss${multiplier}.png`),
|
||||
buildEmote('KISS_2', `${px}/kiss%202${multiplier}.png`),
|
||||
buildEmote('LASER_GUN', `${px}/laser%20gun${multiplier}.png`),
|
||||
buildEmote('LAUGHING_1', `${px}/Laughing${multiplier}.png`),
|
||||
buildEmote('LAUGHING_2', `${px}/Laughing 2${multiplier}.png`),
|
||||
buildEmote('LOLLIPOP', `${px}/Lollipop${multiplier}.png`),
|
||||
buildEmote('LOVE_1', `${px}/Love${multiplier}.png`),
|
||||
buildEmote('LOVE_2', `${px}/Love%202${multiplier}.png`),
|
||||
buildEmote('MONSTER', `${px}/Monster${multiplier}.png`),
|
||||
buildEmote('MUSHROOM', `${px}/mushroom${multiplier}.png`),
|
||||
buildEmote('NAIL_IT', `${px}/Nail%20It${multiplier}.png`),
|
||||
buildEmote('NO', `${px}/NO${multiplier}.png`),
|
||||
buildEmote('OUCH', `${px}/ouch${multiplier}.png`),
|
||||
buildEmote('PIZZA', `${px}/pizza${multiplier}.png`),
|
||||
buildEmote('PREACE', `${px}/peace${multiplier}.png`),
|
||||
buildEmote('RABBIT_HOLE', `${px}/rabbit%20hole${multiplier}.png`),
|
||||
buildEmote('RAINBOW_PUKE_1', `${px}/rainbow%20puke${multiplier}-1.png`),
|
||||
buildEmote('RAINBOW_PUKE_2', `${px}/rainbow%20puke${multiplier}.png`),
|
||||
buildEmote('ROCK', `${px}/ROCK${multiplier}.png`),
|
||||
buildEmote('SAD', `${px}/sad${multiplier}.png`),
|
||||
buildEmote('SALTY', `${px}/salty${multiplier}.png`),
|
||||
buildEmote('SCARY', `${px}/scary${multiplier}.png`),
|
||||
buildEmote('SLEEP', `${px}/Sleep${multiplier}.png`),
|
||||
buildEmote('SLIME_DOWN', `${px}/slime%20down${multiplier}.png`),
|
||||
buildEmote('SMELLY_SOCKS', `${px}/smelly%20socks${multiplier}.png`),
|
||||
buildEmote('SMILE_1', `${px}/smile${multiplier}.png`),
|
||||
buildEmote('SMILE_2', `${px}/smile%202${multiplier}.png`),
|
||||
buildEmote('SPACE_CHAD', `${px}/space%20chad${multiplier}.png`),
|
||||
buildEmote('SPACE_DOGE', `${px}/doge${multiplier}.png`),
|
||||
buildEmote('SPACE_GREEN_WOJAK', `${px}/space%20wojak${multiplier}-1.png`),
|
||||
buildEmote('SPACE_JULIAN', `${px}/Space%20Julian${multiplier}.png`),
|
||||
buildEmote('SPACE_RED_WOJAK', `${px}/space%20wojak${multiplier}.png`),
|
||||
buildEmote('SPACE_RESITAS', `${px}/resitas${multiplier}.png`),
|
||||
buildEmote('SPACE_TOM', `${px}/space%20Tom${multiplier}.png`),
|
||||
buildEmote('SPOCK', `${px}/SPOCK${multiplier}.png`),
|
||||
buildEmote('STAR', `${px}/Star${multiplier}.png`),
|
||||
buildEmote('SUNNY_DAY', `${px}/sunny%20day${multiplier}.png`),
|
||||
buildEmote('SUPRISED', `${px}/surprised${multiplier}.png`),
|
||||
buildEmote('SWEET', `${px}/sweet${multiplier}.png`),
|
||||
buildEmote('THINKING_1', `${px}/thinking${multiplier}-1.png`),
|
||||
buildEmote('THINKING_2', `${px}/thinking${multiplier}.png`),
|
||||
buildEmote('THUMB_DOWN', `${px}/thumb%20down${multiplier}.png`),
|
||||
buildEmote('THUMB_UP_1', `${px}/thumb%20up${multiplier}-1.png`),
|
||||
buildEmote('THUMB_UP_2', `${px}/thumb%20up${multiplier}.png`),
|
||||
buildEmote('TINFOIL_HAT', `${px}/tin%20hat${multiplier}.png`),
|
||||
buildEmote('TROLL_KING', `${px}/Troll%20king${multiplier}.png`),
|
||||
buildEmote('UFO', `${px}/ufo${multiplier}.png`),
|
||||
buildEmote('WAITING', `${px}/waiting${multiplier}.png`),
|
||||
buildEmote('WHAT', `${px}/what_${multiplier}.png`),
|
||||
buildEmote('WOODOO_DOLL', `${px}/woodo%20doll${multiplier}.png`),
|
||||
];
|
||||
|
||||
export const EMOTES_24px = getEmotes('24%20px', '');
|
||||
export const EMOTES_36px = getEmotes('36px', '%401.5x');
|
||||
export const EMOTES_48px = getEmotes('48%20px', '%402x');
|
||||
export const EMOTES_72px = getEmotes('72%20px', '%403x');
|
|
@ -178,3 +178,5 @@ export const LIFE = 'Life';
|
|||
export const ARTISTS = 'Artists';
|
||||
export const MYSTERIES = 'Mysteries';
|
||||
export const TECHNOLOGY = 'Technology';
|
||||
export const EMOJI = 'Emoji';
|
||||
export const STICKER = 'Sticker';
|
||||
|
|
139
ui/constants/stickers.js
Normal file
139
ui/constants/stickers.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
// @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 EGIRL = 'EGIRL/PNG/e-girl.png';
|
||||
const BULL_RIDE = 'BULL/PNG/bull-ride.png';
|
||||
const TRAP = 'TRAP/PNG/trap.png';
|
||||
const XMAS = 'SEASONAL/PNG/xmas.png';
|
||||
const ELIMINATED = 'ELIMINATED/PNG/eliminated.png';
|
||||
const TRASH = 'TRASH/PNG/trash.png';
|
||||
const BAN = 'BAN/PNG/ban.png';
|
||||
const KANYE_WEST = 'MISC/PNG/kanye_west.png';
|
||||
const CHE_GUEVARA = 'MISC/PNG/che_guevara.png';
|
||||
const BILL_COSBY = 'MISC/PNG/bill_cosby.png';
|
||||
const KURT_COBAIN = 'MISC/PNG/kurt_cobain.png';
|
||||
const BILL_CLINTON = 'MISC/PNG/bill_clinton.png';
|
||||
const CHRIS_CHAN = 'MISC/PNG/chris_chan.png';
|
||||
const TAYLOR_SWIFT = 'MISC/PNG/taylor_swift.png';
|
||||
const EPSTEIN_ISLAND = 'MISC/PNG/epstein_island.png';
|
||||
const DONALD_TRUMP = 'MISC/PNG/donald_trump.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('XMAS', XMAS),
|
||||
buildSticker('PISS', PISS),
|
||||
buildSticker('BULL_RIDE', BULL_RIDE),
|
||||
buildSticker('ELIMINATED', ELIMINATED),
|
||||
buildSticker('BAN', BAN),
|
||||
buildSticker('EGIRL', EGIRL),
|
||||
buildSticker('KANYE_WEST', KANYE_WEST),
|
||||
buildSticker('TAYLOR_SWIFT', TAYLOR_SWIFT),
|
||||
buildSticker('DONALD_TRUMP', DONALD_TRUMP),
|
||||
buildSticker('BILL_CLINTON', BILL_CLINTON),
|
||||
buildSticker('EPSTEIN_ISLAND', EPSTEIN_ISLAND),
|
||||
buildSticker('KURT_COBAIN', KURT_COBAIN),
|
||||
buildSticker('BILL_COSBY', BILL_COSBY),
|
||||
buildSticker('CHE_GUEVARA', CHE_GUEVARA),
|
||||
buildSticker('CHRIS_CHAN', CHRIS_CHAN),
|
||||
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('TRAP', TRAP),
|
||||
buildSticker('TRASH', TRASH),
|
||||
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, 5),
|
||||
buildSticker('SILVER_ODYSEE_COIN', SILVER_ODYSEE_COIN, 5),
|
||||
buildSticker('LBC_COMET_TIP', LBC_COMET_TIP, 25),
|
||||
buildSticker('SMALL_TIP', SMALL_TIP, 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),
|
||||
];
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
const useEffectOnce = effect => {
|
||||
const useEffectOnce = (effect) => {
|
||||
React.useEffect(effect, []);
|
||||
};
|
||||
|
||||
|
@ -14,7 +14,7 @@ function useUnmount(fn: () => any): void {
|
|||
useEffectOnce(() => () => fnRef.current());
|
||||
}
|
||||
|
||||
export function useThrottle(value: string, ms: number = 200) {
|
||||
export default function useThrottle(value: string, ms: number = 200) {
|
||||
const [state, setState] = React.useState(value);
|
||||
const timeout = React.useRef();
|
||||
const nextValue = React.useRef(null);
|
||||
|
@ -37,7 +37,7 @@ export function useThrottle(value: string, ms: number = 200) {
|
|||
nextValue.current = value;
|
||||
hasNextValue.current = true;
|
||||
}
|
||||
}, [value]);
|
||||
}, [ms, value]);
|
||||
|
||||
useUnmount(() => {
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
|
@ -45,5 +45,3 @@ export function useThrottle(value: string, ms: number = 200) {
|
|||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default useThrottle;
|
||||
|
|
|
@ -87,7 +87,6 @@ function DiscoverPage(props: Props) {
|
|||
icon={ICONS.SUBSCRIBE}
|
||||
iconColor="red"
|
||||
onClick={handleFollowClick}
|
||||
requiresAuth={false}
|
||||
label={label}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -129,6 +129,13 @@ export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptio
|
|||
}
|
||||
};
|
||||
|
||||
export const doSetMentionSearchResults = (query: string, uris: Array<string>) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_MENTION_SEARCH_RESULTS,
|
||||
data: { query, uris },
|
||||
});
|
||||
};
|
||||
|
||||
export const doFetchRecommendedContent = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const claim = selectClaimForUri(state, uri);
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -20,6 +20,8 @@ const defaultState: SearchState = {
|
|||
resultsByQuery: {},
|
||||
hasReachedMaxResultsLength: {},
|
||||
searching: false,
|
||||
results: [],
|
||||
mentionQuery: '',
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
|
@ -66,6 +68,12 @@ export default handleActions(
|
|||
options,
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.SET_MENTION_SEARCH_RESULTS]: (state: SearchState, action: SearchSuccess): SearchState => ({
|
||||
...state,
|
||||
results: action.data.uris,
|
||||
mentionQuery: action.data.query,
|
||||
}),
|
||||
},
|
||||
defaultState
|
||||
);
|
||||
|
|
|
@ -3,11 +3,19 @@ import { createSelector } from 'reselect';
|
|||
import { createCachedSelector } from 're-reselect';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectMentionSearchResults, selectMentionQuery } from 'redux/selectors/search';
|
||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||
import { selectClaimsById, selectMyActiveClaims, selectClaimIdForUri } from 'redux/selectors/claims';
|
||||
import { isClaimNsfw } from 'util/claim';
|
||||
import {
|
||||
selectClaimsById,
|
||||
selectMyClaimIdsRaw,
|
||||
selectMyChannelClaimIds,
|
||||
selectClaimIdForUri,
|
||||
selectClaimIdsByUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { isClaimNsfw, getChannelFromClaim } from 'util/claim';
|
||||
import { selectSubscriptionUris } from 'redux/selectors/subscriptions';
|
||||
|
||||
type State = { comments: CommentsState, claims: any };
|
||||
type State = { claims: any, comments: CommentsState };
|
||||
|
||||
const selectState = (state) => state.comments || {};
|
||||
|
||||
|
@ -51,12 +59,11 @@ export const selectCommentsByUri = createSelector(selectState, (state) => {
|
|||
|
||||
export const selectPinnedCommentsById = (state: State) => selectState(state).pinnedCommentsById;
|
||||
export const selectPinnedCommentsForUri = createCachedSelector(
|
||||
selectCommentsByUri,
|
||||
selectClaimIdForUri,
|
||||
selectCommentsById,
|
||||
selectPinnedCommentsById,
|
||||
(state, uri) => uri,
|
||||
(byUri, byId, pinnedCommentsById, uri) => {
|
||||
const claimId = byUri[uri];
|
||||
(claimId, byId, pinnedCommentsById, uri) => {
|
||||
const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId];
|
||||
const pinnedComments = [];
|
||||
|
||||
|
@ -68,7 +75,7 @@ export const selectPinnedCommentsForUri = createCachedSelector(
|
|||
|
||||
return pinnedComments;
|
||||
}
|
||||
)((state, uri) => uri);
|
||||
)((state, uri) => String(uri));
|
||||
|
||||
export const selectModerationBlockList = createSelector(
|
||||
(state) => selectState(state).moderationBlockList,
|
||||
|
@ -128,7 +135,7 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment
|
|||
return comments;
|
||||
});
|
||||
|
||||
// no superchats?
|
||||
// no superchats
|
||||
export const selectSuperchatsByUri = (state: State) => selectState(state).superChatsByUri;
|
||||
|
||||
export const selectTopLevelCommentsByClaimId = createSelector(
|
||||
|
@ -180,6 +187,7 @@ export const selectCommentIdsForUri = (state: State, uri: string) => {
|
|||
return commentIdsByClaimId[claimId];
|
||||
};
|
||||
|
||||
// deprecated
|
||||
export const makeSelectCommentIdsForUri = (uri: string) =>
|
||||
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
|
||||
const claimId = byUri[uri];
|
||||
|
@ -188,7 +196,8 @@ export const makeSelectCommentIdsForUri = (uri: string) =>
|
|||
|
||||
const filterCommentsDepOnList = {
|
||||
claimsById: selectClaimsById,
|
||||
myClaims: selectMyActiveClaims,
|
||||
myClaimIds: selectMyClaimIdsRaw,
|
||||
myChannelClaimIds: selectMyChannelClaimIds,
|
||||
mutedChannels: selectMutedChannels,
|
||||
personalBlockList: selectModerationBlockList,
|
||||
blacklistedMap: selectBlacklistedOutpointMap,
|
||||
|
@ -206,28 +215,29 @@ export const selectFetchingBlockedWords = (state: State) => selectState(state).f
|
|||
export const selectCommentsForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectCommentsByClaimId,
|
||||
selectCommentsByUri,
|
||||
selectClaimIdForUri,
|
||||
...Object.values(filterCommentsDepOnList),
|
||||
(uri, byClaimId, byUri, ...filterInputs) => {
|
||||
const claimId = byUri[uri];
|
||||
(uri, byClaimId, claimId, ...filterInputs) => {
|
||||
const comments = byClaimId && byClaimId[claimId];
|
||||
return filterComments(comments, claimId, filterInputs);
|
||||
}
|
||||
)((state, uri) => uri);
|
||||
)((state, uri) => String(uri));
|
||||
|
||||
export const selectTopLevelCommentsForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
(state, uri, maxCount) => maxCount,
|
||||
selectTopLevelCommentsByClaimId,
|
||||
selectCommentsByUri,
|
||||
selectClaimIdForUri,
|
||||
...Object.values(filterCommentsDepOnList),
|
||||
(uri, maxCount = -1, byClaimId, byUri, ...filterInputs) => {
|
||||
const claimId = byUri[uri];
|
||||
(uri, maxCount = -1, byClaimId, claimId, ...filterInputs) => {
|
||||
const comments = byClaimId && byClaimId[claimId];
|
||||
const filtered = filterComments(comments, claimId, filterInputs);
|
||||
return maxCount > 0 ? filtered.slice(0, maxCount) : filtered;
|
||||
if (comments) {
|
||||
return filterComments(maxCount > 0 ? comments.slice(0, maxCount) : comments, claimId, filterInputs);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
)((state, uri, maxCount = -1) => `${uri}:${maxCount}`);
|
||||
}
|
||||
)((state, uri, maxCount = -1) => `${String(uri)}:${maxCount}`);
|
||||
|
||||
export const makeSelectTopLevelTotalCommentsForUri = (uri: string) =>
|
||||
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||
|
@ -259,24 +269,25 @@ export const selectRepliesForParentId = createCachedSelector(
|
|||
|
||||
return filterComments(comments, undefined, filterInputs);
|
||||
}
|
||||
)((state, id: string) => id);
|
||||
)((state, id: string) => String(id));
|
||||
|
||||
/**
|
||||
* filterComments
|
||||
*
|
||||
* @param comments List of comments to filter.
|
||||
* @param claimId The claim that `comments` reside in.
|
||||
* @oaram filterInputs Values returned by filterCommentsDepOnList.
|
||||
* @param filterInputs Values returned by filterCommentsDepOnList.
|
||||
*/
|
||||
const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs: any) => {
|
||||
const filterProps = filterInputs.reduce(function (acc, cur, i) {
|
||||
const filterProps = filterInputs.reduce((acc, cur, i) => {
|
||||
acc[filterCommentsPropKeys[i]] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const {
|
||||
claimsById,
|
||||
myClaims,
|
||||
myClaimIds,
|
||||
myChannelClaimIds,
|
||||
mutedChannels,
|
||||
personalBlockList,
|
||||
blacklistedMap,
|
||||
|
@ -295,8 +306,12 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
|
|||
|
||||
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
||||
if (channelClaim) {
|
||||
if (myClaims && myClaims.size > 0) {
|
||||
const claimIsMine = channelClaim.is_my_output || myClaims.has(channelClaim.claim_id);
|
||||
if ((myClaimIds && myClaimIds.size > 0) || (myChannelClaimIds && myChannelClaimIds.length > 0)) {
|
||||
const claimIsMine =
|
||||
channelClaim.is_my_output ||
|
||||
myChannelClaimIds.includes(channelClaim.claim_id) ||
|
||||
myClaimIds.includes(channelClaim.claim_id);
|
||||
// TODO: I believe 'myClaimIds' does not include channels, so it seems wasteful to include it here? ^
|
||||
if (claimIsMine) {
|
||||
return true;
|
||||
}
|
||||
|
@ -316,7 +331,7 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
|
|||
}
|
||||
|
||||
if (claimId) {
|
||||
const claimIdIsMine = myClaims && myClaims.size > 0 && myClaims.has(claimId);
|
||||
const claimIdIsMine = myClaimIds && myClaimIds.size > 0 && myClaimIds.includes(claimId);
|
||||
if (!claimIdIsMine) {
|
||||
if (personalBlockList.includes(comment.channel_url)) {
|
||||
return false;
|
||||
|
@ -376,25 +391,88 @@ export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) =>
|
|||
return blockingByUri[uri] || unBlockingByUri[uri];
|
||||
});
|
||||
|
||||
export const makeSelectSuperChatDataForUri = (uri: string) =>
|
||||
createSelector(selectSuperchatsByUri, (byUri) => {
|
||||
export const selectSuperChatDataForUri = (state: State, uri: string) => {
|
||||
const byUri = selectSuperchatsByUri(state);
|
||||
return byUri[uri];
|
||||
});
|
||||
};
|
||||
|
||||
export const makeSelectSuperChatsForUri = (uri: string) =>
|
||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||
if (!superChatData) {
|
||||
return undefined;
|
||||
export const selectSuperChatsForUri = (state: State, uri: string) => {
|
||||
const superChatData = selectSuperChatDataForUri(state, uri);
|
||||
return superChatData ? superChatData.comments : undefined;
|
||||
};
|
||||
|
||||
export const selectSuperChatTotalAmountForUri = (state: State, uri: string) => {
|
||||
const superChatData = selectSuperChatDataForUri(state, uri);
|
||||
return superChatData ? superChatData.totalAmount : 0;
|
||||
};
|
||||
|
||||
export const selectChannelMentionData = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectClaimIdsByUri,
|
||||
selectClaimsById,
|
||||
selectTopLevelCommentsForUri,
|
||||
selectSubscriptionUris,
|
||||
selectMentionSearchResults,
|
||||
selectMentionQuery,
|
||||
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris, searchUris, query) => {
|
||||
let canonicalCreatorUri;
|
||||
const commentorUris = [];
|
||||
const canonicalCommentors = [];
|
||||
const canonicalSubscriptions = [];
|
||||
const canonicalSearch = [];
|
||||
|
||||
if (uri) {
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
const channelFromClaim = claim && getChannelFromClaim(claim);
|
||||
canonicalCreatorUri = channelFromClaim && channelFromClaim.canonical_url;
|
||||
|
||||
topLevelComments.forEach(({ channel_url: uri }) => {
|
||||
// Check: if there are duplicate commentors
|
||||
if (!commentorUris.includes(uri)) {
|
||||
// Update: commentorUris
|
||||
commentorUris.push(uri);
|
||||
|
||||
// Update: canonicalCommentors
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalCommentors.push(claim.canonical_url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return superChatData.comments;
|
||||
subscriptionUris.forEach((uri) => {
|
||||
// Update: canonicalSubscriptions
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalSubscriptions.push(claim.canonical_url);
|
||||
}
|
||||
});
|
||||
|
||||
export const makeSelectSuperChatTotalAmountForUri = (uri: string) =>
|
||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||
if (!superChatData) {
|
||||
return 0;
|
||||
let hasNewResolvedResults = false;
|
||||
if (searchUris && searchUris.length > 0) {
|
||||
searchUris.forEach((uri) => {
|
||||
// Update: canonicalSubscriptions
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalSearch.push(claim.canonical_url);
|
||||
}
|
||||
});
|
||||
hasNewResolvedResults = canonicalSearch.length > 0;
|
||||
}
|
||||
|
||||
return superChatData.totalAmount;
|
||||
});
|
||||
return {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions,
|
||||
commentorUris,
|
||||
hasNewResolvedResults,
|
||||
query,
|
||||
};
|
||||
}
|
||||
)((state, uri, maxCount) => `${String(uri)}:${maxCount}`);
|
||||
|
|
|
@ -6,13 +6,14 @@ import {
|
|||
selectClaimsByUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectClaimIsNsfw,
|
||||
selectClaimIsNsfwForUri,
|
||||
makeSelectPendingClaimForUri,
|
||||
selectIsUriResolving,
|
||||
} from 'redux/selectors/claims';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { isClaimNsfw } from 'util/claim';
|
||||
import { createSelector } from 'reselect';
|
||||
import { createCachedSelector } from 're-reselect';
|
||||
import { createNormalizedSearchKey, getRecommendationSearchOptions } from 'util/search';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectHistory } from 'redux/selectors/content';
|
||||
|
@ -22,24 +23,16 @@ type State = { claims: any, search: SearchState };
|
|||
|
||||
export const selectState = (state: State): SearchState => state.search;
|
||||
|
||||
export const selectSearchValue: (state: State) => string = createSelector(selectState, (state) => state.searchQuery);
|
||||
|
||||
export const selectSearchOptions: (state: State) => SearchOptions = createSelector(
|
||||
selectState,
|
||||
(state) => state.options
|
||||
);
|
||||
|
||||
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching);
|
||||
|
||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
||||
selectState,
|
||||
(state) => state.resultsByQuery
|
||||
);
|
||||
|
||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector(
|
||||
selectState,
|
||||
(state) => state.hasReachedMaxResultsLength
|
||||
);
|
||||
// $FlowFixMe - 'searchQuery' is never populated. Something lost in a merge?
|
||||
export const selectSearchValue: (state: State) => string = (state) => selectState(state).searchQuery;
|
||||
export const selectSearchOptions: (state: State) => SearchOptions = (state) => selectState(state).options;
|
||||
export const selectIsSearching: (state: State) => boolean = (state) => selectState(state).searching;
|
||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = (state) =>
|
||||
selectState(state).resultsByQuery;
|
||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) =>
|
||||
selectState(state).hasReachedMaxResultsLength;
|
||||
export const selectMentionSearchResults: (state: State) => Array<string> = (state) => selectState(state).results;
|
||||
export const selectMentionQuery: (state: State) => string = (state) => selectState(state).mentionQuery;
|
||||
|
||||
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
|
||||
createSelector(selectSearchResultByQuery, (byQuery) => {
|
||||
|
@ -60,16 +53,16 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
|
|||
return hasReachedMaxResultsLength[query];
|
||||
});
|
||||
|
||||
export const makeSelectRecommendedContentForUri = (uri: string) =>
|
||||
createSelector(
|
||||
export const selectRecommendedContentForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectHistory,
|
||||
selectClaimsByUri,
|
||||
selectShowMatureContent,
|
||||
selectMutedChannels,
|
||||
selectAllCostInfoByUri,
|
||||
selectSearchResultByQuery,
|
||||
makeSelectClaimIsNsfw(uri),
|
||||
(history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
||||
selectClaimIsNsfwForUri, // (state, uri)
|
||||
(uri, history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
||||
const claim = claimsByUri[uri];
|
||||
|
||||
if (!claim) return;
|
||||
|
@ -146,7 +139,7 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
|
|||
}
|
||||
return recommendedContent;
|
||||
}
|
||||
);
|
||||
)((state, uri) => String(uri));
|
||||
|
||||
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
||||
createSelector(
|
||||
|
|
|
@ -20,6 +20,11 @@ export const selectSubscriptions = createSelector(
|
|||
(state) => state.subscriptions && state.subscriptions.sort((a, b) => a.channelName.localeCompare(b.channelName))
|
||||
);
|
||||
|
||||
export const selectSubscriptionUris = createSelector(
|
||||
selectSubscriptions,
|
||||
(subscriptions) => subscriptions && subscriptions.map((sub) => sub.uri)
|
||||
);
|
||||
|
||||
export const selectFollowing = createSelector(selectState, (state) => state.following && state.following);
|
||||
|
||||
// Fetching list of users subscriptions
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
@import 'component/button';
|
||||
@import 'component/card';
|
||||
@import 'component/channel';
|
||||
@import 'component/channel-mention';
|
||||
@import 'component/_textarea-suggestions';
|
||||
@import 'component/claim-list';
|
||||
@import 'component/collection';
|
||||
@import 'component/comments';
|
||||
|
|
|
@ -549,12 +549,6 @@
|
|||
.button--highlighted {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.button--emoji {
|
||||
font-size: 1.1rem;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
.channel-mention {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
bottom: calc(100% - 1.8rem);
|
||||
z-index: 3;
|
||||
font-size: var(--font-small);
|
||||
padding-left: var(--spacing-s);
|
||||
|
||||
> .icon {
|
||||
top: 0;
|
||||
left: var(--spacing-m);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
stroke: var(--color-input-placeholder);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-mention__suggestions {
|
||||
@extend .card;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
position: absolute;
|
||||
text-overflow: ellipsis;
|
||||
width: 22rem;
|
||||
z-index: 3;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
box-shadow: var(--card-box-shadow);
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom: none;
|
||||
|
||||
.channel-mention__label:first-of-type {
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.channel-mention__suggestions[flow-bottom] {
|
||||
top: 4rem;
|
||||
bottom: auto;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top: none;
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom: auto;
|
||||
}
|
||||
|
||||
.channel-mention__input--none {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.channel-mention__label {
|
||||
@extend .wunderbar__label;
|
||||
}
|
||||
|
||||
.channel-mention__top-separator {
|
||||
@extend .wunderbar__top-separator;
|
||||
}
|
||||
|
||||
.channel-mention__suggestion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-xxs);
|
||||
margin-left: var(--spacing-xxs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(2.1rem);
|
||||
position: absolute;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
@include handleChannelGif(2.1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-mention__suggestion-label {
|
||||
@extend .wunderbar__suggestion-label;
|
||||
margin-left: var(--spacing-m);
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channel-mention__suggestion-name {
|
||||
@extend .wunderbar__suggestion-name;
|
||||
margin-left: calc(var(--spacing-l) - var(--spacing-xxs));
|
||||
}
|
||||
|
||||
.channel-mention__suggestion-title {
|
||||
@extend .wunderbar__suggestion-title;
|
||||
margin-left: calc(var(--spacing-l) - var(--spacing-xxs));
|
||||
}
|
||||
|
||||
.channel-mention__placeholder-suggestion {
|
||||
@extend .wunderbar__placeholder-suggestion;
|
||||
padding: 0 var(--spacing-xxs);
|
||||
margin-left: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.channel-mention__placeholder-label {
|
||||
@extend .wunderbar__placeholder-label;
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
.channel-mention__placeholder-thumbnail {
|
||||
@extend .wunderbar__placeholder-thumbnail;
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
.channel-mention__placeholder-info {
|
||||
@extend .wunderbar__placeholder-info;
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
121
ui/scss/component/_comment-create.scss
Normal file
121
ui/scss/component/_comment-create.scss
Normal file
|
@ -0,0 +1,121 @@
|
|||
@import '../init/vars';
|
||||
|
||||
$thumbnailWidth: 1.5rem;
|
||||
$thumbnailWidthSmall: 1rem;
|
||||
|
||||
.create__comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
fieldset-section,
|
||||
.form-field--SimpleMDE {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-field__two-column {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate--nestedReply {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc((#{$thumbnailWidth} + var(--spacing-m)) * 2 + var(--spacing-m) + 4px);
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate--bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.commentCreate__labelWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.credit-amount:not(:last-child) {
|
||||
&::after {
|
||||
margin-left: var(--spacing-xxs);
|
||||
content: '/';
|
||||
}
|
||||
}
|
||||
|
||||
.credit-amount:not(:first-child) {
|
||||
margin-left: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,29 +32,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
fieldset-section,
|
||||
.form-field--SimpleMDE {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-field__two-column {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__create--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment__create--bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
@ -90,10 +67,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.content_comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment__thumbnail-wrapper {
|
||||
flex: 0;
|
||||
margin-top: var(--spacing-xxs);
|
||||
|
@ -136,24 +109,10 @@ $thumbnailWidthSmall: 1rem;
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.comment__sc-preview {
|
||||
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;
|
||||
}
|
||||
|
||||
.comment__edit-input {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.comment__sc-preview-amount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
.comment__threadline {
|
||||
@extend .button--alt;
|
||||
height: auto;
|
||||
|
@ -173,26 +132,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment-new__label-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-new__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comment--highlighted {
|
||||
background: var(--color-comment-highlighted);
|
||||
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
||||
|
@ -429,8 +368,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
@extend .comment__action;
|
||||
}
|
||||
|
||||
.comment__action--nested,
|
||||
.comment__create--nested-reply {
|
||||
.comment__action--nested {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
|
@ -477,20 +415,10 @@ $thumbnailWidthSmall: 1rem;
|
|||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__tip-input {
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.comment--blocked {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.comment--min-amount-notice {
|
||||
.icon {
|
||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||
}
|
||||
}
|
||||
|
||||
.comments-own {
|
||||
.section__actions {
|
||||
align-items: flex-start;
|
||||
|
@ -547,3 +475,19 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticker__comment {
|
||||
margin-left: var(--spacing-m);
|
||||
height: 6rem;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.emote {
|
||||
max-width: 1.5rem;
|
||||
max-height: 1.5rem;
|
||||
}
|
||||
|
|
40
ui/scss/component/_emote-selector.scss
Normal file
40
ui/scss/component/_emote-selector.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
@import '../init/vars';
|
||||
|
||||
.emoteSelector {
|
||||
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);
|
||||
}
|
||||
|
||||
.emoteSelector__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
max-height: 25vh;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.emoteSelector__listRowItems {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.button--file-action {
|
||||
margin: var(--spacing-xxs);
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
.button__content {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
span {
|
||||
margin: auto;
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
}
|
|
@ -448,9 +448,7 @@ fieldset-group {
|
|||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
float: right;
|
||||
font-size: var(--font-xsmall);
|
||||
margin-top: 2.5%;
|
||||
}
|
||||
|
||||
.form-field__textarea-info {
|
||||
|
@ -462,12 +460,6 @@ fieldset-group {
|
|||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.form-field__quick-emojis {
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section {
|
||||
.form-field__internal-option {
|
||||
margin-top: var(--spacing-s);
|
||||
|
|
|
@ -428,6 +428,7 @@
|
|||
max-width: 32rem;
|
||||
}
|
||||
|
||||
// maybe remove all REMOVE
|
||||
.main-wrapper--scrollbar {
|
||||
// The W3C future standard; currently supported by Firefox only.
|
||||
// It'll hopefully auto fallback to this when 'webkit-scrollbar' below is deprecated in the future.
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
}
|
||||
|
||||
// Image
|
||||
img:not(.channel-thumbnail__custom) {
|
||||
img:not(.channel-thumbnail__custom):not(.emote) {
|
||||
margin-bottom: var(--spacing-m);
|
||||
padding-top: var(--spacing-m);
|
||||
max-height: var(--inline-player-max-height);
|
||||
|
|
|
@ -13,9 +13,15 @@ $contentMaxWidth: 60rem;
|
|||
&:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
&:hover {
|
||||
background-color: var(--color-card-background-highlighted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment__create,
|
||||
.commentCreate,
|
||||
.comment__content {
|
||||
margin: var(--spacing-m);
|
||||
margin-bottom: 0;
|
||||
|
@ -25,7 +31,7 @@ $contentMaxWidth: 60rem;
|
|||
.notification__icon {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: auto;
|
||||
margin-top: var(--spacing-xxs);
|
||||
|
||||
.icon__wrapper {
|
||||
width: 1rem;
|
||||
|
@ -94,7 +100,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.notification__content-wrapper {
|
||||
.notificationContent__wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -121,7 +127,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.notification__content-thumbnail {
|
||||
.notificationContent__thumbnail {
|
||||
@include thumbnail;
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
|
@ -139,8 +145,13 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.notification__text-wrapper {
|
||||
.notificationText__wrapper {
|
||||
max-width: calc(#{$contentMaxWidth} - (#{$thumbnailWidth} * 16 / 9) - var(--spacing-m));
|
||||
|
||||
.sticker__comment {
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.notification__title {
|
||||
|
@ -247,7 +258,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.notification__mark-seen {
|
||||
.notification__markSeen {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
ui/scss/component/_textarea-suggestions.scss
Normal file
92
ui/scss/component/_textarea-suggestions.scss
Normal file
|
@ -0,0 +1,92 @@
|
|||
.MuiAutocomplete-inputRoot {
|
||||
padding: 0 !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
font-size: inherit !important;
|
||||
color: var(--color-text) !important;
|
||||
|
||||
.MuiOutlinedInput-notchedOutline {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.create__comment {
|
||||
@extend textarea;
|
||||
|
||||
min-height: calc(var(--height-input) * 1.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiAutocomplete-paper {
|
||||
@extend .card;
|
||||
background-color: var(--color-card-background);
|
||||
box-shadow: var(--card-box-shadow);
|
||||
color: var(--color-text) !important;
|
||||
|
||||
.textareaSuggestions__group {
|
||||
&:last-child hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.textareaSuggestions__label {
|
||||
@extend .wunderbar__label;
|
||||
}
|
||||
|
||||
.Mui-focused {
|
||||
background-color: var(--color-menu-background--active);
|
||||
}
|
||||
}
|
||||
|
||||
> .icon {
|
||||
top: 0;
|
||||
left: var(--spacing-m);
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
stroke: var(--color-input-placeholder);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiAutocomplete-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-xxs);
|
||||
margin: 0 var(--spacing-xxs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(2.1rem);
|
||||
margin-right: 0;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
@include handleChannelGif(2.1rem);
|
||||
}
|
||||
}
|
||||
|
||||
.textareaSuggestion__label {
|
||||
@extend .wunderbar__suggestion-label;
|
||||
margin-left: var(--spacing-m);
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
.textareaSuggestion__title {
|
||||
@extend .wunderbar__suggestion-title;
|
||||
}
|
||||
|
||||
.textareaSuggestion__value {
|
||||
@extend .wunderbar__suggestion-name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textareaSuggestions__topSeparator {
|
||||
@extend .wunderbar__top-separator;
|
||||
}
|
||||
|
||||
.MuiAutocomplete-loading {
|
||||
color: var(--color-text) !important;
|
||||
}
|
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;
|
||||
}
|
|
@ -28,6 +28,27 @@ body {
|
|||
font-weight: 400;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
|
||||
// The W3C future standard; currently supported by Firefox only.
|
||||
// It'll hopefully auto fallback to this when 'webkit-scrollbar' below is deprecated in the future.
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: var(--color-scrollbar-thumb-bg) var(--color-scrollbar-track-bg);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-track {
|
||||
background: var(--color-scrollbar-track-bg);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-thumb {
|
||||
// Don't set 'border-radius' because Firefox's 'scrollbar-xx'
|
||||
// standard currently doesn't support it. Stick with square
|
||||
// scrollbar for all browsers.
|
||||
background-color: var(--color-scrollbar-thumb-bg);
|
||||
}
|
||||
|
||||
hr {
|
||||
|
|
|
@ -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>', '');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
125
ui/util/remark-emote.js
Normal file
125
ui/util/remark-emote.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||
import visit from 'unist-util-visit';
|
||||
|
||||
const EMOTE_NODE_TYPE = 'emote';
|
||||
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
||||
|
||||
// ***************************************************************************
|
||||
// Tokenize emote
|
||||
// ***************************************************************************
|
||||
|
||||
function findNextEmote(value, fromIndex, strictlyFromIndex) {
|
||||
let begin = 0;
|
||||
|
||||
while (begin < value.length) {
|
||||
const match = value.substring(begin).match(RE_EMOTE);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
match.index += begin;
|
||||
|
||||
if (strictlyFromIndex && match.index !== fromIndex) {
|
||||
if (match.index > fromIndex) {
|
||||
// Already gone past desired index. Skip the rest.
|
||||
return null;
|
||||
} else {
|
||||
// Next match might fit 'fromIndex'.
|
||||
begin = match.index + match[0].length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (fromIndex > 0 && fromIndex > match.index && fromIndex < match.index + match[0].length) {
|
||||
// Skip previously-rejected word
|
||||
// This assumes that a non-zero 'fromIndex' means that a previous lookup has failed.
|
||||
begin = match.index + match[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const str = match[0];
|
||||
|
||||
if (EMOTES.some(({ name }) => str.toUpperCase() === name)) {
|
||||
// Profit!
|
||||
return { text: str, index: match.index };
|
||||
}
|
||||
|
||||
if (strictlyFromIndex && match.index >= fromIndex) {
|
||||
return null; // Since it failed and we've gone past the desired index, skip the rest.
|
||||
}
|
||||
|
||||
begin = match.index + match[0].length;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function locateEmote(value, fromIndex) {
|
||||
const emote = findNextEmote(value, fromIndex, false);
|
||||
return emote ? emote.index : -1;
|
||||
}
|
||||
|
||||
// Generate 'emote' markdown node
|
||||
const createEmoteNode = (text) => ({
|
||||
type: EMOTE_NODE_TYPE,
|
||||
value: text,
|
||||
children: [{ type: 'text', value: text }],
|
||||
});
|
||||
|
||||
// Generate a markdown image from emote
|
||||
function tokenizeEmote(eat, value, silent) {
|
||||
if (silent) return true;
|
||||
|
||||
const emote = findNextEmote(value, 0, true);
|
||||
if (emote) {
|
||||
try {
|
||||
const text = emote.text;
|
||||
return eat(text)(createEmoteNode(text));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
tokenizeEmote.locator = locateEmote;
|
||||
|
||||
export function inlineEmote() {
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.inlineTokenizers;
|
||||
const methods = Parser.prototype.inlineMethods;
|
||||
|
||||
// Add an inline tokenizer (defined in the following example).
|
||||
tokenizers.emote = tokenizeEmote;
|
||||
|
||||
// Run it just before `text`.
|
||||
methods.splice(methods.indexOf('text'), 0, 'emote');
|
||||
}
|
||||
|
||||
// ***************************************************************************
|
||||
// Format emote
|
||||
// ***************************************************************************
|
||||
|
||||
const transformer = (node, index, parent) => {
|
||||
if (node.type === EMOTE_NODE_TYPE && parent && parent.type === 'paragraph') {
|
||||
const emoteStr = node.value;
|
||||
const emote = EMOTES.find(({ name }) => emoteStr.toUpperCase() === name);
|
||||
|
||||
node.type = 'image';
|
||||
node.url = emote.url;
|
||||
node.title = emoteStr;
|
||||
node.children = [{ type: 'text', value: emoteStr }];
|
||||
if (!node.data || !node.data.hProperties) {
|
||||
// Create new node data
|
||||
node.data = {
|
||||
hProperties: { emote: true },
|
||||
};
|
||||
} else if (node.data.hProperties) {
|
||||
// Don't overwrite current attributes
|
||||
node.data.hProperties = {
|
||||
emote: true,
|
||||
...node.data.hProperties,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const transform = (tree) => visit(tree, [EMOTE_NODE_TYPE], transformer);
|
||||
|
||||
export const formattedEmote = () => transform;
|
|
@ -149,7 +149,7 @@ const transform = (tree) => {
|
|||
visit(tree, ['link'], visitor);
|
||||
};
|
||||
|
||||
export const formatedLinks = () => transform;
|
||||
export const formattedLinks = () => transform;
|
||||
|
||||
// Main module
|
||||
export function inlineLinks() {
|
||||
|
|
711
web/scss/themes/odysee/component/_form-field.scss
Normal file
711
web/scss/themes/odysee/component/_form-field.scss
Normal file
|
@ -0,0 +1,711 @@
|
|||
@import '../init/mixins';
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
.date-picker-input {
|
||||
height: var(--height-input);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid;
|
||||
color: var(--color-input);
|
||||
border-color: var(--color-input-border);
|
||||
background-color: var(--color-input-bg);
|
||||
padding-right: var(--spacing-s);
|
||||
padding-left: var(--spacing-s);
|
||||
|
||||
&:focus {
|
||||
@include focus;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-input-placeholder);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
|
||||
& + label {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&[type='range'] {
|
||||
height: auto;
|
||||
height: 0.5rem;
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
checkbox-element,
|
||||
radio-element,
|
||||
select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: var(--select-toggle-background);
|
||||
background-position: 99% center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1rem;
|
||||
padding-right: var(--spacing-l);
|
||||
padding-left: var(--spacing-s);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
fieldset-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
&.fieldset-group--smushed {
|
||||
fieldset-section + fieldset-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section,
|
||||
fieldset-group,
|
||||
form,
|
||||
.checkbox,
|
||||
.radio,
|
||||
.form-field--SimpleMDE,
|
||||
.form-field__help {
|
||||
+ fieldset-section,
|
||||
+ fieldset-group,
|
||||
+ form,
|
||||
+ .checkbox,
|
||||
+ .radio,
|
||||
+ .form-field--SimpleMDE {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
+ .form-field__help {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section,
|
||||
.checkbox,
|
||||
.radio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--font-small);
|
||||
color: var(--color-input-label);
|
||||
display: inline-block;
|
||||
margin-bottom: 0.1rem;
|
||||
|
||||
.icon__lbc {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
input-submit {
|
||||
display: flex;
|
||||
|
||||
& > *:first-child,
|
||||
& > *:nth-child(2) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > *:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
& > *:nth-child(2) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox,
|
||||
.radio {
|
||||
position: relative;
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
height: var(--height-checkbox);
|
||||
width: var(--height-checkbox);
|
||||
position: absolute;
|
||||
border: none;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
|
||||
&:disabled + label {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-size: var(--font-base);
|
||||
padding-left: calc(var(--height-checkbox) + var(--spacing-s));
|
||||
min-height: var(--height-checkbox);
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-input-toggle-bg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
background-color: var(--color-input-toggle-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label::before,
|
||||
label::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
// Hide the checkmark by default
|
||||
input[type='checkbox'] + label::after,
|
||||
input[type='radio'] + label::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
// Unhide on the checked state
|
||||
input[type='checkbox']:checked + label::after,
|
||||
input[type='radio']:checked + label::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
input[type='checkbox']:focus + label::before,
|
||||
input[type='radio']:focus + label::before {
|
||||
@include focus;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
// Outer box of the fake checkbox
|
||||
label::before {
|
||||
height: var(--height-checkbox);
|
||||
width: var(--height-checkbox);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: var(--border-radius);
|
||||
left: 0px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
// Checkmark of the fake checkbox
|
||||
label::after {
|
||||
height: 6px;
|
||||
width: 12px;
|
||||
border-left: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-color: var(--color-input-toggle);
|
||||
border-left-color: var(--color-input-toggle);
|
||||
transform: rotate(-45deg);
|
||||
left: 6px;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
input[type='radio'] {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
// Outer box of the fake radio
|
||||
label::before {
|
||||
height: var(--height-radio);
|
||||
width: var(--height-radio);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: calc(var(--height-radio) * 0.5);
|
||||
left: 0px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
// Checkmark of the fake radio
|
||||
label::after {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary);
|
||||
left: 6px;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.range__label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
> * {
|
||||
width: 33%;
|
||||
text-align: center;
|
||||
|
||||
&:first-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fieldset-group {
|
||||
@extend fieldset-group;
|
||||
}
|
||||
|
||||
.fieldset-section {
|
||||
@extend fieldset-section;
|
||||
}
|
||||
|
||||
.input-submit {
|
||||
@extend input-submit;
|
||||
}
|
||||
|
||||
input-submit {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
fieldset-group {
|
||||
+ fieldset-group {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
&.fieldset-group--smushed {
|
||||
justify-content: flex-start;
|
||||
|
||||
fieldset-section {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
|
||||
&:first-child {
|
||||
input,
|
||||
select {
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
input,
|
||||
select {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fieldgroup--paginate {
|
||||
padding-bottom: var(--spacing-l);
|
||||
margin-top: var(--spacing-l);
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a special case where the prefix appears "inside" the input
|
||||
// It would be way simpler to just use position: absolute and give it a width
|
||||
// but the width can change when we use it for the name prefix
|
||||
// lbry:// {input}, lbry://@short {input}, @lbry://longername {input}
|
||||
// The spacing/alignment isn't very robust and will probably need to be changed
|
||||
// if we use this in more places
|
||||
&.fieldset-group--disabled-prefix {
|
||||
align-items: flex-end;
|
||||
|
||||
label {
|
||||
min-height: 18px;
|
||||
white-space: nowrap;
|
||||
// Set width 0 and overflow visible so the label can act as if it's the input label and not a random text node in a side by side div
|
||||
overflow: visible;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
fieldset-section:first-child {
|
||||
max-width: 40%;
|
||||
|
||||
.form-field__prefix {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem;
|
||||
height: var(--height-input);
|
||||
border: 1px solid;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-color: var(--color-input-border);
|
||||
border-right-color: var(--color-input-prefix-border);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-input-prefix-bg);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section:last-child {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
// Overwrite the input's label to wrap instead. This is usually
|
||||
// an error message, which could be long in other languages.
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
input {
|
||||
border-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-color: var(--color-input-border);
|
||||
padding-left: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-field--copyable {
|
||||
padding: 0.2rem 0.75rem;
|
||||
text-overflow: ellipsis;
|
||||
user-select: text;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.form-field--short {
|
||||
width: 100%;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field--price-amount {
|
||||
max-width: 6em;
|
||||
}
|
||||
|
||||
.form-field--price-amount--auto {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.form-field--address {
|
||||
min-width: 18em;
|
||||
@media (max-width: $breakpoint-xxsmall) {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__help {
|
||||
@extend .help;
|
||||
}
|
||||
|
||||
.form-field__help + .checkbox,
|
||||
.form-field__help + .radio {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.form-field__conjuction {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.form-field__two-column {
|
||||
@media (min-width: $breakpoint-small) {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
.form-field__textarea-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-xxs);
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.form-field__quick-emojis {
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section {
|
||||
.form-field__internal-option {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: 2.2rem;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: var(--spacing-s); // Extra specificity needed here since _section.scss is applied after this file
|
||||
}
|
||||
}
|
||||
|
||||
.select--slim {
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
select {
|
||||
max-height: 1.5rem !important;
|
||||
padding: 0 var(--spacing-xs);
|
||||
padding-right: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#automatic_dark_mode_range_start,
|
||||
#automatic_dark_mode_range_end {
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.date-picker-input {
|
||||
font-weight: bold;
|
||||
|
||||
.react-datetime-picker__wrapper {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-date-picker {
|
||||
margin-bottom: var(--spacing-l);
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
|
||||
.date-picker-input,
|
||||
.button--link {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datetime-picker__button {
|
||||
svg {
|
||||
stroke: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datetime-picker__button:enabled:hover .react-datetime-picker__button__icon,
|
||||
.react-datetime-picker__button:enabled:focus .react-datetime-picker__button__icon {
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
|
||||
.react-date-picker__calendar {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.react-calendar {
|
||||
width: 350px;
|
||||
max-width: 100%;
|
||||
background: var(--color-card-background);
|
||||
border: 1px solid #a0a096;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer {
|
||||
display: flex;
|
||||
margin: -0.5em;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer > * {
|
||||
width: 50%;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar,
|
||||
.react-calendar *,
|
||||
.react-calendar *:before,
|
||||
.react-calendar *:after {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 1px;
|
||||
}
|
||||
|
||||
.react-calendar button {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-calendar button:enabled:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-calendar__navigation {
|
||||
height: 44px;
|
||||
margin-bottom: 1em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button {
|
||||
min-width: 44px;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button:enabled:hover,
|
||||
.react-calendar__navigation button:enabled:focus {
|
||||
background: var(--color-button-alt-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button[disabled] {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 0.75em;
|
||||
color: var(--color-text-alt);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays__weekday {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekNumbers {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekNumbers .react-calendar__tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75em;
|
||||
padding: calc(0.75em / 0.75) calc(0.5em / 0.75);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day,
|
||||
.react-calendar__month-view__days__day--weekend {
|
||||
color: var(--color-text);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
|
||||
.react-calendar__year-view .react-calendar__tile,
|
||||
.react-calendar__decade-view .react-calendar__tile,
|
||||
.react-calendar__century-view .react-calendar__tile {
|
||||
padding: 2em 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__tile {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.75em 0.5em;
|
||||
background: none;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__tile:enabled:hover,
|
||||
.react-calendar__tile:enabled:focus {
|
||||
background: var(--color-button-alt-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
background: var(--color-button-secondary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:enabled:hover,
|
||||
.react-calendar__tile--now:enabled:focus {
|
||||
background: var(--color-button-secondary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive {
|
||||
color: var(--color-button-primary-text);
|
||||
background: var(--color-button-primary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:enabled:hover,
|
||||
.react-calendar__tile--hasActive:enabled:focus {
|
||||
background: var(--color-button-primary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--active {
|
||||
color: var(--color-button-primary-text);
|
||||
background: var(--color-button-primary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:enabled:hover,
|
||||
.react-calendar__tile--active:enabled:focus {
|
||||
background: var(--color-button-primary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar--selectRange .react-calendar__tile--hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__amPm {
|
||||
background: var(--color-input-bg);
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__leadingZero {
|
||||
// Not perfect, but good enough for our standard zoom levels.
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__input--hasLeadingZero {
|
||||
margin-left: -0.54em;
|
||||
padding-left: calc(1px + 0.54em);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-calendar {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
margin-left: calc(var(--spacing-xs) * -1);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
box-shadow: 3px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
336
yarn.lock
336
yarn.lock
|
@ -275,6 +275,13 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.12.5"
|
||||
|
||||
"@babel/helper-module-imports@^7.12.13":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3"
|
||||
integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.0"
|
||||
|
||||
"@babel/helper-module-transforms@^7.11.0":
|
||||
version "7.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
|
||||
|
@ -438,7 +445,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
|
||||
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.14.9":
|
||||
"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.15.7":
|
||||
version "7.15.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
|
||||
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
|
||||
|
@ -679,6 +686,13 @@
|
|||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.8.0"
|
||||
|
||||
"@babel/plugin-syntax-jsx@^7.12.13":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.0.tgz#f9624394317365a9a88c82358d3f8471154698f1"
|
||||
integrity sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
|
||||
"@babel/plugin-syntax-jsx@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94"
|
||||
|
@ -1160,6 +1174,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7":
|
||||
version "7.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.10.1", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
|
||||
|
@ -1257,6 +1278,14 @@
|
|||
"@babel/helper-validator-identifier" "^7.14.9"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba"
|
||||
integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.15.7"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@datapunt/matomo-tracker-js@^0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@datapunt/matomo-tracker-js/-/matomo-tracker-js-0.1.4.tgz#1226f0964d2c062bf9392e9c2fd89838262b10df"
|
||||
|
@ -1291,6 +1320,107 @@
|
|||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-2.0.1.tgz#810cbc595a21f0f94641eb2d7e8264063a3f84de"
|
||||
integrity sha512-bGX4/yB2bPZwXm1DsxgoABgH0Cz7oFtXJgkerB8VrStYdTyvhGAULzNLRn9rVmeAuC3VUDXaXpZIlZAZHpsLIA==
|
||||
|
||||
"@emotion/babel-plugin@^11.3.0":
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz#3a16850ba04d8d9651f07f3fb674b3436a4fb9d7"
|
||||
integrity sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.12.13"
|
||||
"@babel/plugin-syntax-jsx" "^7.12.13"
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@emotion/hash" "^0.8.0"
|
||||
"@emotion/memoize" "^0.7.5"
|
||||
"@emotion/serialize" "^1.0.2"
|
||||
babel-plugin-macros "^2.6.1"
|
||||
convert-source-map "^1.5.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
find-root "^1.1.0"
|
||||
source-map "^0.5.7"
|
||||
stylis "^4.0.3"
|
||||
|
||||
"@emotion/cache@^11.6.0":
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.6.0.tgz#65fbdbbe4382f1991d8b20853c38e63ecccec9a1"
|
||||
integrity sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.7.4"
|
||||
"@emotion/sheet" "^1.1.0"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
"@emotion/weak-memoize" "^0.2.5"
|
||||
stylis "^4.0.10"
|
||||
|
||||
"@emotion/hash@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
|
||||
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||
|
||||
"@emotion/is-prop-valid@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz#cbd843d409dfaad90f9404e7c0404c55eae8c134"
|
||||
integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.7.4"
|
||||
|
||||
"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
|
||||
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
|
||||
|
||||
"@emotion/react@^11.6.0":
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.6.0.tgz#61fcb95c1e01255734c2c721cb9beabcf521eb0f"
|
||||
integrity sha512-23MnRZFBN9+D1lHXC5pD6z4X9yhPxxtHr6f+iTGz6Fv6Rda0GdefPrsHL7otsEf+//7uqCdT5QtHeRxHCERzuw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@emotion/cache" "^11.6.0"
|
||||
"@emotion/serialize" "^1.0.2"
|
||||
"@emotion/sheet" "^1.1.0"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
"@emotion/weak-memoize" "^0.2.5"
|
||||
hoist-non-react-statics "^3.3.1"
|
||||
|
||||
"@emotion/serialize@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
|
||||
integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==
|
||||
dependencies:
|
||||
"@emotion/hash" "^0.8.0"
|
||||
"@emotion/memoize" "^0.7.4"
|
||||
"@emotion/unitless" "^0.7.5"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@emotion/sheet@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
|
||||
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
|
||||
|
||||
"@emotion/styled@^11.6.0":
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d"
|
||||
integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@emotion/babel-plugin" "^11.3.0"
|
||||
"@emotion/is-prop-valid" "^1.1.1"
|
||||
"@emotion/serialize" "^1.0.2"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
|
||||
"@emotion/unitless@^0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
|
||||
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
|
||||
|
||||
"@emotion/utils@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af"
|
||||
integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==
|
||||
|
||||
"@emotion/weak-memoize@^0.2.5":
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@gar/promisify@^1.0.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
|
||||
|
@ -1321,6 +1451,85 @@
|
|||
tough-cookie "^2.3.1"
|
||||
tough-cookie-web-storage-store "^1.0.0"
|
||||
|
||||
"@mui/base@5.0.0-alpha.57":
|
||||
version "5.0.0-alpha.57"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.57.tgz#1f9bc74da67eec8fbad54402b28c1356ec7c53ae"
|
||||
integrity sha512-UCJthNc4LGttoD/CxdCh8AaEu2B2uWNRW96J6PjlQ125+FEqO7+wuIGT98BNCGguVwetK/jTmo/fiHYDoW9gUA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@emotion/is-prop-valid" "^1.1.1"
|
||||
"@mui/utils" "^5.2.1"
|
||||
"@popperjs/core" "^2.4.4"
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
"@mui/material@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.2.1.tgz#ef75df33da37ed6de1aadf66d2e33fea9685cd0d"
|
||||
integrity sha512-y38+e1Qf95rVQ4lK8knYj4o1kB/WwJU0f/lMNmzlaenqGpyhd1M/e3BNwuYEDOLSPWUVeP2LvX2mL/IhKytA9A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@mui/base" "5.0.0-alpha.57"
|
||||
"@mui/system" "^5.2.1"
|
||||
"@mui/types" "^7.1.0"
|
||||
"@mui/utils" "^5.2.1"
|
||||
"@types/react-transition-group" "^4.4.4"
|
||||
clsx "^1.1.1"
|
||||
csstype "^3.0.10"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
react-transition-group "^4.4.2"
|
||||
|
||||
"@mui/private-theming@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.2.1.tgz#07537a065d752a0d6309ce0db42378f104d1885d"
|
||||
integrity sha512-+OfgeZzEjqwd7Vo1kYISJyLHM+3yUO8UoKhLMtZ1DAlZlqovN6jetPtT6o4BnHEAsc3YC3DET+KicwkRtuvxbw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@mui/utils" "^5.2.1"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/styled-engine@^5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.2.0.tgz#5c97e2b1b6c4c2d9991f07517ed862972d362b85"
|
||||
integrity sha512-NZ4pWYQcM5wreUfiXRd7IMFRF+Nq1vMzsIdXtXNjgctJTKHunrofasoBqv+cqevO+hqT75ezSbNHyaXzOXp6Mg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@emotion/cache" "^11.6.0"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/system@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.2.1.tgz#593e9e8d8ab3dd8946f4e98e5ad7feabbcdb8e80"
|
||||
integrity sha512-C1mva6Uyk2bGCaa/FiaFseSt2iJymxgA8KnJJyKAz8ZQZzuetUV8JbY1qtV9CG1VlJb+Ldm7pc6Px8t59lGfZw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@mui/private-theming" "^5.2.1"
|
||||
"@mui/styled-engine" "^5.2.0"
|
||||
"@mui/types" "^7.1.0"
|
||||
"@mui/utils" "^5.2.1"
|
||||
clsx "^1.1.1"
|
||||
csstype "^3.0.10"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/types@^7.1.0":
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.0.tgz#5ed928c5a41cfbf9a4be82ea3bbdc47bcc9610d5"
|
||||
integrity sha512-Hh7ALdq/GjfIwLvqH3XftuY3bcKhupktTm+S6qRIDGOtPtRuq2L21VWzOK4p7kblirK0XgGVH5BLwa6u8z/6QQ==
|
||||
|
||||
"@mui/utils@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.2.1.tgz#fdd70067f8fb2d73225d56f2e705afacdeb3255b"
|
||||
integrity sha512-JQH5ucBxBrubntrN2mvDcwkXlWaHuZGz5goxg9ixnZXRhlZ9Ed5knfsafrX4OFyXNT48DiZXaTRAEkqjyfCExQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@types/prop-types" "^15.7.4"
|
||||
"@types/react-is" "^16.7.1 || ^17.0.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
|
@ -1441,6 +1650,11 @@
|
|||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
|
||||
|
||||
"@popperjs/core@^2.4.4":
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
|
||||
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
|
||||
|
||||
"@reach/auto-id@0.12.1":
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.12.1.tgz#2e4a7250d2067ec16a9b4ea732695bc75572405c"
|
||||
|
@ -1794,6 +2008,16 @@
|
|||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||
|
||||
"@types/prop-types@*", "@types/prop-types@^15.7.4":
|
||||
version "15.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||
|
||||
"@types/q@^1.5.1":
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
||||
|
@ -1803,6 +2027,34 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/react-calendar/-/react-calendar-3.1.3.tgz#bd0947c28738f6419649be22d80624b05fde2fb9"
|
||||
integrity sha512-4kvDfKta9bNnuRieuGYPxdDlh3UqRUKE8+fMbmZGk0Z/MdUGHupxXwPCWLbVH7FZU48o4bhT+XX8rfZrexdnAw==
|
||||
|
||||
"@types/react-is@^16.7.1 || ^17.0.0":
|
||||
version "17.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
|
||||
integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.4.4":
|
||||
version "4.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
|
||||
integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*":
|
||||
version "17.0.37"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959"
|
||||
integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||
|
||||
"@types/semver@^7.1.0":
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.1.0.tgz#c8c630d4c18cd326beff77404887596f96408408"
|
||||
|
@ -2564,6 +2816,15 @@ babel-plugin-import-glob@^2.0.0:
|
|||
identifierfy "^1.1.0"
|
||||
minimatch-capture "^1.1.0"
|
||||
|
||||
babel-plugin-macros@^2.6.1:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
|
||||
integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.7.2"
|
||||
cosmiconfig "^6.0.0"
|
||||
resolve "^1.12.0"
|
||||
|
||||
babel-plugin-syntax-object-rest-spread@^6.8.0:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
|
||||
|
@ -3430,6 +3691,11 @@ clsx@^1.0.4:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
@ -3677,6 +3943,13 @@ content-type@~1.0.4:
|
|||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
|
||||
convert-source-map@^1.5.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
|
||||
integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.1"
|
||||
|
||||
convert-source-map@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
|
||||
|
@ -3767,6 +4040,17 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.0.2, cosmiconfig@^5.2.1:
|
|||
js-yaml "^3.13.1"
|
||||
parse-json "^4.0.0"
|
||||
|
||||
cosmiconfig@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
|
||||
integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
|
||||
dependencies:
|
||||
"@types/parse-json" "^4.0.0"
|
||||
import-fresh "^3.1.0"
|
||||
parse-json "^5.0.0"
|
||||
path-type "^4.0.0"
|
||||
yaml "^1.7.2"
|
||||
|
||||
country-data@^0.0.31:
|
||||
version "0.0.31"
|
||||
resolved "https://registry.yarnpkg.com/country-data/-/country-data-0.0.31.tgz#80966b8e1d147fa6d6a589d32933f8793774956d"
|
||||
|
@ -4076,6 +4360,11 @@ csso@^4.0.2:
|
|||
dependencies:
|
||||
css-tree "1.0.0-alpha.37"
|
||||
|
||||
csstype@^3.0.10, csstype@^3.0.2:
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
|
||||
integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
|
||||
|
||||
currency-symbol-map@~2:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/currency-symbol-map/-/currency-symbol-map-2.2.0.tgz#2b3c1872ff1ac2ce595d8273e58e1fff0272aea2"
|
||||
|
@ -4393,6 +4682,14 @@ dom-converter@^0.2:
|
|||
dependencies:
|
||||
utila "~0.4"
|
||||
|
||||
dom-helpers@^5.0.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^3.0.2"
|
||||
|
||||
dom-scroll-into-view@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz#e8f36732dd089b0201a88d7815dc3f88e6d66c7e"
|
||||
|
@ -6329,7 +6626,7 @@ hoek@4.x.x:
|
|||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
|
||||
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
|
||||
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
dependencies:
|
||||
|
@ -6650,6 +6947,14 @@ import-fresh@^3.0.0:
|
|||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
import-fresh@^3.1.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
||||
dependencies:
|
||||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
import-from@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
|
||||
|
@ -10010,6 +10315,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2:
|
|||
version "16.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"
|
||||
|
||||
react-is@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
|
@ -10103,6 +10413,16 @@ react-time-picker@^4.2.0:
|
|||
react-fit "^1.0.3"
|
||||
update-input-width "^1.1.1"
|
||||
|
||||
react-transition-group@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
|
||||
integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@^16.8.2:
|
||||
version "16.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||
|
@ -11192,7 +11512,7 @@ source-map-url@^0.4.0:
|
|||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
||||
|
||||
source-map@^0.5.0, source-map@^0.5.6:
|
||||
source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
||||
|
@ -11580,6 +11900,11 @@ stylehacks@^4.0.0:
|
|||
postcss "^7.0.0"
|
||||
postcss-selector-parser "^3.0.0"
|
||||
|
||||
stylis@^4.0.10, stylis@^4.0.3:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
|
||||
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
|
||||
|
||||
sumchecker@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
|
||||
|
@ -13095,6 +13420,11 @@ yallist@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yaml@^1.7.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yargs-parser@^11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
||||
|
|
Loading…
Reference in a new issue