Bringing in emotes, stickers, and refactors from ody (#7435)
* [New Feature] Comment Emotes (#125) * Refactor form-field * Create new Emote Menu * Add Emotes * Add Emote Selector and Emote Comment creation ability * Fix and Split CSS * [New Feature] Stickers (#131) * Refactor filePrice * Refactor Wallet Tip Components * Add backend sticker support for comments * Add stickers * Refactor commentCreate * Add Sticker Selector and sticker comment creation * Add stickers display to comments and hyperchats * Fix wrong checks for total Super Chats * Stickers/emojis fall out / improvements (#220) * Fix error logs * Improve LBC sticker flow/clarity * Show inline error if custom sticker amount below min * Sort emojis alphabetically * Improve loading of Images * Improve quality and display of emojis and fix CSS * Display both USD and LBC prices * Default to LBC tip if creator can't receive USD * Don't clear text-field after sticker is sent * Refactor notification component * Handle notifications * Don't show profile pic on sticker livestream comments * Change Sticker icon * Fix wording and number rounding * Fix blurring emojis * Disable non functional emote buttons * new Stickers! (#248) * Add new stickers (#347) * Fix cancel sending sticker (#447) * Refactor scrollbar CSS for portal components outside of main Refactor channelMention suggestions into new textareaSuggestions component Install @mui/material packages Move channel mentioning to use @mui/Autocomplete combobox without search functionality Add support for suggesting Emotes while typing ':' Improve label to display matching term Add back and improved support for searching while mentioning Add support for suggesting emojis Fix non concatenated strings Add key to groups and options Fix dispatch props Fix Popper positioning to be consistent Fix and Improve searching Add back support for Winning Uri Filter default emojis with the same name as emotes Remove unused topSuggestion component Fix text color on darkmode Fix livestream updating state from both websocket and reducer and causing double of the same comments to appear Fix blur and focus commentCreate events Fix no name after @ error * desktop tweaks Co-authored-by: saltrafael <76502841+saltrafael@users.noreply.github.com> Co-authored-by: Thomas Zarebczan <tzarebczan@users.noreply.github.com> Co-authored-by: Rafael <rafael.saes@odysee.com>
This commit is contained in:
parent
fe95db15b2
commit
0b41fc041a
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
|
tipAmount: tipAmount, // force show decimal places
|
||||||
tipChannelName,
|
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 (
|
||||||
<div className="comment__create">
|
<Form
|
||||||
<div className="comment__sc-preview">
|
onSubmit={() => {}}
|
||||||
|
className={classnames('commentCreate', {
|
||||||
|
'commentCreate--reply': isReply,
|
||||||
|
'commentCreate--nestedReply': isNested,
|
||||||
|
'commentCreate--bottom': bottom,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Input Box/Preview Box */}
|
||||||
|
{stickerSelector ? (
|
||||||
|
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||||
|
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||||
|
<div className="commentCreate__stickerPreview">
|
||||||
|
<div className="commentCreate__stickerPreviewInfo">
|
||||||
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
|
</div>
|
||||||
|
<div className="commentCreate__stickerPreviewImage">
|
||||||
|
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||||
|
</div>
|
||||||
|
{/* figure out lbc sticker prices */}
|
||||||
|
{selectedSticker.price && exchangeRate && (
|
||||||
|
<FilePrice
|
||||||
|
customPrices={{ priceFiat: selectedSticker.price, priceLBC: selectedSticker.price / exchangeRate }}
|
||||||
|
isFiat
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||||
|
<div className="commentCreate__supportCommentPreview">
|
||||||
<CreditAmount
|
<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,95 +532,38 @@ 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) {
|
||||||
{MinAmountNotice}
|
handleSupportComment();
|
||||||
</div>
|
} else {
|
||||||
</div>
|
handleCreateComment();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
setSelectedSticker(null);
|
||||||
return (
|
setReviewingStickerComment(false);
|
||||||
<Form
|
setStickerSelector(false);
|
||||||
onSubmit={handleSubmit}
|
setIsSupportComment(false);
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
) : isSupportComment ? (
|
||||||
<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
|
<Button
|
||||||
disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
|
disabled={disabled || tipError || disableReviewButton || !minAmountMet}
|
||||||
type="button"
|
type="button"
|
||||||
button="primary"
|
button="primary"
|
||||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} // only LBC
|
||||||
label={__('Review')}
|
label={__('Review')}
|
||||||
onClick={() => setIsReviewingSupportComment(true)}
|
onClick={() => setReviewingSupportComment(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
button="link"
|
|
||||||
label={__('Cancel')}
|
|
||||||
onClick={() => setIsSupportComment(false)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
(!minTip || claimIsMine) && (
|
||||||
{(!minTip || claimIsMine) && (
|
|
||||||
<Button
|
<Button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
button="primary"
|
button="primary"
|
||||||
disabled={disabled}
|
disabled={disabled || stickerSelector}
|
||||||
type="submit"
|
type="submit"
|
||||||
label={
|
label={
|
||||||
isReply
|
isReply
|
||||||
|
@ -579,37 +574,106 @@ export function CommentCreate(props: Props) {
|
||||||
? __('Commenting...')
|
? __('Commenting...')
|
||||||
: __('Comment --[button to submit something]--')
|
: __('Comment --[button to submit something]--')
|
||||||
}
|
}
|
||||||
|
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{!supportDisabled && !claimIsMine && (
|
|
||||||
|
{/** Stickers/Support Buttons **/}
|
||||||
|
{!supportDisabled && !stickerSelector && (
|
||||||
<>
|
<>
|
||||||
<Button
|
{getActionButton(
|
||||||
disabled={disabled}
|
__('Stickers'),
|
||||||
button="alt"
|
isReviewingStickerComment ? __('Different Sticker') : undefined,
|
||||||
className="thatButton"
|
ICONS.STICKER,
|
||||||
icon={ICONS.LBC}
|
() => {
|
||||||
onClick={() => {
|
if (isReviewingStickerComment) setReviewingStickerComment(false);
|
||||||
|
setIsSupportComment(false);
|
||||||
|
setStickerSelector(true);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/* below buttons are unnecessary - REMOVE */}
|
||||||
|
{!claimIsMine && (
|
||||||
|
<>
|
||||||
|
{(!isSupportComment || activeTab !== TAB_LBC) &&
|
||||||
|
getActionButton(
|
||||||
|
__('Credits'),
|
||||||
|
isSupportComment ? __('Switch to Credits') : undefined,
|
||||||
|
ICONS.LBC,
|
||||||
|
() => {
|
||||||
setIsSupportComment(true);
|
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 && (
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancel Button */}
|
||||||
|
{(isSupportComment ||
|
||||||
|
isReviewingSupportComment ||
|
||||||
|
stickerSelector ||
|
||||||
|
isReviewingStickerComment ||
|
||||||
|
(isReply && !minTip)) && (
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isSupportComment && isSubmitting}
|
||||||
button="link"
|
button="link"
|
||||||
label={__('Cancel')}
|
label={__('Cancel')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onCancelReplying) {
|
if (isSupportComment || isReviewingSupportComment) {
|
||||||
|
if (!isReviewingSupportComment) setIsSupportComment(false);
|
||||||
|
setReviewingSupportComment(false);
|
||||||
|
if (stickerPrice) {
|
||||||
|
setReviewingStickerComment(false);
|
||||||
|
setStickerSelector(false);
|
||||||
|
setSelectedSticker(null);
|
||||||
|
}
|
||||||
|
} else if (stickerSelector || isReviewingStickerComment) {
|
||||||
|
setReviewingStickerComment(false);
|
||||||
|
setStickerSelector(false);
|
||||||
|
setSelectedSticker(null);
|
||||||
|
} else if (isReply && !minTip && onCancelReplying) {
|
||||||
onCancelReplying();
|
onCancelReplying();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
{/* Help Text */}
|
||||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||||
{MinAmountNotice}
|
{!!minAmount && (
|
||||||
|
<div className="help--notice commentCreate__minAmountNotice">
|
||||||
|
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||||
|
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||||
|
</I18nMessage>
|
||||||
|
<Icon
|
||||||
|
customTooltipText={
|
||||||
|
minTip
|
||||||
|
? __('This channel requires a minimum tip for each comment.')
|
||||||
|
: minSuper
|
||||||
|
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
className="icon--help"
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
tooltip
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,63 +1,65 @@
|
||||||
// @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;
|
|
||||||
}
|
function getAmountText(amount: number, isFiat?: boolean) {
|
||||||
const fullPrice = formatFullPrice(amount, 2);
|
const fullPrice = formatFullPrice(amount, 2);
|
||||||
const isFree = parseFloat(amount) === 0;
|
const isFree = parseFloat(amount) === 0;
|
||||||
|
|
||||||
let formattedAmount;
|
let formattedAmount;
|
||||||
|
|
||||||
if (showFullPrice) {
|
if (showFullPrice) {
|
||||||
formattedAmount = fullPrice;
|
formattedAmount = fullPrice;
|
||||||
} else {
|
} else {
|
||||||
|
@ -67,11 +69,10 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
: formatCredits(amount, precision, true);
|
: formatCredits(amount, precision, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let amountText;
|
|
||||||
if (showFree && isFree) {
|
if (showFree && isFree) {
|
||||||
amountText = __('Free');
|
return __('Free');
|
||||||
} else {
|
} else {
|
||||||
amountText = noFormat ? amount : formattedAmount;
|
let amountText = noFormat ? amount : formattedAmount;
|
||||||
|
|
||||||
if (showPlus && amount > 0) {
|
if (showPlus && amount > 0) {
|
||||||
amountText = `+${amountText}`;
|
amountText = `+${amountText}`;
|
||||||
|
@ -86,17 +87,26 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
if (fee) {
|
if (fee) {
|
||||||
amountText = __('%amount% fee', { amount: amountText });
|
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,32 +113,15 @@ 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 = (
|
|
||||||
<Wrapper>
|
|
||||||
<input id={name} type="radio" {...inputProps} />
|
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
</Wrapper>
|
</>
|
||||||
);
|
);
|
||||||
} else if (type === 'checkbox') {
|
|
||||||
input = (
|
const inputSelect = (selectClass: string) => (
|
||||||
<div className="checkbox">
|
<fieldset-section class={selectClass}>
|
||||||
<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 || errorMessage) && (
|
||||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||||
)}
|
)}
|
||||||
|
@ -142,21 +130,21 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
</select>
|
</select>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
} else if (type === 'select-tiny') {
|
|
||||||
input = (
|
const input = () => {
|
||||||
<fieldset-section class="select--slim">
|
switch (type) {
|
||||||
{(label || errorMessage) && (
|
case 'radio':
|
||||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
return <Wrapper>{inputSimple('radio')}</Wrapper>;
|
||||||
)}
|
case 'checkbox':
|
||||||
<select id={name} {...inputProps}>
|
return <div className="checkbox">{inputSimple('checkbox')}</div>;
|
||||||
{children}
|
case 'range':
|
||||||
</select>
|
return <div>{inputSimple('range')}</div>;
|
||||||
</fieldset-section>
|
case 'select':
|
||||||
);
|
return inputSelect('');
|
||||||
} else if (type === 'markdown') {
|
case 'select-tiny':
|
||||||
const handleEvents = {
|
return inputSelect('select--slim');
|
||||||
contextmenu: openEditorMenu,
|
case 'markdown':
|
||||||
};
|
const handleEvents = { contextmenu: openEditorMenu };
|
||||||
|
|
||||||
const getInstance = (editor) => {
|
const getInstance = (editor) => {
|
||||||
// SimpleMDE max char check
|
// SimpleMDE max char check
|
||||||
|
@ -164,9 +152,9 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
if (textAreaMaxLength && changes.update) {
|
if (textAreaMaxLength && changes.update) {
|
||||||
var str = changes.text.join('\n');
|
var str = changes.text.join('\n');
|
||||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||||
if (delta <= 0) {
|
|
||||||
return;
|
if (delta <= 0) return;
|
||||||
}
|
|
||||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
str = str.substr(0, str.length - delta);
|
str = str.substr(0, str.length - delta);
|
||||||
|
@ -208,22 +196,11 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
}, 25);
|
}, 25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e) {} // Do nothing (revert to original behavior)
|
||||||
// Do nothing (revert to original behavior)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ideally, the character count should (and can) be appended to the
|
return (
|
||||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
|
||||||
// to pass the current value to it's callback, nor query the current
|
|
||||||
// text length from the callback. So, we'll use our own widget.
|
|
||||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
input = (
|
|
||||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
|
@ -251,21 +228,17 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (type === 'textarea') {
|
case 'textarea':
|
||||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
return (
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
|
||||||
<span className="comment__char-count">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
|
||||||
);
|
|
||||||
input = (
|
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || quickAction) && (
|
{(label || quickAction) && (
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
<div>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
</div>
|
|
||||||
{quickAction}
|
{quickAction}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{hideSuggestions ? (
|
||||||
<textarea
|
<textarea
|
||||||
type={type}
|
type={type}
|
||||||
id={name}
|
id={name}
|
||||||
|
@ -273,30 +246,33 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
ref={this.input}
|
ref={this.input}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
<div className="form-field__textarea-info">
|
) : (
|
||||||
{!noEmojis && (
|
<TextareaWithSuggestions
|
||||||
<div className="form-field__quick-emojis">
|
type={type}
|
||||||
{QUICK_EMOJIS.map((emoji) => (
|
id={name}
|
||||||
<Button
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
key={emoji}
|
inputRef={this.input}
|
||||||
disabled={inputProps.disabled}
|
isLivestream={isLivestream}
|
||||||
type="button"
|
{...inputProps}
|
||||||
className="button--emoji"
|
/>
|
||||||
label={emoji}
|
)}
|
||||||
onClick={() => {
|
|
||||||
inputProps.onChange({
|
<div className="form-field__textarea-info">
|
||||||
target: { value: inputProps.value ? `${inputProps.value} ${emoji}` : emoji },
|
{!noEmojis && openEmoteMenu && (
|
||||||
});
|
<Button
|
||||||
}}
|
type="alt"
|
||||||
|
className="button--file-action"
|
||||||
|
title="Emotes"
|
||||||
|
onClick={openEmoteMenu}
|
||||||
|
icon={ICONS.EMOJI}
|
||||||
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{countInfo}
|
{countInfo}
|
||||||
</div>
|
</div>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
} else {
|
default:
|
||||||
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
||||||
const inner = inputButton ? (
|
const inner = inputButton ? (
|
||||||
<input-submit>
|
<input-submit>
|
||||||
|
@ -307,8 +283,7 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
inputElement
|
inputElement
|
||||||
);
|
);
|
||||||
|
|
||||||
input = (
|
return (
|
||||||
<React.Fragment>
|
|
||||||
<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) {
|
|
||||||
default:
|
|
||||||
notificationTarget = notification_parameters.device.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
const creatorIcon = (channelUrl) => {
|
|
||||||
return (
|
|
||||||
<UriIndicator uri={channelUrl} link>
|
<UriIndicator uri={channelUrl} link>
|
||||||
<ChannelThumbnail small uri={channelUrl} />
|
<ChannelThumbnail small uri={channelUrl} />
|
||||||
</UriIndicator>
|
</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>}
|
|
||||||
|
|
||||||
{isCommentNotification && commentText ? (
|
|
||||||
<>
|
|
||||||
<div className="notification__title">{title}</div>
|
<div className="notification__title">{title}</div>
|
||||||
<div title={commentText} className="notification__text">
|
|
||||||
{commentText}
|
{!commentText ? (
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
<div
|
||||||
title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')}
|
title={notification_parameters.device.text.replace(/\sLBC/g, ' Credits')}
|
||||||
className="notification__text"
|
className="notification__text"
|
||||||
>
|
>
|
||||||
<LbcMessage>{notification_parameters.device.text}</LbcMessage>
|
<LbcMessage>{notification_parameters.device.text}</LbcMessage>
|
||||||
</div>
|
</div>
|
||||||
</>
|
) : stickerFromComment ? (
|
||||||
|
<div className="sticker__comment">
|
||||||
|
<OptimizedImage src={stickerFromComment.url} waitLoad loading="lazy" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div title={commentText} className="notification__text">
|
||||||
|
{commentText}
|
||||||
|
</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,218 +1,161 @@
|
||||||
// @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 claimTypeText = getClaimTypeText();
|
||||||
|
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||||
|
const titleText = claimIsMine
|
||||||
|
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||||
|
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||||
const { claim_id: claimId } = claim;
|
const { claim_id: claimId } = claim;
|
||||||
|
let channelName;
|
||||||
|
try {
|
||||||
|
({ channelName } = parseURI(uri));
|
||||||
|
} catch (e) {}
|
||||||
|
// don't need this - fiat only, for reference REMOVE
|
||||||
|
// const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
|
// const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
|
//
|
||||||
|
// // setup variables for backend tip API
|
||||||
|
// const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||||
|
// const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||||
|
|
||||||
// channel name used in url
|
// icon to use or explainer text to show per tab
|
||||||
const { channelName } = parseURI(uri);
|
let explainerText = '',
|
||||||
|
confirmLabel = '';
|
||||||
// focus tip element if it exists
|
switch (activeTab) {
|
||||||
React.useEffect(() => {
|
case TAB_BOOST:
|
||||||
const tipInputElement = document.getElementById('tip-input');
|
explainerText = __(
|
||||||
if (tipInputElement) {
|
'This refundable boost will improve the discoverability of this %claimTypeText% while active. ',
|
||||||
tipInputElement.focus();
|
{ claimTypeText }
|
||||||
|
);
|
||||||
|
confirmLabel = __('Boosting');
|
||||||
|
break;
|
||||||
|
case TAB_FIAT:
|
||||||
|
explainerText = __('Show this channel your appreciation by sending a donation in USD. ');
|
||||||
|
confirmLabel = __('Tipping Fiat (USD)');
|
||||||
|
break;
|
||||||
|
case TAB_LBC:
|
||||||
|
explainerText = __('Show this channel your appreciation by sending a donation of Credits. ');
|
||||||
|
confirmLabel = __('Tipping Credit');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, []);
|
|
||||||
|
|
||||||
// if user has no balance, used to show conditional frontend
|
/** FUNCTIONS **/
|
||||||
const noBalance = balance === 0;
|
|
||||||
|
|
||||||
// the tip amount, based on if a preset or custom tip amount is being used
|
|
||||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
|
||||||
|
|
||||||
// get type of claim (stream/channel/repost/collection) for display on frontend
|
|
||||||
function getClaimTypeText() {
|
function getClaimTypeText() {
|
||||||
if (claim.value_type === 'stream') {
|
switch (claim.value_type) {
|
||||||
|
case 'stream':
|
||||||
return __('Content');
|
return __('Content');
|
||||||
} else if (claim.value_type === 'channel') {
|
case 'channel':
|
||||||
return __('Channel');
|
return __('Channel');
|
||||||
} else if (claim.value_type === 'repost') {
|
case 'repost':
|
||||||
return __('Repost');
|
return __('Repost');
|
||||||
} else if (claim.value_type === 'collection') {
|
case 'collection':
|
||||||
return __('List');
|
return __('List');
|
||||||
} else {
|
default:
|
||||||
return __('Claim');
|
return __('Claim');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const claimTypeText = getClaimTypeText();
|
|
||||||
|
|
||||||
// icon to use or explainer text to show per tab
|
|
||||||
let iconToUse;
|
|
||||||
let explainerText = '';
|
|
||||||
if (activeTab === TAB_BOOST) {
|
|
||||||
iconToUse = ICONS.LBC;
|
|
||||||
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {
|
|
||||||
claimTypeText,
|
|
||||||
});
|
|
||||||
} else if (activeTab === TAB_LBC) {
|
|
||||||
iconToUse = ICONS.LBC;
|
|
||||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// Regex for number up to 8 decimal places
|
|
||||||
let regexp;
|
|
||||||
let tipError;
|
|
||||||
|
|
||||||
if (tipAmount === 0) {
|
|
||||||
tipError = __('Amount must be a positive number');
|
|
||||||
} else if (!tipAmount || typeof tipAmount !== 'number') {
|
|
||||||
tipError = __('Amount must be a number');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it's not fiat, aka it's boost or lbc tip
|
|
||||||
else {
|
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
|
||||||
const validTipInput = regexp.test(String(tipAmount));
|
|
||||||
|
|
||||||
if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
|
||||||
} else if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
|
||||||
} else if (tipAmount === balance) {
|
|
||||||
tipError = __('Please decrease the amount to account for transaction fees');
|
|
||||||
} else if (tipAmount > balance) {
|
|
||||||
tipError = __('Not enough Credits');
|
|
||||||
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
|
|
||||||
tipError = __('Amount must be higher');
|
|
||||||
}
|
|
||||||
// if tip fiat tab
|
|
||||||
}
|
|
||||||
|
|
||||||
setTipError(tipError);
|
|
||||||
}, [tipAmount, balance, setTipError, activeTab]);
|
|
||||||
|
|
||||||
// make call to the backend to send lbc or fiat
|
// make call to the backend to send lbc or fiat
|
||||||
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
||||||
// send a tip
|
if (!isOnConfirmationPage && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||||
if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
setConfirmationPage(true);
|
||||||
setIsConfirming(true);
|
|
||||||
} else {
|
} else {
|
||||||
// send a boost
|
const supportParams: SupportParams = {
|
||||||
const supportParams: SupportParams = { amount: tipAmount, claim_id: claimId };
|
amount: tipAmount,
|
||||||
|
claim_id: claimId,
|
||||||
// include channel name if donation not anonymous
|
channel_id: activeChannelClaim && !incognito ? activeChannelClaim.claim_id : undefined,
|
||||||
if (activeChannelClaim && !incognito) {
|
};
|
||||||
supportParams.channel_id = activeChannelClaim.claim_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send tip/boost
|
// send tip/boost
|
||||||
sendSupport(supportParams, isSupport);
|
doSendTip(supportParams, isSupport);
|
||||||
closeModal();
|
doHideModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when the form button is clicked
|
// when the form button is clicked
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (tipAmount && claimId) {
|
if (!tipAmount || !claimId) return;
|
||||||
|
|
||||||
// send an instant tip (no need to go to an exchange first)
|
// send an instant tip (no need to go to an exchange first)
|
||||||
if (instantTipEnabled) {
|
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||||
if (instantTipMax.currency === 'LBC') {
|
if (instantTipMax.currency === 'LBC') {
|
||||||
sendSupportOrConfirm(instantTipMax.amount);
|
sendSupportOrConfirm(instantTipMax.amount);
|
||||||
} else {
|
} else {
|
||||||
// Need to convert currency of instant purchase maximum before trying to send support
|
// Need to convert currency of instant purchase maximum before trying to send support
|
||||||
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
|
Lbryio.getExchangeRates().then(({ LBC_USD }) => sendSupportOrConfirm(instantTipMax.amount / LBC_USD));
|
||||||
sendSupportOrConfirm(instantTipMax.amount / LBC_USD);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// sending fiat tip
|
|
||||||
} else {
|
} else {
|
||||||
sendSupportOrConfirm();
|
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
|
||||||
|
@ -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 });
|
||||||
|
case TAB_LBC:
|
||||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dont allow user to click send button
|
/** RENDER **/
|
||||||
function shouldDisableAmountSelector(amount) {
|
|
||||||
return amount > balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,76 +1,119 @@
|
||||||
// @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);
|
// parse number as float and sets it in the parent component
|
||||||
|
function handleCustomPriceChange(amount: number) {
|
||||||
// setup variables for tip API
|
const tipAmountValue = parseFloat(amount);
|
||||||
let channelClaimId, tipChannelName;
|
onChange(tipAmountValue);
|
||||||
// if there is a signing channel it's on a file
|
if (fiatConversion && exchangeRate && setConvertedAmount && convertedAmount !== tipAmountValue * exchangeRate) {
|
||||||
if (claim.signing_channel) {
|
setConvertedAmount(tipAmountValue * exchangeRate);
|
||||||
channelClaimId = claim.signing_channel.claim_id;
|
}
|
||||||
tipChannelName = claim.signing_channel.name;
|
|
||||||
|
|
||||||
// otherwise it's on the channel page
|
|
||||||
} else {
|
|
||||||
channelClaimId = claim.claim_id;
|
|
||||||
tipChannelName = claim.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if creator has a payment method saved
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (stripeEnvironment) {
|
if (setConvertedAmount && exchangeRate && (!convertedAmount || convertedAmount !== amount * exchangeRate)) {
|
||||||
|
setConvertedAmount(amount * exchangeRate);
|
||||||
|
}
|
||||||
|
}, [amount, convertedAmount, exchangeRate, setConvertedAmount]);
|
||||||
|
|
||||||
|
// check if user has a payment method saved
|
||||||
|
// REMOVE
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!stripeEnvironment) return;
|
||||||
|
|
||||||
Lbryio.call(
|
Lbryio.call(
|
||||||
'customer',
|
'customer',
|
||||||
'status',
|
'status',
|
||||||
|
@ -87,12 +130,12 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
|
|
||||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||||
});
|
});
|
||||||
}
|
}, [setHasSavedCard]);
|
||||||
}, [stripeEnvironment]);
|
|
||||||
|
|
||||||
//
|
// check if creator has a tip account saved REMOVE
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (stripeEnvironment) {
|
if (!stripeEnvironment) return;
|
||||||
|
|
||||||
Lbryio.call(
|
Lbryio.call(
|
||||||
'account',
|
'account',
|
||||||
'check',
|
'check',
|
||||||
|
@ -108,74 +151,90 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
setCanReceiveFiatTip(true);
|
setCanReceiveFiatTip(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(() => {});
|
||||||
// console.log(error);
|
}, [canReceiveFiatTip, channelClaimId, tipChannelName]);
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [stripeEnvironment]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// setHasSavedCard(false);
|
let regexp;
|
||||||
// setCanReceiveFiatTip(true);
|
|
||||||
|
|
||||||
let regexp,
|
|
||||||
tipError = '';
|
|
||||||
|
|
||||||
if (amount === 0) {
|
if (amount === 0) {
|
||||||
tipError = __('Amount must be a positive number');
|
setTipError(__('Amount cannot be zero.'));
|
||||||
} else if (!amount || typeof amount !== 'number') {
|
} else if (!amount || typeof amount !== 'number') {
|
||||||
tipError = __('Amount must be a number');
|
setTipError(__('Amount must be a number.'));
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// if it's not fiat, aka it's boost or lbc tip
|
// if it's not fiat, aka it's boost or lbc tip
|
||||||
else if (activeTab !== TAB_FIAT) {
|
if (activeTab !== TAB_FIAT) {
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||||
const validTipInput = regexp.test(String(amount));
|
const validTipInput = regexp.test(String(amount));
|
||||||
|
|
||||||
if (!validTipInput) {
|
if (!validTipInput) {
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
setTipError(__('Amount must have no more than 8 decimal places'));
|
||||||
} else if (amount === balance) {
|
} else if (amount === balance) {
|
||||||
tipError = __('Please decrease the amount to account for transaction fees');
|
setTipError(__('Please decrease the amount to account for transaction fees'));
|
||||||
} else if (amount > balance) {
|
} else if (amount > balance || balance === 0) {
|
||||||
tipError = __('Not enough Credits');
|
setTipError(__('Not enough Credits'));
|
||||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||||
tipError = __('Amount must be higher');
|
setTipError(__('Amount must be higher'));
|
||||||
|
} else if (
|
||||||
|
convertedAmount &&
|
||||||
|
exchangeRate &&
|
||||||
|
customTipAmount &&
|
||||||
|
amount < convertToTwoDecimalsOrMore(customTipAmount / exchangeRate)
|
||||||
|
) {
|
||||||
|
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||||
|
const validCustomTipInput = regexp.test(String(amount));
|
||||||
|
|
||||||
|
if (validCustomTipInput) {
|
||||||
|
setTipError(
|
||||||
|
__('Amount of $%input_amount% LBC in USB is lower than price of $%price_amount%', {
|
||||||
|
input_amount: convertToTwoDecimalsOrMore(convertedAmount, 4),
|
||||||
|
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||||
}
|
}
|
||||||
// if tip fiat tab
|
} else {
|
||||||
|
setTipError(false);
|
||||||
|
}
|
||||||
|
// if tip fiat tab REMOVE
|
||||||
} else {
|
} else {
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||||
const validTipInput = regexp.test(String(amount));
|
const validTipInput = regexp.test(String(amount));
|
||||||
|
|
||||||
if (!validTipInput) {
|
if (!validTipInput) {
|
||||||
tipError = __('Amount must have no more than 2 decimal places');
|
setTipError(__('Amount must have no more than 2 decimal places'));
|
||||||
} else if (amount < 1) {
|
} else if (amount < 1) {
|
||||||
tipError = __('Amount must be at least one dollar');
|
setTipError(__('Amount must be at least one dollar'));
|
||||||
} else if (amount > 1000) {
|
} else if (amount > 1000) {
|
||||||
tipError = __('Amount cannot be over 1000 dollars');
|
setTipError(__('Amount cannot be over 1000 dollars'));
|
||||||
|
} else if (customTipAmount && amount < customTipAmount) {
|
||||||
|
setTipError(
|
||||||
|
__('Amount is lower than price of $%price_amount%', {
|
||||||
|
price_amount: convertToTwoDecimalsOrMore(customTipAmount),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTipError(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTipError(tipError);
|
|
||||||
onTipErrorChange(tipError);
|
|
||||||
}, [amount, balance, setTipError, activeTab]);
|
|
||||||
|
|
||||||
// parse number as float and sets it in the parent component
|
|
||||||
function handleCustomPriceChange(amount: number) {
|
|
||||||
const tipAmount = parseFloat(amount);
|
|
||||||
|
|
||||||
onChange(tipAmount);
|
|
||||||
}
|
}
|
||||||
|
}, [activeTab, amount, balance, convertedAmount, customTipAmount, exchangeRate, setTipError]);
|
||||||
|
|
||||||
|
const getHelpMessage = (helpMessage: any) => <div className="help">{helpMessage}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
{tipAmountsToDisplay &&
|
||||||
|
tipAmountsToDisplay.map((defaultAmount) => (
|
||||||
<Button
|
<Button
|
||||||
key={defaultAmount}
|
key={defaultAmount}
|
||||||
disabled={shouldDisableAmountSelector(defaultAmount)}
|
disabled={shouldDisableAmountSelector(defaultAmount)}
|
||||||
button="alt"
|
button="alt"
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
'button-toggle--active': defaultAmount === amount && !useCustomTip,
|
'button-toggle--active':
|
||||||
|
convertToTwoDecimalsOrMore(defaultAmount) === convertToTwoDecimalsOrMore(amount) && !useCustomTip,
|
||||||
'button-toggle--disabled': amount > balance,
|
'button-toggle--disabled': amount > balance,
|
||||||
})}
|
})}
|
||||||
label={defaultAmount}
|
label={defaultAmount}
|
||||||
|
@ -186,9 +245,10 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
disabled={shouldDisableFiatSelectors}
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
'button-toggle--active': useCustomTip,
|
'button-toggle--active': useCustomTip,
|
||||||
})}
|
})}
|
||||||
|
@ -207,60 +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">
|
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||||
<span className="help--spendable">
|
{' ' + __('To Tip Creators')}
|
||||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
|
||||||
{__('Tip Creators')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
: !canReceiveFiatTip
|
||||||
{/* has card saved but cant creator cant receive tips */}
|
? getHelpMessage(__('Only creators that verify cash accounts can receive tips'))
|
||||||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
: getHelpMessage(__('Send a tip directly from your attached card')))}
|
||||||
<>
|
|
||||||
<div className="help">
|
|
||||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* has card saved but cant creator cant receive tips */}
|
|
||||||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
|
||||||
<>
|
|
||||||
<div className="help">
|
|
||||||
<span className="help--spendable">Send a tip directly from your attached card</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,16 +53,16 @@ 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;
|
||||||
|
@ -146,7 +139,7 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
|
||||||
}
|
}
|
||||||
return recommendedContent;
|
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