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 = {
|
declare type SearchState = {
|
||||||
options: SearchOptions,
|
options: SearchOptions,
|
||||||
resultsByQuery: {},
|
resultsByQuery: {},
|
||||||
|
results: Array<string>,
|
||||||
hasReachedMaxResultsLength: {},
|
hasReachedMaxResultsLength: {},
|
||||||
searching: boolean,
|
searching: boolean,
|
||||||
|
mentionQuery: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type SearchSuccess = {
|
declare type SearchSuccess = {
|
||||||
|
@ -41,6 +43,7 @@ declare type SearchSuccess = {
|
||||||
size: number,
|
size: number,
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
recsys: 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'"
|
"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": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.6.0",
|
||||||
|
"@emotion/styled": "^11.6.0",
|
||||||
|
"@mui/material": "^5.2.1",
|
||||||
"@electron/remote": "^2.0.1",
|
"@electron/remote": "^2.0.1",
|
||||||
"@ungap/from-entries": "^0.2.1",
|
"@ungap/from-entries": "^0.2.1",
|
||||||
"auto-launch": "^5.0.5",
|
"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.",
|
"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)",
|
"Content: Limit (GB)": "Content: Limit (GB)",
|
||||||
"Network: Allow (GB)": "Network: Allow (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",
|
"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",
|
"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--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,8 +68,8 @@ type Props = {
|
||||||
syncLoop: (?boolean) => void,
|
syncLoop: (?boolean) => void,
|
||||||
currentModal: any,
|
currentModal: any,
|
||||||
syncFatalError: boolean,
|
syncFatalError: boolean,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelId: ?string,
|
||||||
myChannelUrls: ?Array<string>,
|
myChannelClaimIds: ?Array<string>,
|
||||||
subscriptions: Array<Subscription>,
|
subscriptions: Array<Subscription>,
|
||||||
setActiveChannelIfNotSet: () => void,
|
setActiveChannelIfNotSet: () => void,
|
||||||
setIncognito: (boolean) => void,
|
setIncognito: (boolean) => void,
|
||||||
|
@ -103,8 +103,8 @@ function App(props: Props) {
|
||||||
syncLoop,
|
syncLoop,
|
||||||
currentModal,
|
currentModal,
|
||||||
syncFatalError,
|
syncFatalError,
|
||||||
myChannelUrls,
|
myChannelClaimIds,
|
||||||
activeChannelClaim,
|
activeChannelId,
|
||||||
setActiveChannelIfNotSet,
|
setActiveChannelIfNotSet,
|
||||||
setIncognito,
|
setIncognito,
|
||||||
fetchModBlockedList,
|
fetchModBlockedList,
|
||||||
|
@ -125,6 +125,7 @@ function App(props: Props) {
|
||||||
const { pathname, search } = props.location;
|
const { pathname, search } = props.location;
|
||||||
const [upgradeNagClosed, setUpgradeNagClosed] = useState(false);
|
const [upgradeNagClosed, setUpgradeNagClosed] = useState(false);
|
||||||
const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
|
const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
|
||||||
|
// const [retryingSync, setRetryingSync] = useState(false);
|
||||||
const [sidebarOpen] = usePersistedState('sidebar', true);
|
const [sidebarOpen] = usePersistedState('sidebar', true);
|
||||||
const showUpgradeButton =
|
const showUpgradeButton =
|
||||||
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
|
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
|
||||||
|
@ -135,10 +136,10 @@ function App(props: Props) {
|
||||||
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
|
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
|
||||||
const userId = user && user.id;
|
const userId = user && user.id;
|
||||||
const useCustomScrollbar = !IS_MAC;
|
const useCustomScrollbar = !IS_MAC;
|
||||||
const hasMyChannels = myChannelUrls && myChannelUrls.length > 0;
|
const hasMyChannels = myChannelClaimIds && myChannelClaimIds.length > 0;
|
||||||
const hasNoChannels = myChannelUrls && myChannelUrls.length === 0;
|
const hasNoChannels = myChannelClaimIds && myChannelClaimIds.length === 0;
|
||||||
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
||||||
const hasActiveChannelClaim = activeChannelClaim !== undefined;
|
const hasActiveChannelClaim = activeChannelId !== undefined;
|
||||||
const isPersonalized = hasVerifiedEmail;
|
const isPersonalized = hasVerifiedEmail;
|
||||||
const renderFiledrop = isAuthenticated;
|
const renderFiledrop = isAuthenticated;
|
||||||
|
|
||||||
|
@ -152,7 +153,7 @@ function App(props: Props) {
|
||||||
if (!uploadCount) return;
|
if (!uploadCount) return;
|
||||||
const handleBeforeUnload = (event) => {
|
const handleBeforeUnload = (event) => {
|
||||||
event.preventDefault();
|
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);
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
@ -272,7 +273,6 @@ function App(props: Props) {
|
||||||
}
|
}
|
||||||
}, [previousRewardApproved, isRewardApproved]);
|
}, [previousRewardApproved, isRewardApproved]);
|
||||||
|
|
||||||
// @if TARGET='app'
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (updatePreferences && getWalletSyncPref && readyForPrefs) {
|
if (updatePreferences && getWalletSyncPref && readyForPrefs) {
|
||||||
getWalletSyncPref()
|
getWalletSyncPref()
|
||||||
|
@ -282,7 +282,6 @@ function App(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [updatePreferences, getWalletSyncPref, setReadyForSync, readyForPrefs, hasVerifiedEmail]);
|
}, [updatePreferences, getWalletSyncPref, setReadyForSync, readyForPrefs, hasVerifiedEmail]);
|
||||||
// @endif
|
|
||||||
|
|
||||||
// ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too.
|
// ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too.
|
||||||
useEffect(() => {
|
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 CommentMenuList from 'component/commentMenuList';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import { parseSticker } from 'util/comments';
|
||||||
|
|
||||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||||
|
|
||||||
|
@ -130,6 +132,7 @@ function Comment(props: Props) {
|
||||||
const totalLikesAndDislikes = likesCount + dislikesCount;
|
const totalLikesAndDislikes = likesCount + dislikesCount;
|
||||||
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
||||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||||
|
const stickerFromMessage = parseSticker(message);
|
||||||
|
|
||||||
let channelOwnerOfContent;
|
let channelOwnerOfContent;
|
||||||
try {
|
try {
|
||||||
|
@ -324,6 +327,10 @@ function Comment(props: Props) {
|
||||||
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
||||||
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
||||||
</div>
|
</div>
|
||||||
|
) : stickerFromMessage ? (
|
||||||
|
<div className="sticker__comment">
|
||||||
|
<OptimizedImage src={stickerFromMessage.url} waitLoad loading="lazy" />
|
||||||
|
</div>
|
||||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||||
<Expandable>
|
<Expandable>
|
||||||
<MarkdownPreview
|
<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,
|
selectFetchingMyChannels,
|
||||||
makeSelectTagInClaimOrChannelForUri,
|
makeSelectTagInClaimOrChannelForUri,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { doSendTip } from 'redux/actions/wallet';
|
import { CommentCreate } from './view';
|
||||||
|
import { DISABLE_SUPPORT_TAG } from 'constants/tags';
|
||||||
import { doCommentCreate, doFetchCreatorSettings, doCommentById } from 'redux/actions/comments';
|
import { doCommentCreate, doFetchCreatorSettings, doCommentById } from 'redux/actions/comments';
|
||||||
|
import { doSendTip } from 'redux/actions/wallet';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectSettingsByChannelId } from 'redux/selectors/comments';
|
import { selectSettingsByChannelId } from 'redux/selectors/comments';
|
||||||
import { CommentCreate } from './view';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
import { DISABLE_SUPPORT_TAG } from 'constants/tags';
|
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const claim = selectClaimForUri(state, props.uri);
|
const claim = selectClaimForUri(state, props.uri);
|
||||||
|
@ -28,12 +28,12 @@ const select = (state, props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
|
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment, sticker) =>
|
||||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment)),
|
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment, sticker)),
|
||||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
|
||||||
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||||
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
fetchComment: (commentId) => dispatch(doCommentById(commentId, false)),
|
fetchComment: (commentId) => dispatch(doCommentById(commentId, false)),
|
||||||
|
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentCreate);
|
export default connect(select, perform)(CommentCreate);
|
||||||
|
|
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
94
ui/component/commentCreate/sticker-selector.jsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// @flow
|
||||||
|
import 'scss/component/_sticker-selector.scss';
|
||||||
|
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const buildStickerSideLink = (section: string, icon: string) => ({ section, icon });
|
||||||
|
|
||||||
|
const STICKER_SIDE_LINKS = [
|
||||||
|
buildStickerSideLink(__('Free'), ICONS.TAG),
|
||||||
|
buildStickerSideLink(__('Tips'), ICONS.FINANCE),
|
||||||
|
// Future work may include Channel, Subscriptions, ...
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = { claimIsMine: boolean, onSelect: (any) => void };
|
||||||
|
|
||||||
|
export default function StickerSelector(props: Props) {
|
||||||
|
const { claimIsMine, onSelect } = props;
|
||||||
|
|
||||||
|
function scrollToStickerSection(section: string) {
|
||||||
|
const listBodyEl = document.querySelector('.stickerSelector__listBody');
|
||||||
|
const sectionToScroll = document.getElementById(section);
|
||||||
|
|
||||||
|
if (listBodyEl && sectionToScroll) {
|
||||||
|
// $FlowFixMe
|
||||||
|
listBodyEl.scrollTo({
|
||||||
|
top: sectionToScroll.offsetTop - sectionToScroll.getBoundingClientRect().height * 2,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getListRow = (rowTitle: string, rowStickers: any) => (
|
||||||
|
<div className="stickerSelector__listBody-row">
|
||||||
|
<div id={rowTitle} className="stickerSelector__listBody-rowTitle">
|
||||||
|
{rowTitle}
|
||||||
|
</div>
|
||||||
|
<div className="stickerSelector__listBody-rowItems">
|
||||||
|
{rowStickers.map((sticker) => (
|
||||||
|
<Button
|
||||||
|
key={sticker.name}
|
||||||
|
title={sticker.name}
|
||||||
|
button="alt"
|
||||||
|
className="button--file-action"
|
||||||
|
onClick={() => onSelect(sticker)}
|
||||||
|
>
|
||||||
|
<OptimizedImage src={sticker.url} waitLoad 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
|
// @flow
|
||||||
|
|
||||||
|
import 'scss/component/_comment-create.scss';
|
||||||
|
|
||||||
|
import { buildValidSticker } from 'util/comments';
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
|
@ -8,158 +12,142 @@ import * as ICONS from 'constants/icons';
|
||||||
import * as KEYCODES from 'constants/keycodes';
|
import * as KEYCODES from 'constants/keycodes';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ChannelMentionSuggestions from 'component/channelMentionSuggestions';
|
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import EmoteSelector from './emote-selector';
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
|
import FilePrice from 'component/filePrice';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SelectChannel from 'component/selectChannel';
|
import SelectChannel from 'component/selectChannel';
|
||||||
|
import StickerSelector from './sticker-selector';
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
const TAB_FIAT = 'TabFiat';
|
const TAB_FIAT = 'TabFiat';
|
||||||
const TAB_LBC = 'TabLBC';
|
const TAB_LBC = 'TabLBC';
|
||||||
const MENTION_DEBOUNCE_MS = 100;
|
|
||||||
|
// for sendCashTip REMOVE
|
||||||
|
// type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
|
||||||
|
// type UserParams = { activeChannelName: ?string, activeChannelId: ?string };
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
|
||||||
claim: StreamClaim,
|
|
||||||
hasChannels: boolean,
|
|
||||||
isNested: boolean,
|
|
||||||
isFetchingChannels: boolean,
|
|
||||||
parentId: string,
|
|
||||||
isReply: boolean,
|
|
||||||
activeChannel: string,
|
activeChannel: string,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
bottom: boolean,
|
bottom: boolean,
|
||||||
embed?: boolean,
|
hasChannels: boolean,
|
||||||
|
claim: StreamClaim,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
supportDisabled: boolean,
|
isFetchingChannels: boolean,
|
||||||
|
isNested: boolean,
|
||||||
|
isReply: boolean,
|
||||||
|
parentId: string,
|
||||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||||
shouldFetchComment: boolean,
|
shouldFetchComment: boolean,
|
||||||
doToast: ({ message: string }) => void,
|
supportDisabled: boolean,
|
||||||
createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
|
uri: string,
|
||||||
onDoneReplying?: () => void,
|
createComment: (string, string, string, ?string, ?string, ?string, ?boolean) => Promise<any>,
|
||||||
onCancelReplying?: () => void,
|
|
||||||
toast: (string) => void,
|
|
||||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
|
||||||
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
||||||
setQuickReply: (any) => void,
|
doToast: ({ message: string }) => void,
|
||||||
fetchComment: (commentId: string) => Promise<any>,
|
fetchComment: (commentId: string) => Promise<any>,
|
||||||
|
onCancelReplying?: () => void,
|
||||||
|
onDoneReplying?: () => void,
|
||||||
|
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||||
|
setQuickReply: (any) => void,
|
||||||
|
toast: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommentCreate(props: Props) {
|
export function CommentCreate(props: Props) {
|
||||||
const {
|
const {
|
||||||
uri,
|
|
||||||
claim,
|
|
||||||
hasChannels,
|
|
||||||
isNested,
|
|
||||||
isFetchingChannels,
|
|
||||||
isReply,
|
|
||||||
parentId,
|
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
bottom,
|
bottom,
|
||||||
|
hasChannels,
|
||||||
|
claim,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
|
isFetchingChannels,
|
||||||
|
isNested,
|
||||||
|
isReply,
|
||||||
|
parentId,
|
||||||
settingsByChannelId,
|
settingsByChannelId,
|
||||||
supportDisabled,
|
|
||||||
shouldFetchComment,
|
shouldFetchComment,
|
||||||
doToast,
|
supportDisabled,
|
||||||
createComment,
|
createComment,
|
||||||
onDoneReplying,
|
|
||||||
onCancelReplying,
|
|
||||||
sendTip,
|
|
||||||
doFetchCreatorSettings,
|
doFetchCreatorSettings,
|
||||||
setQuickReply,
|
doToast,
|
||||||
fetchComment,
|
fetchComment,
|
||||||
|
onCancelReplying,
|
||||||
|
onDoneReplying,
|
||||||
|
sendTip,
|
||||||
|
setQuickReply,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const formFieldRef: ElementRef<any> = React.useRef();
|
const formFieldRef: ElementRef<any> = React.useRef();
|
||||||
const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
|
||||||
const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart;
|
|
||||||
const buttonRef: ElementRef<any> = React.useRef();
|
const buttonRef: ElementRef<any> = React.useRef();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setSubmitting] = React.useState(false);
|
||||||
const [commentFailure, setCommentFailure] = React.useState(false);
|
const [commentFailure, setCommentFailure] = React.useState(false);
|
||||||
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
|
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
|
||||||
const [isSupportComment, setIsSupportComment] = React.useState();
|
const [isSupportComment, setIsSupportComment] = React.useState();
|
||||||
const [isReviewingSupportComment, setIsReviewingSupportComment] = React.useState();
|
const [isReviewingSupportComment, setReviewingSupportComment] = React.useState();
|
||||||
|
const [isReviewingStickerComment, setReviewingStickerComment] = React.useState();
|
||||||
|
const [selectedSticker, setSelectedSticker] = React.useState();
|
||||||
const [tipAmount, setTipAmount] = React.useState(1);
|
const [tipAmount, setTipAmount] = React.useState(1);
|
||||||
|
const [convertedAmount, setConvertedAmount] = React.useState();
|
||||||
const [commentValue, setCommentValue] = React.useState('');
|
const [commentValue, setCommentValue] = React.useState('');
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const [activeTab, setActiveTab] = React.useState('');
|
const [stickerSelector, setStickerSelector] = React.useState();
|
||||||
|
const [activeTab, setActiveTab] = React.useState();
|
||||||
const [tipError, setTipError] = React.useState();
|
const [tipError, setTipError] = React.useState();
|
||||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||||
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||||
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
||||||
|
const [exchangeRate, setExchangeRate] = React.useState();
|
||||||
const selectedMentionIndex =
|
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
|
||||||
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 claimId = claim && claim.claim_id;
|
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 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 channelId = getChannelIdFromClaim(claim);
|
||||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||||
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
|
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
|
||||||
const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0;
|
const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0;
|
||||||
const minAmount = minTip || minSuper || 0;
|
const minAmount = minTip || minSuper || 0;
|
||||||
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
||||||
|
const stickerPrice = selectedSticker && selectedSticker.price;
|
||||||
|
|
||||||
const minAmountRef = React.useRef(minAmount);
|
const minAmountRef = React.useRef(minAmount);
|
||||||
minAmountRef.current = minAmount;
|
minAmountRef.current = minAmount;
|
||||||
|
|
||||||
const MinAmountNotice = minAmount ? (
|
|
||||||
<div className="help--notice comment--min-amount-notice">
|
|
||||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
|
||||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
|
||||||
</I18nMessage>
|
|
||||||
<Icon
|
|
||||||
customTooltipText={
|
|
||||||
minTip
|
|
||||||
? __('This channel requires a minimum tip for each comment.')
|
|
||||||
: minSuper
|
|
||||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
className="icon--help"
|
|
||||||
icon={ICONS.HELP}
|
|
||||||
tooltip
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// Functions
|
// Functions
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
|
function handleSelectSticker(sticker: any) {
|
||||||
|
// $FlowFixMe
|
||||||
|
setSelectedSticker(sticker);
|
||||||
|
setReviewingStickerComment(true);
|
||||||
|
setTipAmount(sticker.price || 0);
|
||||||
|
setStickerSelector(false);
|
||||||
|
|
||||||
|
if (sticker.price && sticker.price > 0) {
|
||||||
|
setActiveTab(TAB_LBC);
|
||||||
|
setIsSupportComment(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleCommentChange(event) {
|
function handleCommentChange(event) {
|
||||||
let commentValue;
|
let commentValue;
|
||||||
if (isReply) {
|
if (isReply) {
|
||||||
|
@ -171,19 +159,6 @@ export function CommentCreate(props: Props) {
|
||||||
setCommentValue(commentValue);
|
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<*>) {
|
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
if ((e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -199,16 +174,8 @@ export function CommentCreate(props: Props) {
|
||||||
window.removeEventListener('keydown', altEnterListener);
|
window.removeEventListener('keydown', altEnterListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
if (activeChannelClaim && commentValue.length) {
|
|
||||||
handleCreateComment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSupportComment() {
|
function handleSupportComment() {
|
||||||
if (!activeChannelClaim) {
|
if (!activeChannelClaim) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelId) {
|
if (!channelId) {
|
||||||
doToast({
|
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.'),
|
message: __('The creator just updated the minimum setting. Please revise or double-check your tip amount.'),
|
||||||
isError: true,
|
isError: true,
|
||||||
});
|
});
|
||||||
setIsReviewingSupportComment(false);
|
setReviewingSupportComment(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,33 +212,18 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSubmitTip() {
|
function doSubmitTip() {
|
||||||
if (!activeChannelClaim) {
|
if (!activeChannelClaim || isSubmitting) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
setSubmitting(true);
|
||||||
amount: tipAmount,
|
|
||||||
claim_id: claimId,
|
|
||||||
channel_id: activeChannelClaim.claim_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
const params = { amount: tipAmount, claim_id: claimId, channel_id: activeChannelClaim.claim_id };
|
||||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
// FIAT ONLY - REMOVE
|
||||||
|
// const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
|
// const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
|
|
||||||
// setup variables for tip API
|
// setup variables for tip API
|
||||||
let channelClaimId, tipChannelName;
|
// const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||||
// if there is a signing channel it's on a file
|
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||||
if (claim.signing_channel) {
|
|
||||||
channelClaimId = claim.signing_channel.claim_id;
|
|
||||||
tipChannelName = claim.signing_channel.name;
|
|
||||||
|
|
||||||
// otherwise it's on the channel page
|
|
||||||
} else {
|
|
||||||
channelClaimId = claim.claim_id;
|
|
||||||
tipChannelName = claim.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
if (activeTab === TAB_LBC) {
|
if (activeTab === TAB_LBC) {
|
||||||
// call sendTip and then run the callback from the response
|
// call sendTip and then run the callback from the response
|
||||||
|
@ -286,72 +238,34 @@ export function CommentCreate(props: Props) {
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
doToast({
|
doToast({
|
||||||
message: __(
|
message: __("You sent %tipAmount% Credits as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||||
"You sent %tipAmount% LBRY Credits as a tip to %tipChannelName%, I'm sure they appreciate it!",
|
tipAmount: tipAmount, // force show decimal places
|
||||||
{
|
tipChannelName,
|
||||||
tipAmount: tipAmount, // force show decimal places
|
}),
|
||||||
tipChannelName,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setSuccessTip({ txid, tipAmount });
|
setSuccessTip({ txid, tipAmount });
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
// reset the frontend so people can send a new comment
|
// reset the frontend so people can send a new comment
|
||||||
setIsSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const sourceClaimId = claim.claim_id;
|
// No cash tips - REMOVE
|
||||||
const roundedAmount = Math.round(tipAmount * 100) / 100;
|
// const tipParams: TipParams = { tipAmount: Math.round(tipAmount * 100) / 100, tipChannelName, channelClaimId };
|
||||||
|
// const userParams: UserParams = { activeChannelName, activeChannelId };
|
||||||
Lbryio.call(
|
// sendCashTip(tipParams, userParams, claim.claim_id, stripeEnvironment, (customerTipResponse) => {
|
||||||
'customer',
|
// const { payment_intent_id } = customerTipResponse;
|
||||||
'tip',
|
//
|
||||||
{
|
// handleCreateComment(null, payment_intent_id, stripeEnvironment);
|
||||||
// round to deal with floating point precision
|
//
|
||||||
amount: Math.round(100 * roundedAmount), // convert from dollars to cents
|
// setCommentValue('');
|
||||||
creator_channel_name: tipChannelName, // creator_channel_name
|
// setReviewingSupportComment(false);
|
||||||
creator_channel_claim_id: channelClaimId,
|
// setIsSupportComment(false);
|
||||||
tipper_channel_name: activeChannelName,
|
// setCommentFailure(false);
|
||||||
tipper_channel_claim_id: activeChannelId,
|
// setSubmitting(false);
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,16 +276,21 @@ export function CommentCreate(props: Props) {
|
||||||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||||
*/
|
*/
|
||||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
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) => {
|
.then((res) => {
|
||||||
setIsSubmitting(false);
|
setSubmitting(false);
|
||||||
if (setQuickReply) setQuickReply(res);
|
if (setQuickReply) setQuickReply(res);
|
||||||
|
|
||||||
if (res && res.signature) {
|
if (res && res.signature) {
|
||||||
setCommentValue('');
|
if (!stickerValue) setCommentValue('');
|
||||||
setIsReviewingSupportComment(false);
|
setReviewingSupportComment(false);
|
||||||
setIsSupportComment(false);
|
setIsSupportComment(false);
|
||||||
setCommentFailure(false);
|
setCommentFailure(false);
|
||||||
|
|
||||||
|
@ -381,7 +300,7 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setIsSubmitting(false);
|
setSubmitting(false);
|
||||||
setCommentFailure(true);
|
setCommentFailure(true);
|
||||||
|
|
||||||
if (channelId) {
|
if (channelId) {
|
||||||
|
@ -412,22 +331,72 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
}, [fetchComment, shouldFetchComment, parentId]);
|
}, [fetchComment, shouldFetchComment, parentId]);
|
||||||
|
|
||||||
// Debounce for disabling the submit button when mentioning a user with Enter
|
// Stickers: Get LBC-USD exchange rate if hasn't yet and selected a paid sticker
|
||||||
// so that the comment isn't sent at the same time
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
if (stickerPrice && !exchangeRate) Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD));
|
||||||
if (pauseQuickSend) {
|
}, [exchangeRate, stickerPrice]);
|
||||||
setPauseQuickSend(false);
|
|
||||||
}
|
|
||||||
}, MENTION_DEBOUNCE_MS);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
// Stickers: Check if creator has a tip account saved (on selector so that if a paid sticker is selected,
|
||||||
}, [pauseQuickSend]);
|
// 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
|
// 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) {
|
if (channelSettings && !channelSettings.comments_enabled) {
|
||||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||||
}
|
}
|
||||||
|
@ -449,24 +418,107 @@ export function CommentCreate(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReviewingSupportComment && activeChannelClaim) {
|
return (
|
||||||
return (
|
<Form
|
||||||
<div className="comment__create">
|
onSubmit={() => {}}
|
||||||
<div className="comment__sc-preview">
|
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
|
<CreditAmount
|
||||||
className="comment__sc-preview-amount"
|
|
||||||
isFiat={activeTab === TAB_FIAT}
|
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
|
className="commentCreate__supportCommentPreviewAmount"
|
||||||
|
isFiat={activeTab === TAB_FIAT}
|
||||||
size={activeTab === TAB_LBC ? 18 : 2}
|
size={activeTab === TAB_LBC ? 18 : 2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<div>
|
<div className="commentCreate__supportCommentBody">
|
||||||
<UriIndicator uri={activeChannelClaim.name} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
<div>{commentValue}</div>
|
<div>{commentValue}</div>
|
||||||
</div>
|
</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
|
<Button
|
||||||
autoFocus
|
autoFocus
|
||||||
button="primary"
|
button="primary"
|
||||||
|
@ -480,136 +532,148 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
onClick={handleSupportComment}
|
onClick={handleSupportComment}
|
||||||
/>
|
/>
|
||||||
|
) : isReviewingStickerComment && selectedSticker ? (
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
button="primary"
|
||||||
button="link"
|
label={__('Send')}
|
||||||
label={__('Cancel')}
|
disabled={isSupportComment && (tipError || disableReviewButton)}
|
||||||
onClick={() => setIsReviewingSupportComment(false)}
|
onClick={() => {
|
||||||
|
if (isSupportComment) {
|
||||||
|
handleSupportComment();
|
||||||
|
} else {
|
||||||
|
handleCreateComment();
|
||||||
|
}
|
||||||
|
setSelectedSticker(null);
|
||||||
|
setReviewingStickerComment(false);
|
||||||
|
setStickerSelector(false);
|
||||||
|
setIsSupportComment(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : isSupportComment ? (
|
||||||
|
<Button
|
||||||
|
disabled={disabled || tipError || disableReviewButton || !minAmountMet}
|
||||||
|
type="button"
|
||||||
|
button="primary"
|
||||||
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} // only LBC
|
||||||
|
label={__('Review')}
|
||||||
|
onClick={() => setReviewingSupportComment(true)}
|
||||||
/>
|
/>
|
||||||
{MinAmountNotice}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<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 ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
|
|
||||||
type="button"
|
|
||||||
button="primary"
|
|
||||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
|
||||||
label={__('Review')}
|
|
||||||
onClick={() => setIsReviewingSupportComment(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
button="link"
|
|
||||||
label={__('Cancel')}
|
|
||||||
onClick={() => setIsSupportComment(false)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
|
(!minTip || claimIsMine) && (
|
||||||
|
<Button
|
||||||
|
ref={buttonRef}
|
||||||
|
button="primary"
|
||||||
|
disabled={disabled || stickerSelector}
|
||||||
|
type="submit"
|
||||||
|
label={
|
||||||
|
isReply
|
||||||
|
? isSubmitting
|
||||||
|
? __('Replying...')
|
||||||
|
: __('Reply')
|
||||||
|
: isSubmitting
|
||||||
|
? __('Commenting...')
|
||||||
|
: __('Comment --[button to submit something]--')
|
||||||
|
}
|
||||||
|
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/** Stickers/Support Buttons **/}
|
||||||
|
{!supportDisabled && !stickerSelector && (
|
||||||
<>
|
<>
|
||||||
{(!minTip || claimIsMine) && (
|
{getActionButton(
|
||||||
<Button
|
__('Stickers'),
|
||||||
ref={buttonRef}
|
isReviewingStickerComment ? __('Different Sticker') : undefined,
|
||||||
button="primary"
|
ICONS.STICKER,
|
||||||
disabled={disabled}
|
() => {
|
||||||
type="submit"
|
if (isReviewingStickerComment) setReviewingStickerComment(false);
|
||||||
label={
|
setIsSupportComment(false);
|
||||||
isReply
|
setStickerSelector(true);
|
||||||
? isSubmitting
|
}
|
||||||
? __('Replying...')
|
|
||||||
: __('Reply')
|
|
||||||
: isSubmitting
|
|
||||||
? __('Commenting...')
|
|
||||||
: __('Comment --[button to submit something]--')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{!supportDisabled && !claimIsMine && (
|
{/* below buttons are unnecessary - REMOVE */}
|
||||||
|
{!claimIsMine && (
|
||||||
<>
|
<>
|
||||||
<Button
|
{(!isSupportComment || activeTab !== TAB_LBC) &&
|
||||||
disabled={disabled}
|
getActionButton(
|
||||||
button="alt"
|
__('Credits'),
|
||||||
className="thatButton"
|
isSupportComment ? __('Switch to Credits') : undefined,
|
||||||
icon={ICONS.LBC}
|
ICONS.LBC,
|
||||||
onClick={() => {
|
() => {
|
||||||
setIsSupportComment(true);
|
setIsSupportComment(true);
|
||||||
setActiveTab(TAB_LBC);
|
setActiveTab(TAB_LBC);
|
||||||
}}
|
}
|
||||||
/>
|
)}
|
||||||
|
|
||||||
|
{stripeEnvironment &&
|
||||||
|
(!isSupportComment || activeTab !== TAB_FIAT) &&
|
||||||
|
getActionButton(
|
||||||
|
__('Cash'),
|
||||||
|
isSupportComment ? __('Switch to Cash') : undefined,
|
||||||
|
ICONS.FINANCE,
|
||||||
|
() => {
|
||||||
|
setIsSupportComment(true);
|
||||||
|
setActiveTab(TAB_FIAT);
|
||||||
|
}
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isReply && !minTip && (
|
|
||||||
<Button
|
|
||||||
button="link"
|
|
||||||
label={__('Cancel')}
|
|
||||||
onClick={() => {
|
|
||||||
if (onCancelReplying) {
|
|
||||||
onCancelReplying();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Cancel Button */}
|
||||||
|
{(isSupportComment ||
|
||||||
|
isReviewingSupportComment ||
|
||||||
|
stickerSelector ||
|
||||||
|
isReviewingStickerComment ||
|
||||||
|
(isReply && !minTip)) && (
|
||||||
|
<Button
|
||||||
|
disabled={isSupportComment && isSubmitting}
|
||||||
|
button="link"
|
||||||
|
label={__('Cancel')}
|
||||||
|
onClick={() => {
|
||||||
|
if (isSupportComment || isReviewingSupportComment) {
|
||||||
|
if (!isReviewingSupportComment) setIsSupportComment(false);
|
||||||
|
setReviewingSupportComment(false);
|
||||||
|
if (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>}
|
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||||
{MinAmountNotice}
|
{!!minAmount && (
|
||||||
|
<div className="help--notice commentCreate__minAmountNotice">
|
||||||
|
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||||
|
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||||
|
</I18nMessage>
|
||||||
|
<Icon
|
||||||
|
customTooltipText={
|
||||||
|
minTip
|
||||||
|
? __('This channel requires a minimum tip for each comment.')
|
||||||
|
: minSuper
|
||||||
|
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
className="icon--help"
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
tooltip
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,102 +1,112 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import { formatCredits, formatFullPrice } from 'util/format-credits';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import { formatCredits, formatFullPrice } from 'util/format-credits';
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
amount: number,
|
amount?: number,
|
||||||
|
className?: string,
|
||||||
|
customAmounts?: { amountFiat: number, amountLBC: number },
|
||||||
|
fee?: boolean,
|
||||||
|
isEstimate?: boolean,
|
||||||
|
isFiat?: boolean,
|
||||||
|
noFormat?: boolean,
|
||||||
precision: number,
|
precision: number,
|
||||||
showFree: boolean,
|
showFree: boolean,
|
||||||
showFullPrice: boolean,
|
showFullPrice: boolean,
|
||||||
showPlus: boolean,
|
|
||||||
isEstimate?: boolean,
|
|
||||||
showLBC?: boolean,
|
showLBC?: boolean,
|
||||||
fee?: boolean,
|
showPlus: boolean,
|
||||||
className?: string,
|
|
||||||
noFormat?: boolean,
|
|
||||||
size?: number,
|
size?: number,
|
||||||
superChat?: boolean,
|
superChat?: boolean,
|
||||||
superChatLight?: boolean,
|
superChatLight?: boolean,
|
||||||
isFiat?: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CreditAmount extends React.PureComponent<Props> {
|
class CreditAmount extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
noFormat: false,
|
||||||
precision: 2,
|
precision: 2,
|
||||||
showFree: false,
|
showFree: false,
|
||||||
showFullPrice: false,
|
showFullPrice: false,
|
||||||
showPlus: false,
|
|
||||||
showLBC: true,
|
showLBC: true,
|
||||||
noFormat: false,
|
showPlus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
precision,
|
|
||||||
showFullPrice,
|
|
||||||
showFree,
|
|
||||||
showPlus,
|
|
||||||
isEstimate,
|
|
||||||
fee,
|
|
||||||
showLBC,
|
|
||||||
className,
|
className,
|
||||||
|
customAmounts,
|
||||||
|
fee,
|
||||||
|
isEstimate,
|
||||||
|
isFiat,
|
||||||
noFormat,
|
noFormat,
|
||||||
|
precision,
|
||||||
|
showFree,
|
||||||
|
showFullPrice,
|
||||||
|
showLBC,
|
||||||
|
showPlus,
|
||||||
size,
|
size,
|
||||||
superChat,
|
superChat,
|
||||||
superChatLight,
|
superChatLight,
|
||||||
isFiat,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const minimumRenderableAmount = 10 ** (-1 * precision);
|
const minimumRenderableAmount = 10 ** (-1 * precision);
|
||||||
|
|
||||||
// return null, otherwise it will try and convert undefined to a string
|
// return null, otherwise it will try and convert undefined to a string
|
||||||
if (amount === undefined) {
|
if (amount === undefined && customAmounts === undefined) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const fullPrice = formatFullPrice(amount, 2);
|
|
||||||
const isFree = parseFloat(amount) === 0;
|
|
||||||
|
|
||||||
let formattedAmount;
|
function getAmountText(amount: number, isFiat?: boolean) {
|
||||||
if (showFullPrice) {
|
const fullPrice = formatFullPrice(amount, 2);
|
||||||
formattedAmount = fullPrice;
|
const isFree = parseFloat(amount) === 0;
|
||||||
} else {
|
let formattedAmount;
|
||||||
formattedAmount =
|
|
||||||
amount > 0 && amount < minimumRenderableAmount
|
|
||||||
? `<${minimumRenderableAmount}`
|
|
||||||
: formatCredits(amount, precision, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let amountText;
|
if (showFullPrice) {
|
||||||
if (showFree && isFree) {
|
formattedAmount = fullPrice;
|
||||||
amountText = __('Free');
|
} else {
|
||||||
} else {
|
formattedAmount =
|
||||||
amountText = noFormat ? amount : formattedAmount;
|
amount > 0 && amount < minimumRenderableAmount
|
||||||
|
? `<${minimumRenderableAmount}`
|
||||||
if (showPlus && amount > 0) {
|
: formatCredits(amount, precision, true);
|
||||||
amountText = `+${amountText}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showLBC && !isFiat) {
|
if (showFree && isFree) {
|
||||||
amountText = <LbcSymbol postfix={amountText} size={size} />;
|
return __('Free');
|
||||||
} else if (showLBC && isFiat) {
|
} else {
|
||||||
amountText = <p style={{ display: 'inline' }}> ${(Math.round(Number(amountText) * 100) / 100).toFixed(2)}</p>;
|
let amountText = noFormat ? amount : formattedAmount;
|
||||||
}
|
|
||||||
|
|
||||||
if (fee) {
|
if (showPlus && amount > 0) {
|
||||||
amountText = __('%amount% fee', { amount: amountText });
|
amountText = `+${amountText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLBC && !isFiat) {
|
||||||
|
amountText = <LbcSymbol postfix={amountText} size={size} />;
|
||||||
|
} else if (showLBC && isFiat) {
|
||||||
|
amountText = <p style={{ display: 'inline' }}> ${(Math.round(Number(amountText) * 100) / 100).toFixed(2)}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fee) {
|
||||||
|
amountText = __('%amount% fee', { amount: amountText });
|
||||||
|
}
|
||||||
|
|
||||||
|
return amountText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
title={fullPrice}
|
title={amount ? formatFullPrice(amount, 2) : ''}
|
||||||
className={classnames(className, {
|
className={classnames(className, {
|
||||||
'super-chat': superChat,
|
'super-chat': superChat,
|
||||||
'super-chat--light': superChatLight,
|
'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 ? (
|
{isEstimate ? (
|
||||||
<span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}>
|
<span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}>
|
||||||
|
|
|
@ -1,60 +1,53 @@
|
||||||
// @flow
|
// @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 React from 'react';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
import ReactDOMServer from 'react-dom/server';
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
import type { ElementRef, Node } from 'react';
|
||||||
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'),
|
|
||||||
];
|
|
||||||
|
|
||||||
type Props = {
|
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
|
affixClass?: string, // class applied to prefix/postfix label
|
||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
labelOnLeft: boolean,
|
|
||||||
inputButton?: React$Node,
|
|
||||||
blockWrap: boolean,
|
blockWrap: boolean,
|
||||||
charCount?: number,
|
charCount?: number,
|
||||||
textAreaMaxLength?: number,
|
children?: React$Node,
|
||||||
range?: number,
|
defaultValue?: string | number,
|
||||||
min?: number,
|
|
||||||
max?: number,
|
|
||||||
quickActionLabel?: string,
|
|
||||||
quickActionHandler?: (any) => any,
|
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
onChange: (any) => void,
|
error?: string | boolean,
|
||||||
value?: string | number,
|
helper?: string | React$Node,
|
||||||
|
hideSuggestions?: boolean,
|
||||||
|
inputButton?: React$Node,
|
||||||
|
isLivestream?: boolean,
|
||||||
|
label?: string | Node,
|
||||||
|
labelOnLeft: boolean,
|
||||||
|
max?: number,
|
||||||
|
min?: number,
|
||||||
|
name: string,
|
||||||
noEmojis?: boolean,
|
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> {
|
export class FormField extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||||
labelOnLeft: false,
|
|
||||||
blockWrap: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
input: { current: ElementRef<any> };
|
input: { current: ElementRef<any> };
|
||||||
|
|
||||||
|
@ -67,36 +60,48 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
const { autoFocus } = this.props;
|
const { autoFocus } = this.props;
|
||||||
const input = this.input.current;
|
const input = this.input.current;
|
||||||
|
|
||||||
if (input && autoFocus) {
|
if (input && autoFocus) input.focus();
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
render,
|
|
||||||
label,
|
|
||||||
prefix,
|
|
||||||
postfix,
|
|
||||||
error,
|
|
||||||
helper,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
children,
|
|
||||||
stretch,
|
|
||||||
affixClass,
|
affixClass,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
inputButton,
|
|
||||||
labelOnLeft,
|
|
||||||
blockWrap,
|
blockWrap,
|
||||||
charCount,
|
charCount,
|
||||||
textAreaMaxLength,
|
children,
|
||||||
quickActionLabel,
|
error,
|
||||||
quickActionHandler,
|
helper,
|
||||||
|
hideSuggestions,
|
||||||
|
inputButton,
|
||||||
|
isLivestream,
|
||||||
|
label,
|
||||||
|
labelOnLeft,
|
||||||
|
name,
|
||||||
noEmojis,
|
noEmojis,
|
||||||
|
postfix,
|
||||||
|
prefix,
|
||||||
|
quickActionLabel,
|
||||||
|
stretch,
|
||||||
|
textAreaMaxLength,
|
||||||
|
type,
|
||||||
|
openEmoteMenu,
|
||||||
|
quickActionHandler,
|
||||||
|
render,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const errorMessage = typeof error === 'object' ? error.message : error;
|
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
|
const Wrapper = blockWrap
|
||||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||||
|
@ -108,207 +113,177 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
let input;
|
const inputSimple = (type: string) => (
|
||||||
if (type) {
|
<>
|
||||||
if (type === 'radio') {
|
<input id={name} type={type} {...inputProps} />
|
||||||
input = (
|
<label htmlFor={name}>{label}</label>
|
||||||
<Wrapper>
|
</>
|
||||||
<input id={name} type="radio" {...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>
|
|
||||||
{(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 === '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 getInstance = (editor) => {
|
const inputSelect = (selectClass: string) => (
|
||||||
// SimpleMDE max char check
|
<fieldset-section class={selectClass}>
|
||||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
{(label || errorMessage) && (
|
||||||
if (textAreaMaxLength && changes.update) {
|
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||||
var str = changes.text.join('\n');
|
)}
|
||||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
<select id={name} {...inputProps}>
|
||||||
if (delta <= 0) {
|
{children}
|
||||||
return;
|
</select>
|
||||||
}
|
</fieldset-section>
|
||||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
);
|
||||||
if (delta > 0) {
|
|
||||||
str = str.substr(0, str.length - delta);
|
|
||||||
changes.update(changes.from, changes.to, str.split('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
const input = () => {
|
||||||
editor.codemirror.on('changes', (instance, changes) => {
|
switch (type) {
|
||||||
try {
|
case 'radio':
|
||||||
// Grab the last change from the buffered list. I assume the
|
return <Wrapper>{inputSimple('radio')}</Wrapper>;
|
||||||
// buffered one ('changes', instead of 'change') is more efficient,
|
case 'checkbox':
|
||||||
// and that "Create Link" will always end up last in the list.
|
return <div className="checkbox">{inputSimple('checkbox')}</div>;
|
||||||
const lastChange = changes[changes.length - 1];
|
case 'range':
|
||||||
if (lastChange.origin === '+input') {
|
return <div>{inputSimple('range')}</div>;
|
||||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
case 'select':
|
||||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
return inputSelect('');
|
||||||
|
case 'select-tiny':
|
||||||
|
return inputSelect('select--slim');
|
||||||
|
case 'markdown':
|
||||||
|
const handleEvents = { contextmenu: openEditorMenu };
|
||||||
|
|
||||||
// The URL placeholder is always placed last, so just look at the
|
const getInstance = (editor) => {
|
||||||
// last text in the array to also cover the multi-line case:
|
// SimpleMDE max char check
|
||||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||||
|
if (textAreaMaxLength && changes.update) {
|
||||||
|
var str = changes.text.join('\n');
|
||||||
|
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||||
|
|
||||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
if (delta <= 0) return;
|
||||||
const from = lastChange.from;
|
|
||||||
const to = lastChange.to;
|
|
||||||
const isSelectionMultiline = lastChange.text.length > 1;
|
|
||||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
|
||||||
|
|
||||||
// Everything works fine for the [Ctrl-K] case, but for the
|
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||||
// [Button] case, this handler happens before the original
|
if (delta > 0) {
|
||||||
// code, thus our change got wiped out.
|
str = str.substr(0, str.length - delta);
|
||||||
// Add a small delay to handle that case.
|
changes.update(changes.from, changes.to, str.split('\n'));
|
||||||
setTimeout(() => {
|
|
||||||
instance.setSelection(
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
|
||||||
);
|
|
||||||
}, 25);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
});
|
||||||
// Do nothing (revert to original behavior)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ideally, the character count should (and can) be appended to the
|
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
editor.codemirror.on('changes', (instance, changes) => {
|
||||||
// to pass the current value to it's callback, nor query the current
|
try {
|
||||||
// text length from the callback. So, we'll use our own widget.
|
// Grab the last change from the buffered list. I assume the
|
||||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
// buffered one ('changes', instead of 'change') is more efficient,
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
// and that "Create Link" will always end up last in the list.
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
const lastChange = changes[changes.length - 1];
|
||||||
);
|
if (lastChange.origin === '+input') {
|
||||||
|
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||||
|
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||||
|
|
||||||
input = (
|
// The URL placeholder is always placed last, so just look at the
|
||||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
// last text in the array to also cover the multi-line case:
|
||||||
|
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||||
|
|
||||||
|
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||||
|
const from = lastChange.from;
|
||||||
|
const to = lastChange.to;
|
||||||
|
const isSelectionMultiline = lastChange.text.length > 1;
|
||||||
|
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||||
|
|
||||||
|
// Everything works fine for the [Ctrl-K] case, but for the
|
||||||
|
// [Button] case, this handler happens before the original
|
||||||
|
// code, thus our change got wiped out.
|
||||||
|
// Add a small delay to handle that case.
|
||||||
|
setTimeout(() => {
|
||||||
|
instance.setSelection(
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||||
|
);
|
||||||
|
}, 25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {} // Do nothing (revert to original behavior)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||||
|
<fieldset-section>
|
||||||
|
<div className="form-field__two-column">
|
||||||
|
<div>
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
</div>
|
||||||
|
{quickAction}
|
||||||
|
</div>
|
||||||
|
<SimpleMDE
|
||||||
|
{...inputProps}
|
||||||
|
id={name}
|
||||||
|
type="textarea"
|
||||||
|
events={handleEvents}
|
||||||
|
getMdeInstance={getInstance}
|
||||||
|
options={{
|
||||||
|
spellChecker: true,
|
||||||
|
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||||
|
previewRender(plainText) {
|
||||||
|
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||||
|
return ReactDOMServer.renderToString(preview);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{countInfo}
|
||||||
|
</fieldset-section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'textarea':
|
||||||
|
return (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
<div className="form-field__two-column">
|
{(label || quickAction) && (
|
||||||
<div>
|
<div className="form-field__two-column">
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
</div>
|
{quickAction}
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
<SimpleMDE
|
|
||||||
{...inputProps}
|
|
||||||
id={name}
|
|
||||||
type="textarea"
|
|
||||||
events={handleEvents}
|
|
||||||
getMdeInstance={getInstance}
|
|
||||||
options={{
|
|
||||||
spellChecker: true,
|
|
||||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
|
||||||
previewRender(plainText) {
|
|
||||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
|
||||||
return ReactDOMServer.renderToString(preview);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{countInfo}
|
|
||||||
</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 = (
|
|
||||||
<fieldset-section>
|
|
||||||
{(label || quickAction) && (
|
|
||||||
<div className="form-field__two-column">
|
|
||||||
<div>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
</div>
|
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<textarea
|
|
||||||
type={type}
|
|
||||||
id={name}
|
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{countInfo}
|
|
||||||
</div>
|
|
||||||
</fieldset-section>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
|
||||||
const inner = inputButton ? (
|
|
||||||
<input-submit>
|
|
||||||
{inputElement}
|
|
||||||
{inputButton}
|
|
||||||
</input-submit>
|
|
||||||
) : (
|
|
||||||
inputElement
|
|
||||||
);
|
|
||||||
|
|
||||||
input = (
|
{hideSuggestions ? (
|
||||||
<React.Fragment>
|
<textarea
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
|
ref={this.input}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{countInfo}
|
||||||
|
</div>
|
||||||
|
</fieldset-section>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
||||||
|
const inner = inputButton ? (
|
||||||
|
<input-submit>
|
||||||
|
{inputElement}
|
||||||
|
{inputButton}
|
||||||
|
</input-submit>
|
||||||
|
) : (
|
||||||
|
inputElement
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || errorMessage) && (
|
{(label || errorMessage) && (
|
||||||
<label htmlFor={name}>
|
<label htmlFor={name}>
|
||||||
|
@ -318,17 +293,15 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
{prefix && <label htmlFor={name}>{prefix}</label>}
|
{prefix && <label htmlFor={name}>{prefix}</label>}
|
||||||
{inner}
|
{inner}
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
</React.Fragment>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
{input}
|
{type && input()}
|
||||||
|
|
||||||
{helper && <div className="form-field__help">{helper}</div>}
|
{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" />
|
<line x1="19" y1="20.5" x2="20" y2="20.5" />
|
||||||
</g>
|
</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 reactRenderer from 'remark-react';
|
||||||
import MarkdownLink from 'component/markdownLink';
|
import MarkdownLink from 'component/markdownLink';
|
||||||
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
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 { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
|
||||||
|
import { formattedEmote, inlineEmote } from 'util/remark-emote';
|
||||||
import ZoomableImage from 'component/zoomableImage';
|
import ZoomableImage from 'component/zoomableImage';
|
||||||
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config';
|
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import { parse } from 'node-html-parser';
|
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 = {
|
type SimpleTextProps = {
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
};
|
};
|
||||||
|
@ -42,6 +51,7 @@ type MarkdownProps = {
|
||||||
className?: string,
|
className?: string,
|
||||||
parentCommentId?: string,
|
parentCommentId?: string,
|
||||||
isMarkdownPost?: boolean,
|
isMarkdownPost?: boolean,
|
||||||
|
disableTimestamps?: boolean,
|
||||||
stakedLevel?: number,
|
stakedLevel?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,10 +103,15 @@ const SimpleLink = (props: SimpleLinkProps) => {
|
||||||
|
|
||||||
const SimpleImageLink = (props: ImageLinkProps) => {
|
const SimpleImageLink = (props: ImageLinkProps) => {
|
||||||
const { src, title, alt, helpText } = props;
|
const { src, title, alt, helpText } = props;
|
||||||
|
|
||||||
if (!src) {
|
if (!src) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEmote(title, src)) {
|
||||||
|
return <OptimizedImage src={src} title={title} className="emote" waitLoad loading="lazy" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
button="link"
|
button="link"
|
||||||
|
@ -132,9 +147,20 @@ function isStakeEnoughForPreview(stakedLevel) {
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
|
||||||
const MarkdownPreview = (props: MarkdownProps) => {
|
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
|
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 the browser try to create an iframe to see if the markup is valid
|
||||||
let lbrySrc;
|
let lbrySrc;
|
||||||
try {
|
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 = {
|
const remarkOptions: Object = {
|
||||||
sanitize: schema,
|
sanitize: schema,
|
||||||
fragment: React.Fragment,
|
fragment: React.Fragment,
|
||||||
|
@ -169,9 +199,12 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
),
|
),
|
||||||
// Workaraund of remarkOptions.Fragment
|
// Workaraund of remarkOptions.Fragment
|
||||||
div: React.Fragment,
|
div: React.Fragment,
|
||||||
img: isStakeEnoughForPreview(stakedLevel)
|
img: (imgProps) =>
|
||||||
? ZoomableImage
|
isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src) ? (
|
||||||
: (imgProps) => <SimpleImageLink src={imgProps.src} alt={imgProps.alt} title={imgProps.title} />,
|
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
|
// Strip all content and just render text
|
||||||
if (strip) {
|
if (strip || stripQuote) {
|
||||||
// Remove new lines and extra space
|
// Remove new lines and extra space
|
||||||
remarkOptions.remarkReactComponents.p = SimpleText;
|
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">
|
<span dir="auto" className="markdown-preview">
|
||||||
{
|
{
|
||||||
remark()
|
remark()
|
||||||
|
@ -206,11 +251,13 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
.use(remarkAttr, remarkAttrOpts)
|
.use(remarkAttr, remarkAttrOpts)
|
||||||
// Remark plugins for lbry urls
|
// Remark plugins for lbry urls
|
||||||
// Note: The order is important
|
// Note: The order is important
|
||||||
.use(formatedLinks)
|
.use(formattedLinks)
|
||||||
.use(inlineLinks)
|
.use(inlineLinks)
|
||||||
.use(isMarkdownPost ? null : inlineTimestamp)
|
.use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
|
||||||
.use(isMarkdownPost ? null : formattedTimestamp)
|
.use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
|
||||||
// Emojis
|
// Emojis
|
||||||
|
.use(inlineEmote)
|
||||||
|
.use(formattedEmote)
|
||||||
.use(remarkEmoji)
|
.use(remarkEmoji)
|
||||||
// Render new lines without needing spaces.
|
// Render new lines without needing spaces.
|
||||||
.use(remarkBreaks)
|
.use(remarkBreaks)
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import 'scss/component/_file-price.scss';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
showFullPrice: boolean,
|
|
||||||
costInfo: ?{ includesData: boolean, cost: number },
|
|
||||||
doFetchCostInfoForUri: (string) => void,
|
|
||||||
uri: string,
|
|
||||||
fetching: boolean,
|
|
||||||
claim: ?{},
|
claim: ?{},
|
||||||
claimWasPurchased: boolean,
|
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
|
claimWasPurchased: boolean,
|
||||||
|
costInfo: ?{ includesData: boolean, cost: number },
|
||||||
|
fetching: boolean,
|
||||||
|
showFullPrice: boolean,
|
||||||
type?: string,
|
type?: string,
|
||||||
|
uri: string,
|
||||||
// below props are just passed to <CreditAmount />
|
// below props are just passed to <CreditAmount />
|
||||||
inheritStyle?: boolean,
|
customPrices?: { priceFiat: number, priceLBC: number },
|
||||||
showLBC?: boolean,
|
|
||||||
hideFree?: boolean, // hide the file price if it's free
|
hideFree?: boolean, // hide the file price if it's free
|
||||||
|
isFiat?: boolean,
|
||||||
|
showLBC?: boolean,
|
||||||
|
doFetchCostInfoForUri: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilePrice extends React.PureComponent<Props> {
|
class FilePrice extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = { showFullPrice: false };
|
||||||
showFullPrice: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchCost(this.props);
|
this.fetchCost(this.props);
|
||||||
|
@ -37,38 +37,45 @@ class FilePrice extends React.PureComponent<Props> {
|
||||||
fetchCost = (props: Props) => {
|
fetchCost = (props: Props) => {
|
||||||
const { costInfo, doFetchCostInfoForUri, uri, fetching, claim } = props;
|
const { costInfo, doFetchCostInfoForUri, uri, fetching, claim } = props;
|
||||||
|
|
||||||
if (costInfo === undefined && !fetching && claim) {
|
if (uri && costInfo === undefined && !fetching && claim) doFetchCostInfoForUri(uri);
|
||||||
doFetchCostInfoForUri(uri);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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)) {
|
if (!customPrices && (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree))) return null;
|
||||||
return null;
|
|
||||||
}
|
const className = classnames(claimWasPurchased ? 'filePrice__key' : 'filePrice', {
|
||||||
|
'filePrice--filepage': type === 'filepage',
|
||||||
|
'filePrice--modal': type === 'modal',
|
||||||
|
});
|
||||||
|
|
||||||
return claimWasPurchased ? (
|
return claimWasPurchased ? (
|
||||||
<span
|
<span className={className}>
|
||||||
className={classnames('file-price__key', {
|
|
||||||
'file-price__key--filepage': type === 'filepage',
|
|
||||||
'file-price__key--modal': type === 'modal',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon icon={ICONS.PURCHASED} size={type === 'filepage' ? 22 : undefined} />
|
<Icon icon={ICONS.PURCHASED} size={type === 'filepage' ? 22 : undefined} />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
className={classnames('file-price', {
|
amount={costInfo ? costInfo.cost : undefined}
|
||||||
'file-price--filepage': type === 'filepage',
|
customAmounts={
|
||||||
'file-price--modal': type === 'modal',
|
customPrices ? { amountFiat: customPrices.priceFiat, amountLBC: customPrices.priceLBC } : undefined
|
||||||
})}
|
}
|
||||||
|
className={className}
|
||||||
|
isEstimate={!!costInfo && !costInfo.includesData}
|
||||||
|
isFiat={isFiat} // this goes
|
||||||
showFree
|
showFree
|
||||||
showLBC={showLBC}
|
|
||||||
amount={costInfo.cost}
|
|
||||||
isEstimate={!costInfo.includesData}
|
|
||||||
showFullPrice={showFullPrice}
|
showFullPrice={showFullPrice}
|
||||||
|
showLBC={showLBC}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { connect } from 'react-redux';
|
||||||
import { doReadNotifications, doDeleteNotification } from 'redux/actions/notifications';
|
import { doReadNotifications, doDeleteNotification } from 'redux/actions/notifications';
|
||||||
import Notification from './view';
|
import Notification from './view';
|
||||||
|
|
||||||
export default connect(null, {
|
const perform = (dispatch, ownProps) => ({
|
||||||
doReadNotifications,
|
readNotification: () => dispatch(doReadNotifications([ownProps.notification.id])),
|
||||||
doDeleteNotification,
|
deleteNotification: () => dispatch(doDeleteNotification(ownProps.notification.id)),
|
||||||
})(Notification);
|
});
|
||||||
|
|
||||||
|
export default connect(null, perform)(Notification);
|
||||||
|
|
|
@ -1,64 +1,63 @@
|
||||||
// @flow
|
// @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 { RULE } from 'constants/notifications';
|
||||||
import React from 'react';
|
import { useHistory } from 'react-router';
|
||||||
import classnames from 'classnames';
|
import * as ICONS from 'constants/icons';
|
||||||
import Icon from 'component/common/icon';
|
|
||||||
import DateTime from 'component/dateTime';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import classnames from 'classnames';
|
||||||
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 CommentCreate from 'component/commentCreate';
|
import CommentCreate from 'component/commentCreate';
|
||||||
|
import CommentReactions from 'component/commentReactions';
|
||||||
import CommentsReplies from 'component/commentsReplies';
|
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 = {
|
type Props = {
|
||||||
notification: WebNotification,
|
|
||||||
menuButton: boolean,
|
menuButton: boolean,
|
||||||
children: any,
|
notification: WebNotification,
|
||||||
doReadNotifications: ([number]) => void,
|
deleteNotification: () => void,
|
||||||
doDeleteNotification: (number) => void,
|
readNotification: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Notification(props: Props) {
|
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 { push } = useHistory();
|
||||||
const { notification_rule, notification_parameters, is_read, id } = notification;
|
|
||||||
const [isReplying, setReplying] = React.useState(false);
|
const [isReplying, setReplying] = React.useState(false);
|
||||||
const [quickReply, setQuickReply] = React.useState();
|
const [quickReply, setQuickReply] = React.useState();
|
||||||
|
|
||||||
|
// ?
|
||||||
const isIgnoredNotification = notification_rule === RULE.NEW_LIVESTREAM;
|
const isIgnoredNotification = notification_rule === RULE.NEW_LIVESTREAM;
|
||||||
if (isIgnoredNotification) {
|
if (isIgnoredNotification) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCommentNotification =
|
const isCommentNotification =
|
||||||
notification_rule === RULE.COMMENT ||
|
notification_rule === RULE.COMMENT ||
|
||||||
notification_rule === RULE.COMMENT_REPLY ||
|
notification_rule === RULE.COMMENT_REPLY ||
|
||||||
notification_rule === RULE.CREATOR_COMMENT;
|
notification_rule === RULE.CREATOR_COMMENT;
|
||||||
const commentText = isCommentNotification && notification_parameters.dynamic.comment;
|
const commentText = isCommentNotification && notification_parameters.dynamic.comment;
|
||||||
|
const stickerFromComment = isCommentNotification && commentText && parseSticker(commentText);
|
||||||
|
const notificationTarget = getNotificationTarget();
|
||||||
|
|
||||||
let notificationTarget;
|
const creatorIcon = (channelUrl) => (
|
||||||
switch (notification_rule) {
|
<UriIndicator uri={channelUrl} link>
|
||||||
default:
|
<ChannelThumbnail small uri={channelUrl} />
|
||||||
notificationTarget = notification_parameters.device.target;
|
</UriIndicator>
|
||||||
}
|
);
|
||||||
|
|
||||||
const creatorIcon = (channelUrl) => {
|
|
||||||
return (
|
|
||||||
<UriIndicator uri={channelUrl} link>
|
|
||||||
<ChannelThumbnail small uri={channelUrl} />
|
|
||||||
</UriIndicator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
let channelUrl;
|
let channelUrl;
|
||||||
let icon;
|
let icon;
|
||||||
switch (notification_rule) {
|
switch (notification_rule) {
|
||||||
|
@ -94,8 +93,7 @@ export default function Notification(props: Props) {
|
||||||
let channelName;
|
let channelName;
|
||||||
if (channelUrl) {
|
if (channelUrl) {
|
||||||
try {
|
try {
|
||||||
const { claimName } = parseURI(channelUrl);
|
({ claimName: channelName } = parseURI(channelUrl));
|
||||||
channelName = claimName;
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,25 +121,28 @@ export default function Notification(props: Props) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { isChannel } = parseURI(notificationTarget);
|
const { isChannel } = parseURI(notificationTarget);
|
||||||
if (isChannel) {
|
if (isChannel) urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
|
||||||
urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
notificationLink += `?${urlParams.toString()}`;
|
notificationLink += `?${urlParams.toString()}`;
|
||||||
const navLinkProps = {
|
const navLinkProps = { to: notificationLink, onClick: (e) => e.stopPropagation() };
|
||||||
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() {
|
function handleNotificationClick() {
|
||||||
if (!is_read) {
|
if (!is_read) readNotification();
|
||||||
doReadNotifications([id]);
|
if (menuButton && notificationLink) push(notificationLink);
|
||||||
}
|
|
||||||
|
|
||||||
if (menuButton && notificationLink) {
|
|
||||||
push(notificationLink);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = menuButton
|
const Wrapper = menuButton
|
||||||
|
@ -166,45 +167,40 @@ export default function Notification(props: Props) {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classnames('notification__wrapper', { 'notification__wrapper--unread': !is_read })}>
|
||||||
className={classnames('notification__wrapper', {
|
|
||||||
'notification__wrapper--unread': !is_read,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<div className="notification__icon">{icon}</div>
|
<div className="notification__icon">{icon}</div>
|
||||||
|
|
||||||
<div className="notification__content-wrapper">
|
<div className="notificationContent__wrapper">
|
||||||
<div className="notification__content">
|
<div className="notification__content">
|
||||||
<div className="notification__text-wrapper">
|
<div className="notificationText__wrapper">
|
||||||
{!isCommentNotification && <div className="notification__title">{title}</div>}
|
<div className="notification__title">{title}</div>
|
||||||
|
|
||||||
{isCommentNotification && commentText ? (
|
{!commentText ? (
|
||||||
<>
|
<div
|
||||||
<div className="notification__title">{title}</div>
|
title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')}
|
||||||
<div title={commentText} className="notification__text">
|
className="notification__text"
|
||||||
{commentText}
|
>
|
||||||
</div>
|
<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">
|
||||||
<div
|
{commentText}
|
||||||
title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')}
|
</div>
|
||||||
className="notification__text"
|
|
||||||
>
|
|
||||||
<LbcMessage>{notification_parameters.device.text}</LbcMessage>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{notification_rule === RULE.NEW_CONTENT && (
|
{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 && (
|
{notification_rule === RULE.NEW_LIVESTREAM && (
|
||||||
<FileThumbnail
|
<FileThumbnail
|
||||||
thumbnail={notification_parameters.device.image_url}
|
thumbnail={notification_parameters.device.image_url}
|
||||||
className="notification__content-thumbnail"
|
className="notificationContent__thumbnail"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -212,10 +208,10 @@ export default function Notification(props: Props) {
|
||||||
<div className="notification__extra">
|
<div className="notification__extra">
|
||||||
{!is_read && (
|
{!is_read && (
|
||||||
<Button
|
<Button
|
||||||
className="notification__mark-seen"
|
className="notification__markSeen"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
doReadNotifications([id]);
|
readNotification();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -228,7 +224,7 @@ export default function Notification(props: Props) {
|
||||||
<div className="notification__menu">
|
<div className="notification__menu">
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
className={'menu__button notification__menu-button'}
|
className="menu__button notification__menuButton"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -237,7 +233,7 @@ export default function Notification(props: Props) {
|
||||||
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList className="menu__list">
|
<MenuList className="menu__list">
|
||||||
<MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}>
|
<MenuItem className="menu__link" onSelect={() => deleteNotification()}>
|
||||||
<Icon aria-hidden icon={ICONS.DELETE} />
|
<Icon aria-hidden icon={ICONS.DELETE} />
|
||||||
{__('Delete')}
|
{__('Delete')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -10,10 +10,11 @@ function scaleToDevicePixelRatio(value: number, window: any) {
|
||||||
type Props = {
|
type Props = {
|
||||||
src: string,
|
src: string,
|
||||||
objectFit?: string,
|
objectFit?: string,
|
||||||
|
waitLoad?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function OptimizedImage(props: Props) {
|
function OptimizedImage(props: Props) {
|
||||||
const { objectFit, src, ...imgProps } = props;
|
const { objectFit, src, waitLoad, ...imgProps } = props;
|
||||||
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
||||||
const ref = React.useRef<any>();
|
const ref = React.useRef<any>();
|
||||||
|
|
||||||
|
@ -101,8 +102,12 @@ function OptimizedImage(props: Props) {
|
||||||
<img
|
<img
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...imgProps}
|
{...imgProps}
|
||||||
|
style={{ visibility: waitLoad ? 'hidden' : 'visible' }}
|
||||||
src={optimizedSrc}
|
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 { connect } from 'react-redux';
|
||||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||||
import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
import { selectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||||
import RecommendedContent from './view';
|
import RecommendedContent from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||||
const { claim_id: claimId } = claim;
|
const { claim_id: claimId } = claim;
|
||||||
const recommendedContentUris = makeSelectRecommendedContentForUri(props.uri)(state);
|
const recommendedContentUris = selectRecommendedContentForUri(state, props.uri);
|
||||||
const nextRecommendedUri = recommendedContentUris && recommendedContentUris[0];
|
const nextRecommendedUri = recommendedContentUris && recommendedContentUris[0];
|
||||||
|
|
||||||
return {
|
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 { selectVolume, selectMute } from 'redux/selectors/app';
|
||||||
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { makeSelectContentPositionForUri, makeSelectIsPlayerFloating, selectPlayingUri } from 'redux/selectors/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 VideoViewer from './view';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||||
|
@ -41,7 +41,7 @@ const select = (state, props) => {
|
||||||
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state);
|
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||||
previousListUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state);
|
previousListUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||||
} else {
|
} else {
|
||||||
const recommendedContent = makeSelectRecommendedContentForUri(uri)(state);
|
const recommendedContent = selectRecommendedContentForUri(state, uri);
|
||||||
nextRecommendedUri = recommendedContent && recommendedContent[0];
|
nextRecommendedUri = recommendedContent && recommendedContent[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,36 +5,26 @@ import {
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
selectFetchingMyChannels,
|
selectFetchingMyChannels,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
import { doHideModal } from 'redux/actions/app';
|
||||||
import { doSendTip } from 'redux/actions/wallet';
|
import { doSendTip } from 'redux/actions/wallet';
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import WalletSendTip from './view';
|
|
||||||
import { doOpenModal, doHideModal } from 'redux/actions/app';
|
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { withRouter } from 'react-router';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import WalletSendTip from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
isPending: selectIsSendingSupport(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
title: selectTitleForUri(state, props.uri),
|
|
||||||
claim: makeSelectClaimForUri(props.uri, false)(state),
|
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
|
claim: makeSelectClaimForUri(props.uri, false)(state), // find this selectClaim
|
||||||
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
|
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
|
||||||
incognito: selectIncognito(state),
|
incognito: selectIncognito(state),
|
||||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
|
||||||
|
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
|
||||||
|
isPending: selectIsSendingSupport(state),
|
||||||
|
title: selectTitleForUri(state, props.uri),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
export default withRouter(connect(select, { doHideModal, doSendTip })(WalletSendTip)); // doSendCashTip gone
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
|
||||||
closeModal: () => dispatch(doHideModal()),
|
|
||||||
sendSupport: (params, isSupport) => dispatch(doSendTip(params, isSupport)),
|
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(WalletSendTip));
|
|
||||||
|
|
|
@ -1,219 +1,162 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { Form } from 'component/common/form';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { parseURI } from 'util/lbryURI';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { FormField, Form } from 'component/common/form';
|
|
||||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import classnames from 'classnames';
|
|
||||||
import ChannelSelector from 'component/channelSelector';
|
import ChannelSelector from 'component/channelSelector';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import React from 'react';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
|
|
||||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
|
||||||
|
|
||||||
const TAB_BOOST = 'TabBoost';
|
const TAB_BOOST = 'TabBoost';
|
||||||
const TAB_LBC = 'TabLBC';
|
const TAB_LBC = 'TabLBC';
|
||||||
|
const TAB_FIAT = 'TabFiat';
|
||||||
|
|
||||||
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
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 = {
|
type Props = {
|
||||||
uri: string,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
claimIsMine: boolean,
|
|
||||||
title: string,
|
|
||||||
claim: StreamClaim,
|
|
||||||
isPending: boolean,
|
|
||||||
isSupport: boolean,
|
|
||||||
sendSupport: (SupportParams, boolean) => void,
|
|
||||||
closeModal: () => void,
|
|
||||||
balance: number,
|
balance: number,
|
||||||
|
claim: StreamClaim,
|
||||||
|
claimIsMine: boolean,
|
||||||
fetchingChannels: boolean,
|
fetchingChannels: boolean,
|
||||||
|
incognito: boolean,
|
||||||
instantTipEnabled: boolean,
|
instantTipEnabled: boolean,
|
||||||
instantTipMax: { amount: number, currency: string },
|
instantTipMax: { amount: number, currency: string },
|
||||||
activeChannelClaim: ?ChannelClaim,
|
isPending: boolean,
|
||||||
incognito: boolean,
|
isSupport: boolean,
|
||||||
doToast: ({ message: string }) => void,
|
title: string,
|
||||||
isAuthenticated: boolean,
|
uri: string,
|
||||||
|
doHideModal: () => void,
|
||||||
|
doSendTip: (SupportParams, boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function WalletSendTip(props: Props) {
|
function WalletSendTip(props: Props) {
|
||||||
const {
|
const {
|
||||||
uri,
|
activeChannelClaim,
|
||||||
title,
|
|
||||||
isPending,
|
|
||||||
claimIsMine,
|
|
||||||
balance,
|
balance,
|
||||||
claim = {},
|
claim = {},
|
||||||
instantTipEnabled,
|
claimIsMine,
|
||||||
instantTipMax,
|
|
||||||
sendSupport,
|
|
||||||
closeModal,
|
|
||||||
fetchingChannels,
|
fetchingChannels,
|
||||||
incognito,
|
incognito,
|
||||||
activeChannelClaim,
|
instantTipEnabled,
|
||||||
|
instantTipMax,
|
||||||
|
isPending,
|
||||||
|
title,
|
||||||
|
uri,
|
||||||
|
doHideModal,
|
||||||
|
doSendTip,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
/** REACT STATE **/
|
/** STATE **/
|
||||||
const [presetTipAmount, setPresetTipAmount] = usePersistedState('comment-support:presetTip', DEFAULT_TIP_AMOUNTS[0]);
|
|
||||||
const [customTipAmount, setCustomTipAmount] = usePersistedState('comment-support:customTip', 1.0);
|
|
||||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
|
||||||
const [isConfirming, setIsConfirming] = React.useState(false);
|
|
||||||
|
|
||||||
// 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();
|
const [tipError, setTipError] = React.useState();
|
||||||
|
|
||||||
// denote which tab to show on the frontend
|
|
||||||
const [activeTab, setActiveTab] = usePersistedState(TAB_BOOST);
|
const [activeTab, setActiveTab] = usePersistedState(TAB_BOOST);
|
||||||
|
const [disableSubmitButton, setDisableSubmitButton] = React.useState();
|
||||||
|
|
||||||
// handle default active tab
|
/** CONSTS **/
|
||||||
React.useEffect(() => {
|
|
||||||
// force to boost tab if it's someone's own upload
|
|
||||||
if (claimIsMine) {
|
|
||||||
setActiveTab(TAB_BOOST);
|
|
||||||
} else {
|
|
||||||
// or set LBC tip as the default if none is set yet
|
|
||||||
if (!activeTab || activeTab === 'undefined') {
|
|
||||||
setActiveTab(TAB_LBC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// alphanumeric claim id
|
|
||||||
const { claim_id: claimId } = claim;
|
|
||||||
|
|
||||||
// channel name used in url
|
|
||||||
const { channelName } = parseURI(uri);
|
|
||||||
|
|
||||||
// focus tip element if it exists
|
|
||||||
React.useEffect(() => {
|
|
||||||
const tipInputElement = document.getElementById('tip-input');
|
|
||||||
if (tipInputElement) {
|
|
||||||
tipInputElement.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// if user has no balance, used to show conditional frontend
|
|
||||||
const noBalance = balance === 0;
|
|
||||||
|
|
||||||
// the tip amount, based on if a preset or custom tip amount is being used
|
|
||||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
|
||||||
|
|
||||||
// get type of claim (stream/channel/repost/collection) for display on frontend
|
|
||||||
function getClaimTypeText() {
|
|
||||||
if (claim.value_type === 'stream') {
|
|
||||||
return __('Content');
|
|
||||||
} else if (claim.value_type === 'channel') {
|
|
||||||
return __('Channel');
|
|
||||||
} else if (claim.value_type === 'repost') {
|
|
||||||
return __('Repost');
|
|
||||||
} else if (claim.value_type === 'collection') {
|
|
||||||
return __('List');
|
|
||||||
} else {
|
|
||||||
return __('Claim');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const claimTypeText = getClaimTypeText();
|
const claimTypeText = getClaimTypeText();
|
||||||
|
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||||
|
const titleText = claimIsMine
|
||||||
|
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||||
|
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||||
|
const { claim_id: claimId } = claim;
|
||||||
|
let channelName;
|
||||||
|
try {
|
||||||
|
({ channelName } = parseURI(uri));
|
||||||
|
} catch (e) {}
|
||||||
|
// don't need this - fiat only, for reference REMOVE
|
||||||
|
// const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
|
// const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
|
//
|
||||||
|
// // setup variables for backend tip API
|
||||||
|
// const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||||
|
// const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||||
|
|
||||||
// icon to use or explainer text to show per tab
|
// icon to use or explainer text to show per tab
|
||||||
let iconToUse;
|
let explainerText = '',
|
||||||
let explainerText = '';
|
confirmLabel = '';
|
||||||
if (activeTab === TAB_BOOST) {
|
switch (activeTab) {
|
||||||
iconToUse = ICONS.LBC;
|
case TAB_BOOST:
|
||||||
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {
|
explainerText = __(
|
||||||
claimTypeText,
|
'This refundable boost will improve the discoverability of this %claimTypeText% while active. ',
|
||||||
});
|
{ claimTypeText }
|
||||||
} else if (activeTab === TAB_LBC) {
|
);
|
||||||
iconToUse = ICONS.LBC;
|
confirmLabel = __('Boosting');
|
||||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
break;
|
||||||
|
case TAB_FIAT:
|
||||||
|
explainerText = __('Show this channel your appreciation by sending a donation in USD. ');
|
||||||
|
confirmLabel = __('Tipping Fiat (USD)');
|
||||||
|
break;
|
||||||
|
case TAB_LBC:
|
||||||
|
explainerText = __('Show this channel your appreciation by sending a donation of Credits. ');
|
||||||
|
confirmLabel = __('Tipping Credit');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
/** FUNCTIONS **/
|
||||||
|
|
||||||
React.useEffect(() => {
|
function getClaimTypeText() {
|
||||||
// Regex for number up to 8 decimal places
|
switch (claim.value_type) {
|
||||||
let regexp;
|
case 'stream':
|
||||||
let tipError;
|
return __('Content');
|
||||||
|
case 'channel':
|
||||||
if (tipAmount === 0) {
|
return __('Channel');
|
||||||
tipError = __('Amount must be a positive number');
|
case 'repost':
|
||||||
} else if (!tipAmount || typeof tipAmount !== 'number') {
|
return __('Repost');
|
||||||
tipError = __('Amount must be a number');
|
case 'collection':
|
||||||
|
return __('List');
|
||||||
|
default:
|
||||||
|
return __('Claim');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// if it's not fiat, aka it's boost or lbc tip
|
|
||||||
else {
|
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
|
||||||
const validTipInput = regexp.test(String(tipAmount));
|
|
||||||
|
|
||||||
if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
|
||||||
} else if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
|
||||||
} else if (tipAmount === balance) {
|
|
||||||
tipError = __('Please decrease the amount to account for transaction fees');
|
|
||||||
} else if (tipAmount > balance) {
|
|
||||||
tipError = __('Not enough Credits');
|
|
||||||
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
|
|
||||||
tipError = __('Amount must be higher');
|
|
||||||
}
|
|
||||||
// if tip fiat tab
|
|
||||||
}
|
|
||||||
|
|
||||||
setTipError(tipError);
|
|
||||||
}, [tipAmount, balance, setTipError, activeTab]);
|
|
||||||
|
|
||||||
// make call to the backend to send lbc or fiat
|
// make call to the backend to send lbc or fiat
|
||||||
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
||||||
// send a tip
|
if (!isOnConfirmationPage && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||||
if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
setConfirmationPage(true);
|
||||||
setIsConfirming(true);
|
|
||||||
} else {
|
} else {
|
||||||
// send a boost
|
const supportParams: SupportParams = {
|
||||||
const supportParams: SupportParams = { amount: tipAmount, claim_id: claimId };
|
amount: tipAmount,
|
||||||
|
claim_id: claimId,
|
||||||
// include channel name if donation not anonymous
|
channel_id: activeChannelClaim && !incognito ? activeChannelClaim.claim_id : undefined,
|
||||||
if (activeChannelClaim && !incognito) {
|
};
|
||||||
supportParams.channel_id = activeChannelClaim.claim_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send tip/boost
|
// send tip/boost
|
||||||
sendSupport(supportParams, isSupport);
|
doSendTip(supportParams, isSupport);
|
||||||
closeModal();
|
doHideModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when the form button is clicked
|
// when the form button is clicked
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (tipAmount && claimId) {
|
if (!tipAmount || !claimId) return;
|
||||||
// send an instant tip (no need to go to an exchange first)
|
|
||||||
if (instantTipEnabled) {
|
// send an instant tip (no need to go to an exchange first)
|
||||||
if (instantTipMax.currency === 'LBC') {
|
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||||
sendSupportOrConfirm(instantTipMax.amount);
|
if (instantTipMax.currency === 'LBC') {
|
||||||
} else {
|
sendSupportOrConfirm(instantTipMax.amount);
|
||||||
// Need to convert currency of instant purchase maximum before trying to send support
|
|
||||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
|
||||||
sendSupportOrConfirm(instantTipMax.amount / LBC_USD);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// sending fiat tip
|
|
||||||
} else {
|
} else {
|
||||||
sendSupportOrConfirm();
|
// Need to convert currency of instant purchase maximum before trying to send support
|
||||||
|
Lbryio.getExchangeRates().then(({ LBC_USD }) => sendSupportOrConfirm(instantTipMax.amount / LBC_USD));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
sendSupportOrConfirm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
|
||||||
let tipAmountAsString = event.target.value;
|
|
||||||
|
|
||||||
let tipAmount = parseFloat(tipAmountAsString);
|
|
||||||
setCustomTipAmount(tipAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildButtonText() {
|
function buildButtonText() {
|
||||||
// test if frontend will show up as isNan
|
// test if frontend will show up as isNan
|
||||||
function isNan(tipAmount) {
|
function isNan(tipAmount) {
|
||||||
|
@ -223,100 +166,72 @@ function WalletSendTip(props: Props) {
|
||||||
return tipAmount !== tipAmount || tipAmount === 'NaN';
|
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
|
// 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
|
// build button text based on tab
|
||||||
if (activeTab === TAB_BOOST) {
|
switch (activeTab) {
|
||||||
return claimIsMine
|
case TAB_BOOST:
|
||||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
return titleText;
|
||||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
// case TAB_FIAT:
|
||||||
} else if (activeTab === TAB_LBC) {
|
// return __('Send a $%displayAmount% Tip', { displayAmount });
|
||||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
case TAB_LBC:
|
||||||
|
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dont allow user to click send button
|
/** RENDER **/
|
||||||
function shouldDisableAmountSelector(amount) {
|
|
||||||
return amount > balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// showed on confirm page above amount
|
const getTabButton = (tabIcon: string, tabLabel: string, tabName: string) => (
|
||||||
function setConfirmLabel() {
|
<Button
|
||||||
if (activeTab === TAB_LBC) {
|
key={tabName}
|
||||||
return __('Tipping Credit');
|
icon={tabIcon}
|
||||||
} else if (activeTab === TAB_BOOST) {
|
label={tabLabel}
|
||||||
return __('Boosting');
|
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 (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
{/* if there is no LBC balance, show user frontend to get credits */}
|
{/* if there is no LBC balance, show user frontend to get credits */}
|
||||||
{/* if there is lbc, the main tip/boost gui with the 3 tabs at the top */}
|
{/* if there is lbc, the main tip/boost gui with the 3 tabs at the top */}
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={<LbcSymbol postfix={titleText} size={22} />}
|
||||||
<LbcSymbol
|
|
||||||
postfix={
|
|
||||||
claimIsMine
|
|
||||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
|
||||||
: __('Support This %claimTypeText%', { claimTypeText })
|
|
||||||
}
|
|
||||||
size={22}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
subtitle={
|
subtitle={
|
||||||
<React.Fragment>
|
<>
|
||||||
{!claimIsMine && (
|
{!claimIsMine && (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{/* tip LBC tab button */}
|
{/* tip LBC tab button */}
|
||||||
<Button
|
{getTabButton(ICONS.LBC, __('Tip'), TAB_LBC)}
|
||||||
key="tip"
|
|
||||||
icon={ICONS.LBC}
|
{/* support LBC tab button */}
|
||||||
label={__('Tip')}
|
{getTabButton(ICONS.TRENDING, __('Boost'), TAB_BOOST)}
|
||||||
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 })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* short explainer under the button */}
|
{/* short explainer under the button */}
|
||||||
<div className="section__subtitle">
|
<div className="section__subtitle">
|
||||||
{explainerText + ' '}
|
{explainerText}
|
||||||
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
||||||
{<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />}
|
{<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />}
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
// confirmation modal, allow user to confirm or cancel transaction
|
// confirmation modal, allow user to confirm or cancel transaction
|
||||||
isConfirming ? (
|
isOnConfirmationPage ? (
|
||||||
<>
|
<>
|
||||||
<div className="section section--padded card--inline confirm__wrapper">
|
<div className="section section--padded card--inline confirm__wrapper">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
|
@ -326,7 +241,7 @@ function WalletSendTip(props: Props) {
|
||||||
<div className="confirm__value">
|
<div className="confirm__value">
|
||||||
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
||||||
</div>
|
</div>
|
||||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
<div className="confirm__label">{confirmLabel}</div>
|
||||||
<div className="confirm__value">
|
<div className="confirm__value">
|
||||||
<LbcSymbol postfix={tipAmount} size={22} />
|
<LbcSymbol postfix={tipAmount} size={22} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -334,85 +249,23 @@ function WalletSendTip(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button autoFocus onClick={handleSubmit} button="primary" disabled={isPending} label={__('Confirm')} />
|
<Button autoFocus onClick={handleSubmit} button="primary" disabled={isPending} label={__('Confirm')} />
|
||||||
<Button button="link" label={__('Cancel')} onClick={() => setIsConfirming(false)} />
|
<Button button="link" label={__('Cancel')} onClick={() => setConfirmationPage(false)} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && noBalance) ? (
|
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && balance === 0) ? (
|
||||||
<>
|
<>
|
||||||
<div className="section">
|
<ChannelSelector />
|
||||||
<ChannelSelector />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* section to pick tip/boost amount */}
|
{/* section to pick tip/boost amount */}
|
||||||
<div className="section">
|
<WalletTipAmountSelector
|
||||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
setTipError={setTipError}
|
||||||
<Button
|
tipError={tipError}
|
||||||
key={amount}
|
claim={claim}
|
||||||
disabled={shouldDisableAmountSelector(amount)}
|
activeTab={TAB_LBC} // active tab
|
||||||
button="alt"
|
amount={tipAmount}
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
onChange={(amount) => setTipAmount(amount)}
|
||||||
'button-toggle--active': tipAmount === amount && !useCustomTip,
|
setDisableSubmitButton={setDisableSubmitButton}
|
||||||
'button-toggle--disabled': amount > balance,
|
/>
|
||||||
})}
|
|
||||||
label={amount}
|
|
||||||
icon={iconToUse}
|
|
||||||
onClick={() => {
|
|
||||||
setPresetTipAmount(amount);
|
|
||||||
setUseCustomTip(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
button="alt"
|
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
|
||||||
'button-toggle--active': useCustomTip, // set as active
|
|
||||||
})}
|
|
||||||
icon={iconToUse}
|
|
||||||
label={__('Custom')}
|
|
||||||
onClick={() => setUseCustomTip(true)}
|
|
||||||
// disabled if it's receive fiat and there is no card or creator can't receive tips
|
|
||||||
/>
|
|
||||||
|
|
||||||
{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 */}
|
{/* send tip/boost button */}
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
|
@ -421,23 +274,25 @@ function WalletSendTip(props: Props) {
|
||||||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||||
button="primary"
|
button="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={fetchingChannels || isPending || tipError || !tipAmount}
|
disabled={fetchingChannels || isPending || tipError || !tipAmount || disableSubmitButton}
|
||||||
label={buildButtonText()}
|
label={buildButtonText()}
|
||||||
/>
|
/>
|
||||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||||
</div>
|
</div>
|
||||||
<WalletSpendableBalanceHelp />
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Supporting content requires %lbc%</I18nMessage>
|
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>
|
||||||
|
{__('Supporting content requires %lbc%')}
|
||||||
|
</I18nMessage>
|
||||||
}
|
}
|
||||||
subtitle={
|
subtitle={
|
||||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>
|
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>
|
||||||
With %lbc%, you can send tips to your favorite creators, or help boost their content for more people
|
{__(
|
||||||
to see.
|
'With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.'
|
||||||
|
)}
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
|
@ -454,7 +309,7 @@ function WalletSendTip(props: Props) {
|
||||||
label={__('Buy/Swap Credits')}
|
label={__('Buy/Swap Credits')}
|
||||||
navigate={`/$/${PAGES.BUY}`}
|
navigate={`/$/${PAGES.BUY}`}
|
||||||
/>
|
/>
|
||||||
<Button button="link" label={__('Nevermind')} onClick={closeModal} />
|
<Button button="link" label={__('Nevermind')} onClick={doHideModal} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { connect } from 'react-redux';
|
||||||
import { selectBalance } from 'redux/selectors/wallet';
|
import { selectBalance } from 'redux/selectors/wallet';
|
||||||
import WalletSpendableBalanceHelp from './view';
|
import WalletSpendableBalanceHelp from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({ balance: selectBalance(state) });
|
||||||
balance: selectBalance(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select)(WalletSpendableBalanceHelp);
|
export default connect(select)(WalletSpendableBalanceHelp);
|
||||||
|
|
|
@ -1,33 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = { balance: number, inline?: boolean };
|
||||||
balance: number,
|
|
||||||
inline?: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
function WalletSpendableBalanceHelp(props: Props) {
|
function WalletSpendableBalanceHelp(props: Props) {
|
||||||
const { balance, inline } = props;
|
const { balance, inline } = props;
|
||||||
|
|
||||||
if (!balance) {
|
const getMessage = (text: string) => (
|
||||||
return null;
|
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>{text}</I18nMessage>
|
||||||
}
|
);
|
||||||
|
|
||||||
return inline ? (
|
return !balance ? null : inline ? (
|
||||||
<span className="help--spendable">
|
<span className="help--spendable">{getMessage(__('%balance% available.'))}</span>
|
||||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
|
||||||
%balance% available.
|
|
||||||
</I18nMessage>
|
|
||||||
</span>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="help">
|
<div className="help">{getMessage(__('Your immediately spendable balance is %balance%.'))}</div>
|
||||||
<I18nMessage tokens={{ balance: <CreditAmount amount={balance} precision={4} /> }}>
|
|
||||||
Your immediately spendable balance is %balance%.
|
|
||||||
</I18nMessage>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectBalance } from 'redux/selectors/wallet';
|
import { selectBalance } from 'redux/selectors/wallet';
|
||||||
import WalletTipAmountSelector from './view';
|
import WalletTipAmountSelector from './view';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state) => ({ balance: selectBalance(state) });
|
||||||
balance: selectBalance(state),
|
|
||||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
|
||||||
// claim: makeSelectClaimForUri(props.uri)(state),
|
|
||||||
// claim: makeSelectClaimForUri(props.uri, false)(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select)(WalletTipAmountSelector);
|
export default connect(select)(WalletTipAmountSelector);
|
||||||
|
|
|
@ -1,194 +1,254 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import 'scss/component/_wallet-tip-selector.scss';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||||
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { FormField } from 'component/common/form';
|
|
||||||
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
|
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||||
|
|
||||||
const TAB_FIAT = 'TabFiat';
|
const TAB_FIAT = 'TabFiat';
|
||||||
const TAB_LBC = 'TabLBC';
|
const TAB_LBC = 'TabLBC';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
balance: number,
|
|
||||||
amount: number,
|
|
||||||
onChange: (number) => void,
|
|
||||||
isAuthenticated: boolean,
|
|
||||||
claim: StreamClaim,
|
|
||||||
uri: string,
|
|
||||||
onTipErrorChange: (string) => void,
|
|
||||||
activeTab: string,
|
activeTab: string,
|
||||||
shouldDisableReviewButton: (boolean) => void,
|
amount: number,
|
||||||
|
balance: number,
|
||||||
|
claim: StreamClaim,
|
||||||
|
convertedAmount?: number,
|
||||||
|
customTipAmount?: number,
|
||||||
|
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) {
|
function WalletTipAmountSelector(props: Props) {
|
||||||
const { balance, amount, onChange, activeTab, claim, onTipErrorChange, shouldDisableReviewButton } = props;
|
const {
|
||||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
activeTab,
|
||||||
const [tipError, setTipError] = React.useState();
|
amount,
|
||||||
|
balance,
|
||||||
|
claim,
|
||||||
|
convertedAmount,
|
||||||
|
customTipAmount,
|
||||||
|
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 [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
|
// if it's fiat but there's no card saved OR the creator can't receive fiat tips
|
||||||
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
|
const shouldDisableFiatSelectors = activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip);
|
||||||
|
if (setDisableSubmitButton) setDisableSubmitButton(shouldDisableFiatSelectors);
|
||||||
|
|
||||||
|
// setup variables for tip API
|
||||||
|
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||||
|
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* whether tip amount selection/review functionality should be disabled
|
* whether tip amount selection/review functionality should be disabled
|
||||||
* @param [amount] LBC amount (optional)
|
* @param [amount] LBC amount (optional)
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function shouldDisableAmountSelector(amount) {
|
function shouldDisableAmountSelector(amount: number) {
|
||||||
// if it's LBC but the balance isn't enough, or fiat conditions met
|
// if it's LBC but the balance isn't enough, or fiat conditions met
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
return (amount > balance && activeTab !== TAB_FIAT) || shouldDisableFiatSelectors;
|
return (
|
||||||
|
((amount > balance || balance === 0) && activeTab !== TAB_FIAT) ||
|
||||||
|
shouldDisableFiatSelectors ||
|
||||||
|
(customTipAmount && fiatConversion && activeTab !== TAB_FIAT && exchangeRate
|
||||||
|
? 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if creator has a payment method saved
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (stripeEnvironment) {
|
|
||||||
Lbryio.call(
|
|
||||||
'customer',
|
|
||||||
'status',
|
|
||||||
{
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
).then((customerStatusResponse) => {
|
|
||||||
const defaultPaymentMethodId =
|
|
||||||
customerStatusResponse.Customer &&
|
|
||||||
customerStatusResponse.Customer.invoice_settings &&
|
|
||||||
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
|
||||||
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
|
||||||
|
|
||||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [stripeEnvironment]);
|
|
||||||
|
|
||||||
//
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (stripeEnvironment) {
|
|
||||||
Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'check',
|
|
||||||
{
|
|
||||||
channel_claim_id: channelClaimId,
|
|
||||||
channel_name: tipChannelName,
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
)
|
|
||||||
.then((accountCheckResponse) => {
|
|
||||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
|
||||||
setCanReceiveFiatTip(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
// console.log(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [stripeEnvironment]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// setHasSavedCard(false);
|
|
||||||
// setCanReceiveFiatTip(true);
|
|
||||||
|
|
||||||
let regexp,
|
|
||||||
tipError = '';
|
|
||||||
|
|
||||||
if (amount === 0) {
|
|
||||||
tipError = __('Amount must be a positive number');
|
|
||||||
} else if (!amount || typeof amount !== 'number') {
|
|
||||||
tipError = __('Amount must be a number');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it's not fiat, aka it's boost or lbc tip
|
|
||||||
else if (activeTab !== TAB_FIAT) {
|
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
|
||||||
const validTipInput = regexp.test(String(amount));
|
|
||||||
|
|
||||||
if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
|
||||||
} else if (amount === balance) {
|
|
||||||
tipError = __('Please decrease the amount to account for transaction fees');
|
|
||||||
} else if (amount > balance) {
|
|
||||||
tipError = __('Not enough Credits');
|
|
||||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
|
||||||
tipError = __('Amount must be higher');
|
|
||||||
}
|
|
||||||
// if tip fiat tab
|
|
||||||
} else {
|
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
|
||||||
const validTipInput = regexp.test(String(amount));
|
|
||||||
|
|
||||||
if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 2 decimal places');
|
|
||||||
} else if (amount < 1) {
|
|
||||||
tipError = __('Amount must be at least one dollar');
|
|
||||||
} else if (amount > 1000) {
|
|
||||||
tipError = __('Amount cannot be over 1000 dollars');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTipError(tipError);
|
|
||||||
onTipErrorChange(tipError);
|
|
||||||
}, [amount, balance, setTipError, activeTab]);
|
|
||||||
|
|
||||||
// parse number as float and sets it in the parent component
|
// parse number as float and sets it in the parent component
|
||||||
function handleCustomPriceChange(amount: number) {
|
function handleCustomPriceChange(amount: number) {
|
||||||
const tipAmount = parseFloat(amount);
|
const tipAmountValue = parseFloat(amount);
|
||||||
|
onChange(tipAmountValue);
|
||||||
onChange(tipAmount);
|
if (fiatConversion && exchangeRate && setConvertedAmount && convertedAmount !== tipAmountValue * exchangeRate) {
|
||||||
|
setConvertedAmount(tipAmountValue * exchangeRate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
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',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
).then((customerStatusResponse) => {
|
||||||
|
const defaultPaymentMethodId =
|
||||||
|
customerStatusResponse.Customer &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||||
|
|
||||||
|
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||||
|
});
|
||||||
|
}, [setHasSavedCard]);
|
||||||
|
|
||||||
|
// check if creator has a tip account saved REMOVE
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!stripeEnvironment) return;
|
||||||
|
|
||||||
|
Lbryio.call(
|
||||||
|
'account',
|
||||||
|
'check',
|
||||||
|
{
|
||||||
|
channel_claim_id: channelClaimId,
|
||||||
|
channel_name: tipChannelName,
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
)
|
||||||
|
.then((accountCheckResponse) => {
|
||||||
|
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||||
|
setCanReceiveFiatTip(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [canReceiveFiatTip, channelClaimId, tipChannelName]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let regexp;
|
||||||
|
|
||||||
|
if (amount === 0) {
|
||||||
|
setTipError(__('Amount cannot be zero.'));
|
||||||
|
} else if (!amount || typeof amount !== 'number') {
|
||||||
|
setTipError(__('Amount must be a number.'));
|
||||||
|
} else {
|
||||||
|
// if it's not fiat, aka it's boost or lbc tip
|
||||||
|
if (activeTab !== TAB_FIAT) {
|
||||||
|
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||||
|
const validTipInput = regexp.test(String(amount));
|
||||||
|
|
||||||
|
if (!validTipInput) {
|
||||||
|
setTipError(__('Amount must have no more than 8 decimal places'));
|
||||||
|
} else if (amount === balance) {
|
||||||
|
setTipError(__('Please decrease the amount to account for transaction fees'));
|
||||||
|
} else if (amount > balance || balance === 0) {
|
||||||
|
setTipError(__('Not enough Credits'));
|
||||||
|
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||||
|
setTipError(__('Amount must be higher'));
|
||||||
|
} else 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'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTipError(false);
|
||||||
|
}
|
||||||
|
// if tip fiat tab REMOVE
|
||||||
|
} else {
|
||||||
|
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||||
|
const validTipInput = regexp.test(String(amount));
|
||||||
|
|
||||||
|
if (!validTipInput) {
|
||||||
|
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||||
|
} else if (amount < 1) {
|
||||||
|
setTipError(__('Amount must be at least one dollar'));
|
||||||
|
} else if (amount > 1000) {
|
||||||
|
setTipError(__('Amount cannot be over 1000 dollars'));
|
||||||
|
} else if (customTipAmount && amount < customTipAmount) {
|
||||||
|
setTipError(
|
||||||
|
__('Amount is lower than price of $%price_amount%', {
|
||||||
|
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTipError(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeTab, amount, balance, convertedAmount, customTipAmount, exchangeRate, setTipError]);
|
||||||
|
|
||||||
|
const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
{tipAmountsToDisplay &&
|
||||||
<Button
|
tipAmountsToDisplay.map((defaultAmount) => (
|
||||||
key={defaultAmount}
|
<Button
|
||||||
disabled={shouldDisableAmountSelector(defaultAmount)}
|
key={defaultAmount}
|
||||||
button="alt"
|
disabled={shouldDisableAmountSelector(defaultAmount)}
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
button="alt"
|
||||||
'button-toggle--active': defaultAmount === amount && !useCustomTip,
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
'button-toggle--disabled': amount > balance,
|
'button-toggle--active':
|
||||||
})}
|
convertToTwoDecimalsOrMore(defaultAmount) === convertToTwoDecimalsOrMore(amount) && !useCustomTip,
|
||||||
label={defaultAmount}
|
'button-toggle--disabled': amount > balance,
|
||||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
})}
|
||||||
onClick={() => {
|
label={defaultAmount}
|
||||||
handleCustomPriceChange(defaultAmount);
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||||
setUseCustomTip(false);
|
onClick={() => {
|
||||||
}}
|
handleCustomPriceChange(defaultAmount);
|
||||||
/>
|
setUseCustomTip(false);
|
||||||
))}
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
disabled={shouldDisableFiatSelectors}
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
'button-toggle--active': useCustomTip,
|
'button-toggle--active': useCustomTip,
|
||||||
})}
|
})}
|
||||||
|
@ -207,60 +267,26 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
{customTipAmount &&
|
||||||
<>
|
fiatConversion &&
|
||||||
<div className="help">
|
activeTab !== TAB_FIAT &&
|
||||||
<span className="help--spendable">
|
getHelpMessage(
|
||||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
__('This support is priced in $USD.') +
|
||||||
{__('Tip Creators')}
|
(convertedAmount
|
||||||
</span>
|
? ' ' +
|
||||||
</div>
|
__('The current exchange rate for the submitted LBC amount is ~ $%exchange_amount%.', {
|
||||||
</>
|
exchange_amount: convertToTwoDecimalsOrMore(convertedAmount),
|
||||||
)}
|
})
|
||||||
|
: '')
|
||||||
{/* has card saved but cant creator cant receive tips */}
|
)}
|
||||||
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
|
||||||
<>
|
|
||||||
<div className="help">
|
|
||||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* has card saved but cant creator cant receive tips */}
|
|
||||||
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
|
||||||
<>
|
|
||||||
<div className="help">
|
|
||||||
<span className="help--spendable">Send a tip directly from your attached card</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* custom number input form */}
|
{/* custom number input form */}
|
||||||
{useCustomTip && (
|
{useCustomTip && (
|
||||||
<div className="comment__tip-input">
|
<div className="walletTipSelector__input">
|
||||||
<FormField
|
<FormField
|
||||||
autoFocus
|
autoFocus={!isMobile}
|
||||||
name="tip-input"
|
name="tip-input"
|
||||||
disabled={shouldDisableAmountSelector()}
|
disabled={!customTipAmount && shouldDisableAmountSelector(0)}
|
||||||
label={
|
|
||||||
activeTab === TAB_LBC ? (
|
|
||||||
<React.Fragment>
|
|
||||||
{__('Custom support amount')}{' '}
|
|
||||||
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
|
|
||||||
(%lbc_balance% available)
|
|
||||||
</I18nMessage>
|
|
||||||
</React.Fragment>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)
|
|
||||||
|
|
||||||
// <>
|
|
||||||
// <div className="">
|
|
||||||
// <span className="help--spendable">Send a tip directly from your attached card</span>
|
|
||||||
// </div>
|
|
||||||
// </>
|
|
||||||
}
|
|
||||||
error={tipError}
|
error={tipError}
|
||||||
min="0"
|
min="0"
|
||||||
step="any"
|
step="any"
|
||||||
|
@ -274,35 +300,17 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
|
|
||||||
{/* lbc tab */}
|
{/* lbc tab */}
|
||||||
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
||||||
{/* fiat button but no card saved */}
|
{activeTab === TAB_FIAT &&
|
||||||
{!useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
(!hasCardSaved
|
||||||
<>
|
? getHelpMessage(
|
||||||
<div className="help">
|
<>
|
||||||
<span className="help--spendable">
|
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
{' ' + __('To Tip Creators')}
|
||||||
{__('Tip Creators')}
|
</>
|
||||||
</span>
|
)
|
||||||
</div>
|
: !canReceiveFiatTip
|
||||||
</>
|
? getHelpMessage(__('Only creators that verify cash accounts can receive tips'))
|
||||||
)}
|
: getHelpMessage(__('Send a tip directly from your attached card')))}
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,6 +232,7 @@ export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
|
||||||
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
||||||
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
|
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
|
||||||
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
||||||
|
export const SET_MENTION_SEARCH_RESULTS = 'SET_MENTION_SEARCH_RESULTS';
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
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 ARTISTS = 'Artists';
|
||||||
export const MYSTERIES = 'Mysteries';
|
export const MYSTERIES = 'Mysteries';
|
||||||
export const TECHNOLOGY = 'Technology';
|
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
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const useEffectOnce = effect => {
|
const useEffectOnce = (effect) => {
|
||||||
React.useEffect(effect, []);
|
React.useEffect(effect, []);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ function useUnmount(fn: () => any): void {
|
||||||
useEffectOnce(() => () => fnRef.current());
|
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 [state, setState] = React.useState(value);
|
||||||
const timeout = React.useRef();
|
const timeout = React.useRef();
|
||||||
const nextValue = React.useRef(null);
|
const nextValue = React.useRef(null);
|
||||||
|
@ -37,7 +37,7 @@ export function useThrottle(value: string, ms: number = 200) {
|
||||||
nextValue.current = value;
|
nextValue.current = value;
|
||||||
hasNextValue.current = true;
|
hasNextValue.current = true;
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [ms, value]);
|
||||||
|
|
||||||
useUnmount(() => {
|
useUnmount(() => {
|
||||||
timeout.current && clearTimeout(timeout.current);
|
timeout.current && clearTimeout(timeout.current);
|
||||||
|
@ -45,5 +45,3 @@ export function useThrottle(value: string, ms: number = 200) {
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useThrottle;
|
|
||||||
|
|
|
@ -87,7 +87,6 @@ function DiscoverPage(props: Props) {
|
||||||
icon={ICONS.SUBSCRIBE}
|
icon={ICONS.SUBSCRIBE}
|
||||||
iconColor="red"
|
iconColor="red"
|
||||||
onClick={handleFollowClick}
|
onClick={handleFollowClick}
|
||||||
requiresAuth={false}
|
|
||||||
label={label}
|
label={label}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -552,6 +552,7 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
* @param claim_id - File claim id
|
* @param claim_id - File claim id
|
||||||
* @param parent_id - What is this?
|
* @param parent_id - What is this?
|
||||||
* @param uri
|
* @param uri
|
||||||
|
* @param sticker
|
||||||
* @param {string} [txid] Optional transaction id
|
* @param {string} [txid] Optional transaction id
|
||||||
* @param {string} [payment_intent_id] Optional transaction id
|
* @param {string} [payment_intent_id] Optional transaction id
|
||||||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||||
|
@ -561,10 +562,11 @@ export function doCommentCreate(
|
||||||
comment: string = '',
|
comment: string = '',
|
||||||
claim_id: string = '',
|
claim_id: string = '',
|
||||||
parent_id?: string,
|
parent_id?: string,
|
||||||
uri: string,
|
uri: string, // REMOVE ed livestream
|
||||||
txid?: string,
|
txid?: string,
|
||||||
payment_intent_id?: string,
|
payment_intent_id?: string,
|
||||||
environment?: string
|
environment?: string,
|
||||||
|
sticker: boolean
|
||||||
) {
|
) {
|
||||||
return async (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -577,9 +579,7 @@ export function doCommentCreate(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({ type: ACTIONS.COMMENT_CREATE_STARTED });
|
||||||
type: ACTIONS.COMMENT_CREATE_STARTED,
|
|
||||||
});
|
|
||||||
|
|
||||||
let signatureData;
|
let signatureData;
|
||||||
if (activeChannelClaim) {
|
if (activeChannelClaim) {
|
||||||
|
@ -592,12 +592,8 @@ export function doCommentCreate(
|
||||||
}
|
}
|
||||||
|
|
||||||
// send a notification
|
// send a notification
|
||||||
if (parent_id) {
|
const notification = parent_id && makeSelectNotificationForCommentId(parent_id)(state);
|
||||||
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
if (notification && !notification.is_seen) dispatch(doSeeNotifications([notification.id]));
|
||||||
if (notification && !notification.is_seen) {
|
|
||||||
dispatch(doSeeNotifications([notification.id]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signatureData) {
|
if (!signatureData) {
|
||||||
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
|
@ -613,6 +609,7 @@ export function doCommentCreate(
|
||||||
parent_id: parent_id,
|
parent_id: parent_id,
|
||||||
signature: signatureData.signature,
|
signature: signatureData.signature,
|
||||||
signing_ts: signatureData.signing_ts,
|
signing_ts: signatureData.signing_ts,
|
||||||
|
sticker: sticker,
|
||||||
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
|
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
|
||||||
...(payment_intent_id ? { payment_intent_id } : {}), // add payment_intent_id if it exists
|
...(payment_intent_id ? { payment_intent_id } : {}), // add payment_intent_id if it exists
|
||||||
...(environment ? { environment } : {}), // add environment for stripe if it exists
|
...(environment ? { environment } : {}), // add environment for stripe if it exists
|
||||||
|
|
|
@ -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) => {
|
export const doFetchRecommendedContent = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const claim = selectClaimForUri(state, uri);
|
const claim = selectClaimForUri(state, uri);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import Lbry from 'lbry';
|
import Lbry from 'lbry';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import {
|
import {
|
||||||
selectBalance,
|
selectBalance,
|
||||||
|
@ -12,7 +13,6 @@ import {
|
||||||
import { creditsToString } from 'util/format-credits';
|
import { creditsToString } from 'util/format-credits';
|
||||||
import { selectMyClaimsRaw, selectClaimsById } from 'redux/selectors/claims';
|
import { selectMyClaimsRaw, selectClaimsById } from 'redux/selectors/claims';
|
||||||
import { doFetchChannelListMine, doFetchClaimListMine, doClaimSearch } from 'redux/actions/claims';
|
import { doFetchChannelListMine, doFetchClaimListMine, doClaimSearch } from 'redux/actions/claims';
|
||||||
|
|
||||||
const FIFTEEN_SECONDS = 15000;
|
const FIFTEEN_SECONDS = 15000;
|
||||||
let walletBalancePromise = null;
|
let walletBalancePromise = null;
|
||||||
|
|
||||||
|
@ -700,3 +700,47 @@ export const doCheckPendingTxs = () => (dispatch, getState) => {
|
||||||
checkTxList();
|
checkTxList();
|
||||||
}, 30000);
|
}, 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: {},
|
resultsByQuery: {},
|
||||||
hasReachedMaxResultsLength: {},
|
hasReachedMaxResultsLength: {},
|
||||||
searching: false,
|
searching: false,
|
||||||
|
results: [],
|
||||||
|
mentionQuery: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
|
@ -66,6 +68,12 @@ export default handleActions(
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ACTIONS.SET_MENTION_SEARCH_RESULTS]: (state: SearchState, action: SearchSuccess): SearchState => ({
|
||||||
|
...state,
|
||||||
|
results: action.data.uris,
|
||||||
|
mentionQuery: action.data.query,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
defaultState
|
defaultState
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,11 +3,19 @@ import { createSelector } from 'reselect';
|
||||||
import { createCachedSelector } from 're-reselect';
|
import { createCachedSelector } from 're-reselect';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
|
import { selectMentionSearchResults, selectMentionQuery } from 'redux/selectors/search';
|
||||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||||
import { selectClaimsById, selectMyActiveClaims, selectClaimIdForUri } from 'redux/selectors/claims';
|
import {
|
||||||
import { isClaimNsfw } from 'util/claim';
|
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 || {};
|
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 selectPinnedCommentsById = (state: State) => selectState(state).pinnedCommentsById;
|
||||||
export const selectPinnedCommentsForUri = createCachedSelector(
|
export const selectPinnedCommentsForUri = createCachedSelector(
|
||||||
selectCommentsByUri,
|
selectClaimIdForUri,
|
||||||
selectCommentsById,
|
selectCommentsById,
|
||||||
selectPinnedCommentsById,
|
selectPinnedCommentsById,
|
||||||
(state, uri) => uri,
|
(state, uri) => uri,
|
||||||
(byUri, byId, pinnedCommentsById, uri) => {
|
(claimId, byId, pinnedCommentsById, uri) => {
|
||||||
const claimId = byUri[uri];
|
|
||||||
const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId];
|
const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId];
|
||||||
const pinnedComments = [];
|
const pinnedComments = [];
|
||||||
|
|
||||||
|
@ -68,7 +75,7 @@ export const selectPinnedCommentsForUri = createCachedSelector(
|
||||||
|
|
||||||
return pinnedComments;
|
return pinnedComments;
|
||||||
}
|
}
|
||||||
)((state, uri) => uri);
|
)((state, uri) => String(uri));
|
||||||
|
|
||||||
export const selectModerationBlockList = createSelector(
|
export const selectModerationBlockList = createSelector(
|
||||||
(state) => selectState(state).moderationBlockList,
|
(state) => selectState(state).moderationBlockList,
|
||||||
|
@ -128,7 +135,7 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment
|
||||||
return comments;
|
return comments;
|
||||||
});
|
});
|
||||||
|
|
||||||
// no superchats?
|
// no superchats
|
||||||
export const selectSuperchatsByUri = (state: State) => selectState(state).superChatsByUri;
|
export const selectSuperchatsByUri = (state: State) => selectState(state).superChatsByUri;
|
||||||
|
|
||||||
export const selectTopLevelCommentsByClaimId = createSelector(
|
export const selectTopLevelCommentsByClaimId = createSelector(
|
||||||
|
@ -180,6 +187,7 @@ export const selectCommentIdsForUri = (state: State, uri: string) => {
|
||||||
return commentIdsByClaimId[claimId];
|
return commentIdsByClaimId[claimId];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// deprecated
|
||||||
export const makeSelectCommentIdsForUri = (uri: string) =>
|
export const makeSelectCommentIdsForUri = (uri: string) =>
|
||||||
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
|
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
|
||||||
const claimId = byUri[uri];
|
const claimId = byUri[uri];
|
||||||
|
@ -188,7 +196,8 @@ export const makeSelectCommentIdsForUri = (uri: string) =>
|
||||||
|
|
||||||
const filterCommentsDepOnList = {
|
const filterCommentsDepOnList = {
|
||||||
claimsById: selectClaimsById,
|
claimsById: selectClaimsById,
|
||||||
myClaims: selectMyActiveClaims,
|
myClaimIds: selectMyClaimIdsRaw,
|
||||||
|
myChannelClaimIds: selectMyChannelClaimIds,
|
||||||
mutedChannels: selectMutedChannels,
|
mutedChannels: selectMutedChannels,
|
||||||
personalBlockList: selectModerationBlockList,
|
personalBlockList: selectModerationBlockList,
|
||||||
blacklistedMap: selectBlacklistedOutpointMap,
|
blacklistedMap: selectBlacklistedOutpointMap,
|
||||||
|
@ -206,28 +215,29 @@ export const selectFetchingBlockedWords = (state: State) => selectState(state).f
|
||||||
export const selectCommentsForUri = createCachedSelector(
|
export const selectCommentsForUri = createCachedSelector(
|
||||||
(state, uri) => uri,
|
(state, uri) => uri,
|
||||||
selectCommentsByClaimId,
|
selectCommentsByClaimId,
|
||||||
selectCommentsByUri,
|
selectClaimIdForUri,
|
||||||
...Object.values(filterCommentsDepOnList),
|
...Object.values(filterCommentsDepOnList),
|
||||||
(uri, byClaimId, byUri, ...filterInputs) => {
|
(uri, byClaimId, claimId, ...filterInputs) => {
|
||||||
const claimId = byUri[uri];
|
|
||||||
const comments = byClaimId && byClaimId[claimId];
|
const comments = byClaimId && byClaimId[claimId];
|
||||||
return filterComments(comments, claimId, filterInputs);
|
return filterComments(comments, claimId, filterInputs);
|
||||||
}
|
}
|
||||||
)((state, uri) => uri);
|
)((state, uri) => String(uri));
|
||||||
|
|
||||||
export const selectTopLevelCommentsForUri = createCachedSelector(
|
export const selectTopLevelCommentsForUri = createCachedSelector(
|
||||||
(state, uri) => uri,
|
(state, uri) => uri,
|
||||||
(state, uri, maxCount) => maxCount,
|
(state, uri, maxCount) => maxCount,
|
||||||
selectTopLevelCommentsByClaimId,
|
selectTopLevelCommentsByClaimId,
|
||||||
selectCommentsByUri,
|
selectClaimIdForUri,
|
||||||
...Object.values(filterCommentsDepOnList),
|
...Object.values(filterCommentsDepOnList),
|
||||||
(uri, maxCount = -1, byClaimId, byUri, ...filterInputs) => {
|
(uri, maxCount = -1, byClaimId, claimId, ...filterInputs) => {
|
||||||
const claimId = byUri[uri];
|
|
||||||
const comments = byClaimId && byClaimId[claimId];
|
const comments = byClaimId && byClaimId[claimId];
|
||||||
const filtered = filterComments(comments, claimId, filterInputs);
|
if (comments) {
|
||||||
return maxCount > 0 ? filtered.slice(0, maxCount) : filtered;
|
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) =>
|
export const makeSelectTopLevelTotalCommentsForUri = (uri: string) =>
|
||||||
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||||
|
@ -259,24 +269,25 @@ export const selectRepliesForParentId = createCachedSelector(
|
||||||
|
|
||||||
return filterComments(comments, undefined, filterInputs);
|
return filterComments(comments, undefined, filterInputs);
|
||||||
}
|
}
|
||||||
)((state, id: string) => id);
|
)((state, id: string) => String(id));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* filterComments
|
* filterComments
|
||||||
*
|
*
|
||||||
* @param comments List of comments to filter.
|
* @param comments List of comments to filter.
|
||||||
* @param claimId The claim that `comments` reside in.
|
* @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 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;
|
acc[filterCommentsPropKeys[i]] = cur;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
claimsById,
|
claimsById,
|
||||||
myClaims,
|
myClaimIds,
|
||||||
|
myChannelClaimIds,
|
||||||
mutedChannels,
|
mutedChannels,
|
||||||
personalBlockList,
|
personalBlockList,
|
||||||
blacklistedMap,
|
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
|
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
||||||
if (channelClaim) {
|
if (channelClaim) {
|
||||||
if (myClaims && myClaims.size > 0) {
|
if ((myClaimIds && myClaimIds.size > 0) || (myChannelClaimIds && myChannelClaimIds.length > 0)) {
|
||||||
const claimIsMine = channelClaim.is_my_output || myClaims.has(channelClaim.claim_id);
|
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) {
|
if (claimIsMine) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -316,7 +331,7 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
|
||||||
}
|
}
|
||||||
|
|
||||||
if (claimId) {
|
if (claimId) {
|
||||||
const claimIdIsMine = myClaims && myClaims.size > 0 && myClaims.has(claimId);
|
const claimIdIsMine = myClaimIds && myClaimIds.size > 0 && myClaimIds.includes(claimId);
|
||||||
if (!claimIdIsMine) {
|
if (!claimIdIsMine) {
|
||||||
if (personalBlockList.includes(comment.channel_url)) {
|
if (personalBlockList.includes(comment.channel_url)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -376,25 +391,88 @@ export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) =>
|
||||||
return blockingByUri[uri] || unBlockingByUri[uri];
|
return blockingByUri[uri] || unBlockingByUri[uri];
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectSuperChatDataForUri = (uri: string) =>
|
export const selectSuperChatDataForUri = (state: State, uri: string) => {
|
||||||
createSelector(selectSuperchatsByUri, (byUri) => {
|
const byUri = selectSuperchatsByUri(state);
|
||||||
return byUri[uri];
|
return byUri[uri];
|
||||||
});
|
};
|
||||||
|
|
||||||
export const makeSelectSuperChatsForUri = (uri: string) =>
|
export const selectSuperChatsForUri = (state: State, uri: string) => {
|
||||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
const superChatData = selectSuperChatDataForUri(state, uri);
|
||||||
if (!superChatData) {
|
return superChatData ? superChatData.comments : undefined;
|
||||||
return 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) =>
|
let hasNewResolvedResults = false;
|
||||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
if (searchUris && searchUris.length > 0) {
|
||||||
if (!superChatData) {
|
searchUris.forEach((uri) => {
|
||||||
return 0;
|
// 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,
|
selectClaimsByUri,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectClaimForClaimId,
|
makeSelectClaimForClaimId,
|
||||||
makeSelectClaimIsNsfw,
|
selectClaimIsNsfwForUri,
|
||||||
makeSelectPendingClaimForUri,
|
makeSelectPendingClaimForUri,
|
||||||
selectIsUriResolving,
|
selectIsUriResolving,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import { parseURI } from 'util/lbryURI';
|
||||||
import { isClaimNsfw } from 'util/claim';
|
import { isClaimNsfw } from 'util/claim';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { createCachedSelector } from 're-reselect';
|
||||||
import { createNormalizedSearchKey, getRecommendationSearchOptions } from 'util/search';
|
import { createNormalizedSearchKey, getRecommendationSearchOptions } from 'util/search';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectHistory } from 'redux/selectors/content';
|
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 selectState = (state: State): SearchState => state.search;
|
||||||
|
|
||||||
export const selectSearchValue: (state: State) => string = createSelector(selectState, (state) => state.searchQuery);
|
// $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 = createSelector(
|
export const selectSearchOptions: (state: State) => SearchOptions = (state) => selectState(state).options;
|
||||||
selectState,
|
export const selectIsSearching: (state: State) => boolean = (state) => selectState(state).searching;
|
||||||
(state) => state.options
|
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = (state) =>
|
||||||
);
|
selectState(state).resultsByQuery;
|
||||||
|
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) =>
|
||||||
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching);
|
selectState(state).hasReachedMaxResultsLength;
|
||||||
|
export const selectMentionSearchResults: (state: State) => Array<string> = (state) => selectState(state).results;
|
||||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
export const selectMentionQuery: (state: State) => string = (state) => selectState(state).mentionQuery;
|
||||||
selectState,
|
|
||||||
(state) => state.resultsByQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector(
|
|
||||||
selectState,
|
|
||||||
(state) => state.hasReachedMaxResultsLength
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
|
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
|
||||||
createSelector(selectSearchResultByQuery, (byQuery) => {
|
createSelector(selectSearchResultByQuery, (byQuery) => {
|
||||||
|
@ -60,93 +53,93 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
|
||||||
return hasReachedMaxResultsLength[query];
|
return hasReachedMaxResultsLength[query];
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectRecommendedContentForUri = (uri: string) =>
|
export const selectRecommendedContentForUri = createCachedSelector(
|
||||||
createSelector(
|
(state, uri) => uri,
|
||||||
selectHistory,
|
selectHistory,
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
selectShowMatureContent,
|
selectShowMatureContent,
|
||||||
selectMutedChannels,
|
selectMutedChannels,
|
||||||
selectAllCostInfoByUri,
|
selectAllCostInfoByUri,
|
||||||
selectSearchResultByQuery,
|
selectSearchResultByQuery,
|
||||||
makeSelectClaimIsNsfw(uri),
|
selectClaimIsNsfwForUri, // (state, uri)
|
||||||
(history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
(uri, history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
||||||
const claim = claimsByUri[uri];
|
const claim = claimsByUri[uri];
|
||||||
|
|
||||||
if (!claim) return;
|
if (!claim) return;
|
||||||
|
|
||||||
let recommendedContent;
|
let recommendedContent;
|
||||||
// always grab the claimId - this value won't change for filtering
|
// always grab the claimId - this value won't change for filtering
|
||||||
const currentClaimId = claim.claim_id;
|
const currentClaimId = claim.claim_id;
|
||||||
|
|
||||||
const { title } = claim.value;
|
const { title } = claim.value;
|
||||||
|
|
||||||
if (!title) return;
|
if (!title) return;
|
||||||
|
|
||||||
const options: {
|
const options: {
|
||||||
size: number,
|
size: number,
|
||||||
nsfw?: boolean,
|
nsfw?: boolean,
|
||||||
isBackgroundSearch?: boolean,
|
isBackgroundSearch?: boolean,
|
||||||
} = { size: 20, nsfw: matureEnabled, isBackgroundSearch: true };
|
} = { size: 20, nsfw: matureEnabled, isBackgroundSearch: true };
|
||||||
|
|
||||||
if (matureEnabled || (!matureEnabled && !isMature)) {
|
if (matureEnabled || (!matureEnabled && !isMature)) {
|
||||||
options[SEARCH_OPTIONS.RELATED_TO] = claim.claim_id;
|
options[SEARCH_OPTIONS.RELATED_TO] = claim.claim_id;
|
||||||
}
|
|
||||||
|
|
||||||
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
|
||||||
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
|
||||||
|
|
||||||
let searchResult = searchUrisByQuery[normalizedSearchQuery];
|
|
||||||
|
|
||||||
if (searchResult) {
|
|
||||||
// Filter from recommended: The same claim and blocked channels
|
|
||||||
recommendedContent = searchResult['uris'].filter((searchUri) => {
|
|
||||||
const searchClaim = claimsByUri[searchUri];
|
|
||||||
|
|
||||||
if (!searchClaim) return;
|
|
||||||
|
|
||||||
const signingChannel = searchClaim && searchClaim.signing_channel;
|
|
||||||
const channelUri = signingChannel && signingChannel.canonical_url;
|
|
||||||
const blockedMatch = blockedChannels.some((blockedUri) => blockedUri.includes(channelUri));
|
|
||||||
|
|
||||||
let isEqualUri;
|
|
||||||
try {
|
|
||||||
const { claimId: searchId } = parseURI(searchUri);
|
|
||||||
isEqualUri = searchId === currentClaimId;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
return !isEqualUri && !blockedMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Claim to play next: playable and free claims not played before in history
|
|
||||||
const nextUriToPlay = recommendedContent.filter((nextRecommendedUri) => {
|
|
||||||
const costInfo = costInfoByUri[nextRecommendedUri] && costInfoByUri[nextRecommendedUri].cost;
|
|
||||||
const recommendedClaim = claimsByUri[nextRecommendedUri];
|
|
||||||
const isVideo = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'video';
|
|
||||||
const isAudio = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'audio';
|
|
||||||
|
|
||||||
let historyMatch = false;
|
|
||||||
try {
|
|
||||||
const { claimId: nextRecommendedId } = parseURI(nextRecommendedUri);
|
|
||||||
|
|
||||||
historyMatch = history.some(
|
|
||||||
(historyItem) =>
|
|
||||||
(claimsByUri[historyItem.uri] && claimsByUri[historyItem.uri].claim_id) === nextRecommendedId
|
|
||||||
);
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
return !historyMatch && costInfo === 0 && (isVideo || isAudio);
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
const index = recommendedContent.indexOf(nextUriToPlay);
|
|
||||||
if (index > 0) {
|
|
||||||
const a = recommendedContent[0];
|
|
||||||
recommendedContent[0] = nextUriToPlay;
|
|
||||||
recommendedContent[index] = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return recommendedContent;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
||||||
|
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
||||||
|
|
||||||
|
let searchResult = searchUrisByQuery[normalizedSearchQuery];
|
||||||
|
|
||||||
|
if (searchResult) {
|
||||||
|
// Filter from recommended: The same claim and blocked channels
|
||||||
|
recommendedContent = searchResult['uris'].filter((searchUri) => {
|
||||||
|
const searchClaim = claimsByUri[searchUri];
|
||||||
|
|
||||||
|
if (!searchClaim) return;
|
||||||
|
|
||||||
|
const signingChannel = searchClaim && searchClaim.signing_channel;
|
||||||
|
const channelUri = signingChannel && signingChannel.canonical_url;
|
||||||
|
const blockedMatch = blockedChannels.some((blockedUri) => blockedUri.includes(channelUri));
|
||||||
|
|
||||||
|
let isEqualUri;
|
||||||
|
try {
|
||||||
|
const { claimId: searchId } = parseURI(searchUri);
|
||||||
|
isEqualUri = searchId === currentClaimId;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return !isEqualUri && !blockedMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Claim to play next: playable and free claims not played before in history
|
||||||
|
const nextUriToPlay = recommendedContent.filter((nextRecommendedUri) => {
|
||||||
|
const costInfo = costInfoByUri[nextRecommendedUri] && costInfoByUri[nextRecommendedUri].cost;
|
||||||
|
const recommendedClaim = claimsByUri[nextRecommendedUri];
|
||||||
|
const isVideo = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'video';
|
||||||
|
const isAudio = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'audio';
|
||||||
|
|
||||||
|
let historyMatch = false;
|
||||||
|
try {
|
||||||
|
const { claimId: nextRecommendedId } = parseURI(nextRecommendedUri);
|
||||||
|
|
||||||
|
historyMatch = history.some(
|
||||||
|
(historyItem) =>
|
||||||
|
(claimsByUri[historyItem.uri] && claimsByUri[historyItem.uri].claim_id) === nextRecommendedId
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return !historyMatch && costInfo === 0 && (isVideo || isAudio);
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const index = recommendedContent.indexOf(nextUriToPlay);
|
||||||
|
if (index > 0) {
|
||||||
|
const a = recommendedContent[0];
|
||||||
|
recommendedContent[0] = nextUriToPlay;
|
||||||
|
recommendedContent[index] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recommendedContent;
|
||||||
|
}
|
||||||
|
)((state, uri) => String(uri));
|
||||||
|
|
||||||
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
|
|
|
@ -20,6 +20,11 @@ export const selectSubscriptions = createSelector(
|
||||||
(state) => state.subscriptions && state.subscriptions.sort((a, b) => a.channelName.localeCompare(b.channelName))
|
(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);
|
export const selectFollowing = createSelector(selectState, (state) => state.following && state.following);
|
||||||
|
|
||||||
// Fetching list of users subscriptions
|
// Fetching list of users subscriptions
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
@import 'component/button';
|
@import 'component/button';
|
||||||
@import 'component/card';
|
@import 'component/card';
|
||||||
@import 'component/channel';
|
@import 'component/channel';
|
||||||
@import 'component/channel-mention';
|
@import 'component/_textarea-suggestions';
|
||||||
@import 'component/claim-list';
|
@import 'component/claim-list';
|
||||||
@import 'component/collection';
|
@import 'component/collection';
|
||||||
@import 'component/comments';
|
@import 'component/comments';
|
||||||
|
|
|
@ -549,12 +549,6 @@
|
||||||
.button--highlighted {
|
.button--highlighted {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--emoji {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
border-radius: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button__content {
|
.button__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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 {
|
.comment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -90,10 +67,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content_comment {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__thumbnail-wrapper {
|
.comment__thumbnail-wrapper {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
|
@ -136,24 +109,10 @@ $thumbnailWidthSmall: 1rem;
|
||||||
opacity: 0.6;
|
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 {
|
.comment__edit-input {
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__sc-preview-amount {
|
|
||||||
margin-right: var(--spacing-m);
|
|
||||||
font-size: var(--font-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__threadline {
|
.comment__threadline {
|
||||||
@extend .button--alt;
|
@extend .button--alt;
|
||||||
height: auto;
|
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 {
|
.comment--highlighted {
|
||||||
background: var(--color-comment-highlighted);
|
background: var(--color-comment-highlighted);
|
||||||
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
||||||
|
@ -429,8 +368,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
@extend .comment__action;
|
@extend .comment__action;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__action--nested,
|
.comment__action--nested {
|
||||||
.comment__create--nested-reply {
|
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||||
|
|
||||||
|
@ -477,20 +415,10 @@ $thumbnailWidthSmall: 1rem;
|
||||||
margin-right: var(--spacing-s);
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__tip-input {
|
|
||||||
margin: var(--spacing-s) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment--blocked {
|
.comment--blocked {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--min-amount-notice {
|
|
||||||
.icon {
|
|
||||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments-own {
|
.comments-own {
|
||||||
.section__actions {
|
.section__actions {
|
||||||
align-items: flex-start;
|
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 {
|
.form-field__quick-action {
|
||||||
float: right;
|
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
margin-top: 2.5%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__textarea-info {
|
.form-field__textarea-info {
|
||||||
|
@ -462,12 +460,6 @@ fieldset-group {
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__quick-emojis {
|
|
||||||
> *:not(:last-child) {
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
.form-field__internal-option {
|
.form-field__internal-option {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
|
|
|
@ -428,6 +428,7 @@
|
||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maybe remove all REMOVE
|
||||||
.main-wrapper--scrollbar {
|
.main-wrapper--scrollbar {
|
||||||
// The W3C future standard; currently supported by Firefox only.
|
// 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.
|
// It'll hopefully auto fallback to this when 'webkit-scrollbar' below is deprecated in the future.
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image
|
// Image
|
||||||
img:not(.channel-thumbnail__custom) {
|
img:not(.channel-thumbnail__custom):not(.emote) {
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
padding-top: var(--spacing-m);
|
padding-top: var(--spacing-m);
|
||||||
max-height: var(--inline-player-max-height);
|
max-height: var(--inline-player-max-height);
|
||||||
|
|
|
@ -13,9 +13,15 @@ $contentMaxWidth: 60rem;
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__create,
|
.commentCreate,
|
||||||
.comment__content {
|
.comment__content {
|
||||||
margin: var(--spacing-m);
|
margin: var(--spacing-m);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -25,7 +31,7 @@ $contentMaxWidth: 60rem;
|
||||||
.notification__icon {
|
.notification__icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin: auto;
|
margin-top: var(--spacing-xxs);
|
||||||
|
|
||||||
.icon__wrapper {
|
.icon__wrapper {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
|
@ -94,7 +100,7 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__content-wrapper {
|
.notificationContent__wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -121,7 +127,7 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__content-thumbnail {
|
.notificationContent__thumbnail {
|
||||||
@include thumbnail;
|
@include thumbnail;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -139,8 +145,13 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__text-wrapper {
|
.notificationText__wrapper {
|
||||||
max-width: calc(#{$contentMaxWidth} - (#{$thumbnailWidth} * 16 / 9) - var(--spacing-m));
|
max-width: calc(#{$contentMaxWidth} - (#{$thumbnailWidth} * 16 / 9) - var(--spacing-m));
|
||||||
|
|
||||||
|
.sticker__comment {
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__title {
|
.notification__title {
|
||||||
|
@ -247,7 +258,7 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__mark-seen {
|
.notification__markSeen {
|
||||||
height: 12px;
|
height: 12px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
border-radius: 50%;
|
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 {
|
.purchase-stuff {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
82
ui/scss/component/_sticker-selector.scss
Normal file
82
ui/scss/component/_sticker-selector.scss
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
@import '../init/vars';
|
||||||
|
|
||||||
|
.stickerSelector {
|
||||||
|
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
|
||||||
|
.stickerSelector__header {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: var(--spacing-s);
|
||||||
|
margin-bottom: 0;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-xxs);
|
||||||
|
|
||||||
|
.stickerSelector__headerTitle {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation__wrapper {
|
||||||
|
height: unset;
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
.navigation-links {
|
||||||
|
li {
|
||||||
|
.button {
|
||||||
|
padding: unset;
|
||||||
|
|
||||||
|
.button__content {
|
||||||
|
justify-content: unset;
|
||||||
|
flex-direction: unset;
|
||||||
|
width: unset;
|
||||||
|
|
||||||
|
.button__label {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
margin: 0 var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stickerSelector__list {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.stickerSelector__listBody {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 25vh;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
|
||||||
|
.button--file-action {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5.3rem;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: unset;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
|
||||||
|
.button__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.super-chat--light {
|
||||||
|
position: absolute;
|
||||||
|
display: inline;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-xsmall) {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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-weight: 400;
|
||||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
||||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
'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 {
|
hr {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
|
||||||
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
|
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
|
||||||
|
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
|
||||||
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
|
|
||||||
|
const ALL_VALID_STICKERS = [...FREE_GLOBAL_STICKERS, ...PAID_GLOBAL_STICKERS];
|
||||||
|
const stickerRegex = /(<stkr>:[A-Z0-9_]+:<stkr>)/;
|
||||||
|
|
||||||
// Mostly taken from Reddit's sorting functions
|
// Mostly taken from Reddit's sorting functions
|
||||||
// https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx
|
// https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx
|
||||||
|
@ -88,3 +92,19 @@ export function sortComments(sortProps: SortProps): Array<Comment> {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const buildValidSticker = (sticker: string) => `<stkr>${sticker}<stkr>`;
|
||||||
|
|
||||||
|
export function parseSticker(comment: string) {
|
||||||
|
const matchSticker = comment.match(stickerRegex);
|
||||||
|
const stickerValue = matchSticker && matchSticker[0];
|
||||||
|
const commentIsSticker = stickerValue && stickerValue.length === comment.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
commentIsSticker &&
|
||||||
|
ALL_VALID_STICKERS.find((sticker) => {
|
||||||
|
// $FlowFixMe
|
||||||
|
return sticker.name === stickerValue.replaceAll('<stkr>', '');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
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);
|
visit(tree, ['link'], visitor);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatedLinks = () => transform;
|
export const formattedLinks = () => transform;
|
||||||
|
|
||||||
// Main module
|
// Main module
|
||||||
export function inlineLinks() {
|
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:
|
dependencies:
|
||||||
"@babel/types" "^7.12.5"
|
"@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":
|
"@babel/helper-module-transforms@^7.11.0":
|
||||||
version "7.11.0"
|
version "7.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
|
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"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
|
||||||
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
|
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"
|
version "7.15.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
|
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==
|
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
|
||||||
|
@ -679,6 +686,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.8.0"
|
"@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":
|
"@babel/plugin-syntax-jsx@^7.8.3":
|
||||||
version "7.8.3"
|
version "7.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94"
|
||||||
|
@ -1160,6 +1174,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
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":
|
"@babel/template@^7.10.1", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||||
version "7.10.1"
|
version "7.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
|
||||||
|
@ -1257,6 +1278,14 @@
|
||||||
"@babel/helper-validator-identifier" "^7.14.9"
|
"@babel/helper-validator-identifier" "^7.14.9"
|
||||||
to-fast-properties "^2.0.0"
|
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":
|
"@datapunt/matomo-tracker-js@^0.1.4":
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@datapunt/matomo-tracker-js/-/matomo-tracker-js-0.1.4.tgz#1226f0964d2c062bf9392e9c2fd89838262b10df"
|
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"
|
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-2.0.1.tgz#810cbc595a21f0f94641eb2d7e8264063a3f84de"
|
||||||
integrity sha512-bGX4/yB2bPZwXm1DsxgoABgH0Cz7oFtXJgkerB8VrStYdTyvhGAULzNLRn9rVmeAuC3VUDXaXpZIlZAZHpsLIA==
|
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":
|
"@gar/promisify@^1.0.1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
|
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
|
||||||
|
@ -1321,6 +1451,85 @@
|
||||||
tough-cookie "^2.3.1"
|
tough-cookie "^2.3.1"
|
||||||
tough-cookie-web-storage-store "^1.0.0"
|
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":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||||
|
@ -1441,6 +1650,11 @@
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
|
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":
|
"@reach/auto-id@0.12.1":
|
||||||
version "0.12.1"
|
version "0.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.12.1.tgz#2e4a7250d2067ec16a9b4ea732695bc75572405c"
|
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.12.1.tgz#2e4a7250d2067ec16a9b4ea732695bc75572405c"
|
||||||
|
@ -1794,6 +2008,16 @@
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
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":
|
"@types/q@^1.5.1":
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/react-calendar/-/react-calendar-3.1.3.tgz#bd0947c28738f6419649be22d80624b05fde2fb9"
|
||||||
integrity sha512-4kvDfKta9bNnuRieuGYPxdDlh3UqRUKE8+fMbmZGk0Z/MdUGHupxXwPCWLbVH7FZU48o4bhT+XX8rfZrexdnAw==
|
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":
|
"@types/semver@^7.1.0":
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.1.0.tgz#c8c630d4c18cd326beff77404887596f96408408"
|
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"
|
identifierfy "^1.1.0"
|
||||||
minimatch-capture "^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:
|
babel-plugin-syntax-object-rest-spread@^6.8.0:
|
||||||
version "6.13.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"
|
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"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
|
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:
|
co@^4.6.0:
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
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"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
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:
|
convert-source-map@^1.7.0:
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
|
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"
|
js-yaml "^3.13.1"
|
||||||
parse-json "^4.0.0"
|
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:
|
country-data@^0.0.31:
|
||||||
version "0.0.31"
|
version "0.0.31"
|
||||||
resolved "https://registry.yarnpkg.com/country-data/-/country-data-0.0.31.tgz#80966b8e1d147fa6d6a589d32933f8793774956d"
|
resolved "https://registry.yarnpkg.com/country-data/-/country-data-0.0.31.tgz#80966b8e1d147fa6d6a589d32933f8793774956d"
|
||||||
|
@ -4076,6 +4360,11 @@ csso@^4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
css-tree "1.0.0-alpha.37"
|
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:
|
currency-symbol-map@~2:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/currency-symbol-map/-/currency-symbol-map-2.2.0.tgz#2b3c1872ff1ac2ce595d8273e58e1fff0272aea2"
|
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:
|
dependencies:
|
||||||
utila "~0.4"
|
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:
|
dom-scroll-into-view@^1.2.1:
|
||||||
version "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"
|
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"
|
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
|
||||||
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
|
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"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6650,6 +6947,14 @@ import-fresh@^3.0.0:
|
||||||
parent-module "^1.0.0"
|
parent-module "^1.0.0"
|
||||||
resolve-from "^4.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:
|
import-from@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
|
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"
|
version "16.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"
|
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:
|
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
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"
|
react-fit "^1.0.3"
|
||||||
update-input-width "^1.1.1"
|
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:
|
react@^16.8.2:
|
||||||
version "16.14.0"
|
version "16.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
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"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
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"
|
version "0.5.7"
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||||
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
||||||
|
@ -11580,6 +11900,11 @@ stylehacks@^4.0.0:
|
||||||
postcss "^7.0.0"
|
postcss "^7.0.0"
|
||||||
postcss-selector-parser "^3.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:
|
sumchecker@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
|
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"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
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:
|
yargs-parser@^11.1.1:
|
||||||
version "11.1.1"
|
version "11.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
||||||
|
|
Loading…
Reference in a new issue