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
This commit is contained in:
parent
0d3ecc87f9
commit
9e01335694
33 changed files with 1346 additions and 845 deletions
3
flow-typed/search.js
vendored
3
flow-typed/search.js
vendored
|
@ -29,8 +29,10 @@ declare type SearchOptions = {
|
|||
declare type SearchState = {
|
||||
options: SearchOptions,
|
||||
resultsByQuery: {},
|
||||
results: Array<string>,
|
||||
hasReachedMaxResultsLength: {},
|
||||
searching: boolean,
|
||||
mentionQuery: string,
|
||||
};
|
||||
|
||||
declare type SearchSuccess = {
|
||||
|
@ -41,6 +43,7 @@ declare type SearchSuccess = {
|
|||
size: number,
|
||||
uris: Array<string>,
|
||||
recsys: string,
|
||||
query: string,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
"postinstall:warning": "echo '\n\nWARNING\n\nNot all node modules were installed because NODE_ENV is set to \"production\".\nThis should only be set after installing dependencies with \"yarn\". The app will not work.\n\n'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.6.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@mui/material": "^5.2.1",
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@ungap/from-entries": "^0.2.1",
|
||||
"auto-launch": "^5.0.5",
|
||||
|
|
|
@ -68,8 +68,8 @@ type Props = {
|
|||
syncLoop: (?boolean) => void,
|
||||
currentModal: any,
|
||||
syncFatalError: boolean,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
myChannelUrls: ?Array<string>,
|
||||
activeChannelId: ?string,
|
||||
myChannelClaimIds: ?Array<string>,
|
||||
subscriptions: Array<Subscription>,
|
||||
setActiveChannelIfNotSet: () => void,
|
||||
setIncognito: (boolean) => void,
|
||||
|
@ -103,8 +103,8 @@ function App(props: Props) {
|
|||
syncLoop,
|
||||
currentModal,
|
||||
syncFatalError,
|
||||
myChannelUrls,
|
||||
activeChannelClaim,
|
||||
myChannelClaimIds,
|
||||
activeChannelId,
|
||||
setActiveChannelIfNotSet,
|
||||
setIncognito,
|
||||
fetchModBlockedList,
|
||||
|
@ -125,6 +125,7 @@ function App(props: Props) {
|
|||
const { pathname, search } = props.location;
|
||||
const [upgradeNagClosed, setUpgradeNagClosed] = useState(false);
|
||||
const [resolvedSubscriptions, setResolvedSubscriptions] = useState(false);
|
||||
// const [retryingSync, setRetryingSync] = useState(false);
|
||||
const [sidebarOpen] = usePersistedState('sidebar', true);
|
||||
const showUpgradeButton =
|
||||
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
|
||||
|
@ -135,10 +136,10 @@ function App(props: Props) {
|
|||
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
|
||||
const userId = user && user.id;
|
||||
const useCustomScrollbar = !IS_MAC;
|
||||
const hasMyChannels = myChannelUrls && myChannelUrls.length > 0;
|
||||
const hasNoChannels = myChannelUrls && myChannelUrls.length === 0;
|
||||
const hasMyChannels = myChannelClaimIds && myChannelClaimIds.length > 0;
|
||||
const hasNoChannels = myChannelClaimIds && myChannelClaimIds.length === 0;
|
||||
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
||||
const hasActiveChannelClaim = activeChannelClaim !== undefined;
|
||||
const hasActiveChannelClaim = activeChannelId !== undefined;
|
||||
const isPersonalized = hasVerifiedEmail;
|
||||
const renderFiledrop = isAuthenticated;
|
||||
|
||||
|
@ -152,7 +153,7 @@ function App(props: Props) {
|
|||
if (!uploadCount) return;
|
||||
const handleBeforeUnload = (event) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = 'magic'; // without setting this to something it doesn't work
|
||||
event.returnValue = __('There are pending uploads.'); // without setting this to something it doesn't work
|
||||
};
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
|
@ -272,7 +273,6 @@ function App(props: Props) {
|
|||
}
|
||||
}, [previousRewardApproved, isRewardApproved]);
|
||||
|
||||
// @if TARGET='app'
|
||||
useEffect(() => {
|
||||
if (updatePreferences && getWalletSyncPref && readyForPrefs) {
|
||||
getWalletSyncPref()
|
||||
|
@ -282,7 +282,6 @@ function App(props: Props) {
|
|||
});
|
||||
}
|
||||
}, [updatePreferences, getWalletSyncPref, setReadyForSync, readyForPrefs, hasVerifiedEmail]);
|
||||
// @endif
|
||||
|
||||
// ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too.
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
|
||||
import ChannelMentionSuggestion from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: selectClaimForUri(state, props.uri),
|
||||
isResolvingUri: selectIsUriResolving(state, props.uri),
|
||||
});
|
||||
|
||||
export default connect(select)(ChannelMentionSuggestion);
|
|
@ -1,32 +0,0 @@
|
|||
// @flow
|
||||
import { ComboboxOption } from '@reach/combobox';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
claim: ?Claim,
|
||||
uri?: string,
|
||||
isResolvingUri: boolean,
|
||||
};
|
||||
|
||||
export default function ChannelMentionSuggestion(props: Props) {
|
||||
const { claim, uri, isResolvingUri } = props;
|
||||
|
||||
return !claim ? null : (
|
||||
<ComboboxOption value={uri}>
|
||||
{isResolvingUri ? (
|
||||
<div className="channel-mention__suggestion">
|
||||
<div className="media__thumb media__thumb--resolving" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="channel-mention__suggestion">
|
||||
<ChannelThumbnail xsmall uri={uri} />
|
||||
<span className="channel-mention__suggestion-label">
|
||||
<div className="channel-mention__suggestion-title">{(claim.value && claim.value.title) || claim.name}</div>
|
||||
<div className="channel-mention__suggestion-name">{claim.name}</div>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</ComboboxOption>
|
||||
);
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { withRouter } from 'react-router';
|
||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import { selectTopLevelCommentsForUri } from 'redux/selectors/comments';
|
||||
import ChannelMentionSuggestions from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const subscriptionUris = selectSubscriptions(state).map(({ uri }) => uri);
|
||||
const topLevelComments = selectTopLevelCommentsForUri(state, props.uri);
|
||||
|
||||
const commentorUris = [];
|
||||
// Avoid repeated commentors
|
||||
topLevelComments.map(({ channel_url }) => !commentorUris.includes(channel_url) && commentorUris.push(channel_url));
|
||||
|
||||
const getUnresolved = (uris) =>
|
||||
uris.map((uri) => !makeSelectClaimForUri(uri)(state) && uri).filter((uri) => uri !== false);
|
||||
const getCanonical = (uris) =>
|
||||
uris
|
||||
.map((uri) => makeSelectClaimForUri(uri)(state) && makeSelectClaimForUri(uri)(state).canonical_url)
|
||||
.filter((uri) => Boolean(uri));
|
||||
|
||||
return {
|
||||
commentorUris,
|
||||
subscriptionUris,
|
||||
unresolvedCommentors: getUnresolved(commentorUris),
|
||||
unresolvedSubscriptions: getUnresolved(subscriptionUris),
|
||||
canonicalCreator: getCanonical([props.creatorUri])[0],
|
||||
canonicalCommentors: getCanonical(commentorUris),
|
||||
canonicalSubscriptions: getCanonical(subscriptionUris),
|
||||
showMature: selectShowMatureContent(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(connect(select, { doResolveUris })(ChannelMentionSuggestions));
|
|
@ -1,277 +0,0 @@
|
|||
// @flow
|
||||
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox';
|
||||
import { Form } from 'component/common/form';
|
||||
import { parseURI, regexInvalidURI } from 'util/lbryURI';
|
||||
import { SEARCH_OPTIONS } from 'constants/search';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import ChannelMentionSuggestion from 'component/channelMentionSuggestion';
|
||||
import ChannelMentionTopSuggestion from 'component/channelMentionTopSuggestion';
|
||||
import React from 'react';
|
||||
import Spinner from 'component/spinner';
|
||||
import type { ElementRef } from 'react';
|
||||
import useLighthouse from 'effects/use-lighthouse';
|
||||
|
||||
const INPUT_DEBOUNCE_MS = 1000;
|
||||
const LIGHTHOUSE_MIN_CHARACTERS = 3;
|
||||
|
||||
type Props = {
|
||||
inputRef: any,
|
||||
mentionTerm: string,
|
||||
noTopSuggestion?: boolean,
|
||||
showMature: boolean,
|
||||
creatorUri: string,
|
||||
commentorUris: Array<string>,
|
||||
subscriptionUris: Array<string>,
|
||||
unresolvedSubscriptions: Array<string>,
|
||||
canonicalCreator: string,
|
||||
canonicalCommentors: Array<string>,
|
||||
canonicalSubscriptions: Array<string>,
|
||||
doResolveUris: (Array<string>) => void,
|
||||
customSelectAction?: (string, number) => void,
|
||||
};
|
||||
|
||||
export default function ChannelMentionSuggestions(props: Props) {
|
||||
const {
|
||||
unresolvedSubscriptions,
|
||||
canonicalCreator,
|
||||
creatorUri,
|
||||
inputRef,
|
||||
showMature,
|
||||
noTopSuggestion,
|
||||
mentionTerm,
|
||||
doResolveUris,
|
||||
customSelectAction,
|
||||
} = props;
|
||||
const comboboxInputRef: ElementRef<any> = React.useRef();
|
||||
const comboboxListRef: ElementRef<any> = React.useRef();
|
||||
|
||||
const mainEl = document.querySelector('.channel-mention__suggestions');
|
||||
|
||||
const [debouncedTerm, setDebouncedTerm] = React.useState('');
|
||||
const [mostSupported, setMostSupported] = React.useState('');
|
||||
const [canonicalResults, setCanonicalResults] = React.useState([]);
|
||||
|
||||
const isRefFocused = (ref) => ref && ref.current === document.activeElement;
|
||||
|
||||
const subscriptionUris = props.subscriptionUris.filter((uri) => uri !== creatorUri);
|
||||
const canonicalSubscriptions = props.canonicalSubscriptions.filter((uri) => uri !== canonicalCreator);
|
||||
const commentorUris = props.commentorUris.filter((uri) => uri !== creatorUri && !subscriptionUris.includes(uri));
|
||||
const canonicalCommentors = props.canonicalCommentors.filter(
|
||||
(uri) => uri !== canonicalCreator && !canonicalSubscriptions.includes(uri)
|
||||
);
|
||||
|
||||
const termToMatch = mentionTerm && mentionTerm.replace('@', '').toLowerCase();
|
||||
const allShownUris = [creatorUri, ...subscriptionUris, ...commentorUris];
|
||||
const allShownCanonical = [canonicalCreator, ...canonicalSubscriptions, ...canonicalCommentors];
|
||||
const possibleMatches = allShownUris.filter((uri) => {
|
||||
try {
|
||||
// yuck a try catch in a filter?
|
||||
const { channelName } = parseURI(uri);
|
||||
return channelName && channelName.toLowerCase().includes(termToMatch);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
const searchSize = 5;
|
||||
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
|
||||
const { results, loading } = useLighthouse(debouncedTerm, showMature, searchSize, additionalOptions, 0);
|
||||
const stringifiedResults = JSON.stringify(results);
|
||||
|
||||
const hasMinLength = mentionTerm && mentionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
|
||||
const isTyping = debouncedTerm !== mentionTerm;
|
||||
const showPlaceholder = isTyping || loading;
|
||||
|
||||
const isUriFromTermValid = !regexInvalidURI.test(mentionTerm.substring(1));
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(value, key) => {
|
||||
if (customSelectAction) {
|
||||
// Give them full results, as our resolved one might truncate the claimId.
|
||||
customSelectAction(value || (results && results.find((r) => r.startsWith(value))) || '', Number(key));
|
||||
}
|
||||
},
|
||||
[customSelectAction, results]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (isTyping) setDebouncedTerm(!hasMinLength ? '' : mentionTerm);
|
||||
}, INPUT_DEBOUNCE_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [hasMinLength, isTyping, mentionTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!mainEl) return;
|
||||
const header = document.querySelector('.header__navigation');
|
||||
|
||||
function handleReflow() {
|
||||
const boxAtTopOfPage = header && mainEl.getBoundingClientRect().top <= header.offsetHeight;
|
||||
const boxAtBottomOfPage = mainEl.getBoundingClientRect().bottom >= window.innerHeight;
|
||||
|
||||
if (boxAtTopOfPage) {
|
||||
mainEl.setAttribute('flow-bottom', '');
|
||||
}
|
||||
if (mainEl.getAttribute('flow-bottom') !== null && boxAtBottomOfPage) {
|
||||
mainEl.removeAttribute('flow-bottom');
|
||||
}
|
||||
}
|
||||
handleReflow();
|
||||
|
||||
window.addEventListener('scroll', handleReflow);
|
||||
return () => window.removeEventListener('scroll', handleReflow);
|
||||
}, [mainEl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!inputRef || !comboboxInputRef || !mentionTerm) return;
|
||||
|
||||
function handleKeyDown(event) {
|
||||
const { keyCode } = event;
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (keyCode === KEYCODES.UP || keyCode === KEYCODES.DOWN) {
|
||||
if (isRefFocused(comboboxInputRef)) {
|
||||
const selectedId = activeElement && activeElement.getAttribute('aria-activedescendant');
|
||||
const selectedItem = selectedId && document.querySelector(`li[id="${selectedId}"]`);
|
||||
if (selectedItem) selectedItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
} else {
|
||||
// $FlowFixMe
|
||||
comboboxInputRef.current.focus();
|
||||
}
|
||||
} else {
|
||||
if ((isRefFocused(comboboxInputRef) || isRefFocused(inputRef)) && keyCode === KEYCODES.TAB) {
|
||||
event.preventDefault();
|
||||
const activeValue = activeElement && activeElement.getAttribute('value');
|
||||
|
||||
if (activeValue) {
|
||||
handleSelect(activeValue, keyCode);
|
||||
} else if (possibleMatches.length) {
|
||||
// $FlowFixMe
|
||||
const suggest = allShownCanonical.find((matchUri) => possibleMatches.find((uri) => uri.includes(matchUri)));
|
||||
if (suggest) handleSelect(suggest, keyCode);
|
||||
} else if (results) {
|
||||
handleSelect(mentionTerm, keyCode);
|
||||
}
|
||||
}
|
||||
if (isRefFocused(comboboxInputRef)) {
|
||||
// $FlowFixMe
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [allShownCanonical, handleSelect, inputRef, mentionTerm, possibleMatches, results]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!stringifiedResults) return;
|
||||
|
||||
const arrayResults = JSON.parse(stringifiedResults);
|
||||
if (arrayResults && arrayResults.length > 0) {
|
||||
// $FlowFixMe
|
||||
doResolveUris(arrayResults).then((response) => {
|
||||
try {
|
||||
// $FlowFixMe
|
||||
const canonical_urls = Object.values(response).map(({ canonical_url }) => canonical_url);
|
||||
setCanonicalResults(canonical_urls);
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
}, [doResolveUris, stringifiedResults]);
|
||||
|
||||
// Only resolve the subscriptions that match the mention term, instead of all
|
||||
React.useEffect(() => {
|
||||
if (isTyping) return;
|
||||
|
||||
const urisToResolve = [];
|
||||
subscriptionUris.map(
|
||||
(uri) =>
|
||||
hasMinLength &&
|
||||
possibleMatches.includes(uri) &&
|
||||
unresolvedSubscriptions.includes(uri) &&
|
||||
urisToResolve.push(uri)
|
||||
);
|
||||
|
||||
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
|
||||
}, [doResolveUris, hasMinLength, isTyping, possibleMatches, subscriptionUris, unresolvedSubscriptions]);
|
||||
|
||||
const suggestionsRow = (
|
||||
label: string,
|
||||
suggestions: Array<string>,
|
||||
canonical: Array<string>,
|
||||
hasSuggestionsBelow: boolean
|
||||
) => {
|
||||
if (mentionTerm.length > 1 && suggestions !== results) {
|
||||
suggestions = suggestions.filter((uri) => possibleMatches.includes(uri));
|
||||
} else if (suggestions === results) {
|
||||
suggestions = suggestions.filter((uri) => !allShownUris.includes(uri));
|
||||
}
|
||||
// $FlowFixMe
|
||||
suggestions = suggestions
|
||||
.map((matchUri) => canonical.find((uri) => matchUri.includes(uri)))
|
||||
.filter((uri) => Boolean(uri));
|
||||
|
||||
if (canonical === canonicalResults) {
|
||||
suggestions = suggestions.filter((uri) => uri !== mostSupported);
|
||||
}
|
||||
|
||||
return !suggestions.length ? null : (
|
||||
<>
|
||||
<div className="channel-mention__label">{label}</div>
|
||||
{suggestions.map((uri) => (
|
||||
<ChannelMentionSuggestion key={uri} uri={uri} />
|
||||
))}
|
||||
{hasSuggestionsBelow && <hr className="channel-mention__top-separator" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return isRefFocused(inputRef) || isRefFocused(comboboxInputRef) ? (
|
||||
<Form onSubmit={() => handleSelect(mentionTerm)}>
|
||||
<Combobox className="channel-mention" onSelect={handleSelect}>
|
||||
<ComboboxInput ref={comboboxInputRef} className="channel-mention__input--none" value={mentionTerm} />
|
||||
{mentionTerm && isUriFromTermValid && (
|
||||
<ComboboxPopover portal={false} className="channel-mention__suggestions">
|
||||
<ComboboxList ref={comboboxListRef}>
|
||||
{creatorUri &&
|
||||
suggestionsRow(
|
||||
__('Creator'),
|
||||
[creatorUri],
|
||||
[canonicalCreator],
|
||||
canonicalSubscriptions.length > 0 || commentorUris.length > 0 || !showPlaceholder
|
||||
)}
|
||||
{canonicalSubscriptions.length > 0 &&
|
||||
suggestionsRow(
|
||||
__('Following'),
|
||||
subscriptionUris,
|
||||
canonicalSubscriptions,
|
||||
commentorUris.length > 0 || !showPlaceholder
|
||||
)}
|
||||
{commentorUris.length > 0 &&
|
||||
suggestionsRow(__('From comments'), commentorUris, canonicalCommentors, !showPlaceholder)}
|
||||
|
||||
{hasMinLength &&
|
||||
(showPlaceholder ? (
|
||||
<Spinner type="small" />
|
||||
) : (
|
||||
results && (
|
||||
<>
|
||||
{!noTopSuggestion && (
|
||||
<ChannelMentionTopSuggestion
|
||||
query={debouncedTerm}
|
||||
shownUris={allShownCanonical}
|
||||
setMostSupported={(winningUri) => setMostSupported(winningUri)}
|
||||
/>
|
||||
)}
|
||||
{suggestionsRow(__('From search'), results, canonicalResults, false)}
|
||||
</>
|
||||
)
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
</Combobox>
|
||||
</Form>
|
||||
) : null;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectIsUriResolving } from 'redux/selectors/claims';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
||||
import ChannelMentionTopSuggestion from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const uriFromQuery = `lbry://${props.query}`;
|
||||
return {
|
||||
uriFromQuery,
|
||||
isResolvingUri: selectIsUriResolving(state, uriFromQuery),
|
||||
winningUri: makeSelectWinningUriForQuery(props.query)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, { doResolveUri })(ChannelMentionTopSuggestion);
|
|
@ -1,49 +0,0 @@
|
|||
// @flow
|
||||
import ChannelMentionSuggestion from 'component/channelMentionSuggestion';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
uriFromQuery: string,
|
||||
winningUri: string,
|
||||
isResolvingUri: boolean,
|
||||
shownUris: Array<string>,
|
||||
setMostSupported: (string) => void,
|
||||
doResolveUri: (string) => void,
|
||||
};
|
||||
|
||||
export default function ChannelMentionTopSuggestion(props: Props) {
|
||||
const { uriFromQuery, winningUri, isResolvingUri, shownUris, setMostSupported, doResolveUri } = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uriFromQuery) doResolveUri(uriFromQuery);
|
||||
}, [doResolveUri, uriFromQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (winningUri) setMostSupported(winningUri);
|
||||
}, [setMostSupported, winningUri]);
|
||||
|
||||
if (isResolvingUri) {
|
||||
return (
|
||||
<div className="channel-mention__winning-claim">
|
||||
<div className="channel-mention__label channel-mention__placeholder-label" />
|
||||
|
||||
<div className="channel-mention__suggestion channel-mention__placeholder-suggestion">
|
||||
<div className="channel-mention__placeholder-thumbnail" />
|
||||
<div className="channel-mention__placeholder-info" />
|
||||
</div>
|
||||
<hr className="channel-mention__top-separator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return !winningUri || shownUris.includes(winningUri) ? null : (
|
||||
<>
|
||||
<div className="channel-mention__label">
|
||||
<LbcSymbol prefix={__('Most Supported')} />
|
||||
</div>
|
||||
<ChannelMentionSuggestion uri={winningUri} />
|
||||
<hr className="channel-mention__top-separator" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -30,10 +30,10 @@ const select = (state, props) => {
|
|||
const perform = (dispatch, ownProps) => ({
|
||||
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment, sticker) =>
|
||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment, sticker)),
|
||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
fetchComment: (commentId) => dispatch(doCommentById(commentId, false)),
|
||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentCreate);
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
// @flow
|
||||
|
||||
import 'scss/component/_comment-create.scss';
|
||||
|
||||
import { buildValidSticker } from 'util/comments';
|
||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { useHistory } from 'react-router';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import Button from 'component/button';
|
||||
import ChannelMentionSuggestions from 'component/channelMentionSuggestions';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import classnames from 'classnames';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
|
@ -26,14 +28,12 @@ import type { ElementRef } from 'react';
|
|||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
const MENTION_DEBOUNCE_MS = 100;
|
||||
|
||||
// for sendCashTip REMOVE
|
||||
// type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
|
||||
|
@ -43,7 +43,7 @@ type Props = {
|
|||
activeChannel: string,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
bottom: boolean,
|
||||
hasChannels: boolean, //
|
||||
hasChannels: boolean,
|
||||
claim: StreamClaim,
|
||||
claimIsMine: boolean,
|
||||
isFetchingChannels: boolean,
|
||||
|
@ -79,7 +79,6 @@ export function CommentCreate(props: Props) {
|
|||
settingsByChannelId,
|
||||
shouldFetchComment,
|
||||
supportDisabled,
|
||||
uri,
|
||||
createComment,
|
||||
doFetchCreatorSettings,
|
||||
doToast,
|
||||
|
@ -91,8 +90,6 @@ export function CommentCreate(props: Props) {
|
|||
} = props;
|
||||
|
||||
const formFieldRef: ElementRef<any> = React.useRef();
|
||||
const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||
const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart;
|
||||
const buttonRef: ElementRef<any> = React.useRef();
|
||||
|
||||
const {
|
||||
|
@ -115,33 +112,14 @@ export function CommentCreate(props: Props) {
|
|||
const [activeTab, setActiveTab] = React.useState();
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
||||
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||
const [disableReviewButton, setDisableReviewButton] = React.useState();
|
||||
const [exchangeRate, setExchangeRate] = React.useState();
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined);
|
||||
|
||||
const selectedMentionIndex =
|
||||
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
||||
? commentValue.indexOf('@', selectionIndex)
|
||||
: commentValue.lastIndexOf('@', selectionIndex);
|
||||
const modifierIndex = commentValue.indexOf(':', selectedMentionIndex);
|
||||
const spaceIndex = commentValue.indexOf(' ', selectedMentionIndex);
|
||||
const mentionLengthIndex =
|
||||
modifierIndex >= 0 && (spaceIndex === -1 || modifierIndex < spaceIndex)
|
||||
? modifierIndex
|
||||
: spaceIndex >= 0 && (modifierIndex === -1 || spaceIndex < modifierIndex)
|
||||
? spaceIndex
|
||||
: commentValue.length;
|
||||
const channelMention =
|
||||
selectedMentionIndex >= 0 && selectionIndex <= mentionLengthIndex
|
||||
? commentValue.substring(selectedMentionIndex, mentionLengthIndex)
|
||||
: '';
|
||||
|
||||
const claimId = claim && claim.claim_id;
|
||||
const channelUri = claim && (claim.signing_channel ? claim.signing_channel.permanent_url : claim.permanent_url);
|
||||
const charCount = commentValue ? commentValue.length : 0;
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend;
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length;
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
|
||||
|
@ -181,19 +159,6 @@ export function CommentCreate(props: Props) {
|
|||
setCommentValue(commentValue);
|
||||
}
|
||||
|
||||
function handleSelectMention(mentionValue, key) {
|
||||
let newMentionValue = mentionValue.replace('lbry://', '');
|
||||
if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':');
|
||||
|
||||
setCommentValue(
|
||||
commentValue.substring(0, selectedMentionIndex) +
|
||||
`${newMentionValue}` +
|
||||
(commentValue.length > mentionLengthIndex + 1
|
||||
? commentValue.substring(mentionLengthIndex, commentValue.length)
|
||||
: ' ')
|
||||
);
|
||||
}
|
||||
|
||||
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||
e.preventDefault();
|
||||
|
@ -247,7 +212,9 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
|
||||
function doSubmitTip() {
|
||||
if (!activeChannelClaim) return;
|
||||
if (!activeChannelClaim || isSubmitting) return;
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
const params = { amount: tipAmount, claim_id: claimId, channel_id: activeChannelClaim.claim_id };
|
||||
// FIAT ONLY - REMOVE
|
||||
|
@ -258,8 +225,6 @@ export function CommentCreate(props: Props) {
|
|||
// const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id;
|
||||
const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name;
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
if (activeTab === TAB_LBC) {
|
||||
// call sendTip and then run the callback from the response
|
||||
// second parameter is callback
|
||||
|
@ -273,13 +238,10 @@ export function CommentCreate(props: Props) {
|
|||
}, 1500);
|
||||
|
||||
doToast({
|
||||
message: __(
|
||||
"You sent %tipAmount% LBRY Credits as a tip to %tipChannelName%, I'm sure they appreciate it!",
|
||||
{
|
||||
tipAmount: tipAmount, // force show decimal places
|
||||
tipChannelName,
|
||||
}
|
||||
),
|
||||
message: __("You sent %tipAmount% Credits as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||
tipAmount: tipAmount, // force show decimal places
|
||||
tipChannelName,
|
||||
}),
|
||||
});
|
||||
|
||||
setSuccessTip({ txid, tipAmount });
|
||||
|
@ -314,8 +276,11 @@ export function CommentCreate(props: Props) {
|
|||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||
*/
|
||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
setShowEmotes(false);
|
||||
setSubmitting(true);
|
||||
|
||||
const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name);
|
||||
|
||||
createComment(stickerValue || commentValue, claimId, parentId, txid, payment_intent_id, environment, !!stickerValue)
|
||||
|
@ -366,18 +331,6 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
}, [fetchComment, shouldFetchComment, parentId]);
|
||||
|
||||
// Debounce for disabling the submit button when mentioning a user with Enter
|
||||
// so that the comment isn't sent at the same time
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (pauseQuickSend) {
|
||||
setPauseQuickSend(false);
|
||||
}
|
||||
}, MENTION_DEBOUNCE_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [pauseQuickSend]);
|
||||
|
||||
// Stickers: Get LBC-USD exchange rate if hasn't yet and selected a paid sticker
|
||||
React.useEffect(() => {
|
||||
if (stickerPrice && !exchangeRate) Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD));
|
||||
|
@ -411,6 +364,30 @@ export function CommentCreate(props: Props) {
|
|||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||
|
||||
// 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
|
||||
// **************************************************************************
|
||||
|
@ -492,37 +469,29 @@ export function CommentCreate(props: Props) {
|
|||
closeSelector={() => setShowEmotes(false)}
|
||||
/>
|
||||
)}
|
||||
{!advancedEditor && (
|
||||
<ChannelMentionSuggestions
|
||||
uri={uri}
|
||||
inputRef={formFieldInputRef}
|
||||
mentionTerm={channelMention}
|
||||
creatorUri={channelUri}
|
||||
customSelectAction={handleSelectMention}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
disabled={isFetchingChannels}
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
ref={formFieldRef}
|
||||
autoFocus={isReply}
|
||||
charCount={charCount}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
disabled={isFetchingChannels}
|
||||
label={
|
||||
<span className="commentCreate__labelWrapper">
|
||||
<div className="commentCreate__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||
<div className="commentCreate__labelWrapper">
|
||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||
<SelectChannel tiny />
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
ref={formFieldRef}
|
||||
onChange={handleCommentChange}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
value={commentValue}
|
||||
charCount={charCount}
|
||||
onChange={handleCommentChange}
|
||||
autoFocus={isReply}
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
</>
|
||||
|
@ -587,7 +556,6 @@ export function CommentCreate(props: Props) {
|
|||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} // only LBC
|
||||
label={__('Review')}
|
||||
onClick={() => setReviewingSupportComment(true)}
|
||||
requiresAuth
|
||||
/>
|
||||
) : (
|
||||
(!minTip || claimIsMine) && (
|
||||
|
@ -605,7 +573,6 @@ export function CommentCreate(props: Props) {
|
|||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
requiresAuth
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
/>
|
||||
)
|
||||
|
@ -628,10 +595,15 @@ export function CommentCreate(props: Props) {
|
|||
{!claimIsMine && (
|
||||
<>
|
||||
{(!isSupportComment || activeTab !== TAB_LBC) &&
|
||||
getActionButton(__('LBC'), isSupportComment ? __('Switch to LBC') : undefined, ICONS.LBC, () => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_LBC);
|
||||
})}
|
||||
getActionButton(
|
||||
__('Credits'),
|
||||
isSupportComment ? __('Switch to Credits') : undefined,
|
||||
ICONS.LBC,
|
||||
() => {
|
||||
setIsSupportComment(true);
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
)}
|
||||
|
||||
{stripeEnvironment &&
|
||||
(!isSupportComment || activeTab !== TAB_FIAT) &&
|
||||
|
|
|
@ -8,38 +8,42 @@ import MarkdownPreview from 'component/common/markdown-preview';
|
|||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||
import type { ElementRef, Node } from 'react';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
label?: string | Node,
|
||||
prefix?: string,
|
||||
postfix?: string,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
type?: string,
|
||||
defaultValue?: string | number,
|
||||
placeholder?: string | number,
|
||||
children?: React$Node,
|
||||
stretch?: boolean,
|
||||
affixClass?: string, // class applied to prefix/postfix label
|
||||
autoFocus?: boolean,
|
||||
labelOnLeft: boolean,
|
||||
inputButton?: React$Node,
|
||||
blockWrap: boolean,
|
||||
charCount?: number,
|
||||
textAreaMaxLength?: number,
|
||||
range?: number,
|
||||
min?: number,
|
||||
max?: number,
|
||||
quickActionLabel?: string,
|
||||
children?: React$Node,
|
||||
defaultValue?: string | number,
|
||||
disabled?: boolean,
|
||||
value?: string | number,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
hideSuggestions?: boolean,
|
||||
inputButton?: React$Node,
|
||||
isLivestream?: boolean,
|
||||
label?: string | Node,
|
||||
labelOnLeft: boolean,
|
||||
max?: number,
|
||||
min?: number,
|
||||
name: string,
|
||||
noEmojis?: boolean,
|
||||
render?: () => React$Node,
|
||||
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,
|
||||
quickActionHandler?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
quickActionHandler?: (any) => any,
|
||||
render?: () => React$Node,
|
||||
};
|
||||
|
||||
export class FormField extends React.PureComponent<Props> {
|
||||
|
@ -61,27 +65,29 @@ export class FormField extends React.PureComponent<Props> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
prefix,
|
||||
postfix,
|
||||
error,
|
||||
helper,
|
||||
name,
|
||||
type,
|
||||
children,
|
||||
stretch,
|
||||
affixClass,
|
||||
autoFocus,
|
||||
inputButton,
|
||||
labelOnLeft,
|
||||
blockWrap,
|
||||
charCount,
|
||||
textAreaMaxLength,
|
||||
quickActionLabel,
|
||||
children,
|
||||
error,
|
||||
helper,
|
||||
hideSuggestions,
|
||||
inputButton,
|
||||
isLivestream,
|
||||
label,
|
||||
labelOnLeft,
|
||||
name,
|
||||
noEmojis,
|
||||
render,
|
||||
quickActionHandler,
|
||||
postfix,
|
||||
prefix,
|
||||
quickActionLabel,
|
||||
stretch,
|
||||
textAreaMaxLength,
|
||||
type,
|
||||
openEmoteMenu,
|
||||
quickActionHandler,
|
||||
render,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -231,13 +237,26 @@ export class FormField extends React.PureComponent<Props> {
|
|||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
) : (
|
||||
<TextareaWithSuggestions
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
inputRef={this.input}
|
||||
isLivestream={isLivestream}
|
||||
{...inputProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && openEmoteMenu && (
|
||||
<Button
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||
import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||
import { selectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||
import RecommendedContent from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||
const { claim_id: claimId } = claim;
|
||||
const recommendedContentUris = makeSelectRecommendedContentForUri(props.uri)(state);
|
||||
const recommendedContentUris = selectRecommendedContentForUri(state, props.uri);
|
||||
const nextRecommendedUri = recommendedContentUris && recommendedContentUris[0];
|
||||
|
||||
return {
|
||||
|
|
9
ui/component/textareaSuggestionsItem/index.js
Normal file
9
ui/component/textareaSuggestionsItem/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||
import TextareaSuggestionsItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: props.uri && selectClaimForUri(state, props.uri),
|
||||
});
|
||||
|
||||
export default connect(select)(TextareaSuggestionsItem);
|
46
ui/component/textareaSuggestionsItem/view.jsx
Normal file
46
ui/component/textareaSuggestionsItem/view.jsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
// @flow
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
claim?: Claim,
|
||||
emote?: any,
|
||||
uri?: string,
|
||||
};
|
||||
|
||||
export default function TextareaSuggestionsItem(props: Props) {
|
||||
const { claim, emote, uri, ...autocompleteProps } = props;
|
||||
|
||||
if (emote) {
|
||||
const { name: value, url, unicode } = emote;
|
||||
|
||||
return (
|
||||
<div {...autocompleteProps} dispatch={undefined}>
|
||||
{unicode ? <div className="emote">{unicode}</div> : <img className="emote" src={url} />}
|
||||
|
||||
<div className="textareaSuggestion__label">
|
||||
<span className="textareaSuggestion__title textareaSuggestion__value textareaSuggestion__value--emote">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (claim) {
|
||||
const value = claim.canonical_url.replace('lbry://', '').replace('#', ':');
|
||||
|
||||
return (
|
||||
<div {...autocompleteProps} dispatch={undefined}>
|
||||
<ChannelThumbnail xsmall uri={uri} />
|
||||
|
||||
<div className="textareaSuggestion__label">
|
||||
<span className="textareaSuggestion__title">{(claim.value && claim.value.title) || value}</span>
|
||||
<span className="textareaSuggestion__value">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
46
ui/component/textareaWithSuggestions/index.js
Normal file
46
ui/component/textareaWithSuggestions/index.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import { doSetMentionSearchResults } from 'redux/actions/search';
|
||||
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
||||
// import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
|
||||
import { selectChannelMentionData } from 'redux/selectors/comments';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { withRouter } from 'react-router';
|
||||
import TextareaWithSuggestions from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { pathname } = props.location;
|
||||
const uri = `lbry:/${pathname.replaceAll(':', '#')}`;
|
||||
|
||||
// const maxComments = props.isLivestream ? MAX_LIVESTREAM_COMMENTS : -1;
|
||||
const maxComments = -1;
|
||||
const data = selectChannelMentionData(state, uri, maxComments);
|
||||
const {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions,
|
||||
commentorUris,
|
||||
hasNewResolvedResults,
|
||||
query,
|
||||
} = data;
|
||||
|
||||
return {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions,
|
||||
canonicalTop: makeSelectWinningUriForQuery(query)(state),
|
||||
commentorUris,
|
||||
hasNewResolvedResults,
|
||||
searchQuery: query,
|
||||
showMature: selectShowMatureContent(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
||||
doSetMentionSearchResults: (query, uris) => dispatch(doSetMentionSearchResults(query, uris)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(TextareaWithSuggestions));
|
417
ui/component/textareaWithSuggestions/view.jsx
Normal file
417
ui/component/textareaWithSuggestions/view.jsx
Normal file
|
@ -0,0 +1,417 @@
|
|||
// @flow
|
||||
import { EMOTES_48px as EMOTES } from 'constants/emotes';
|
||||
import { matchSorter } from 'match-sorter';
|
||||
import { SEARCH_OPTIONS } from 'constants/search';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import EMOJIS from 'emoji-dictionary';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import React from 'react';
|
||||
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import useLighthouse from 'effects/use-lighthouse';
|
||||
import useThrottle from 'effects/use-throttle';
|
||||
|
||||
const SUGGESTION_REGEX = new RegExp(
|
||||
'(?<Mention>(?:^| |\n)@[^\\s=&#$@%?:;/\\"<>%{}|^~[]*(?::[\\w]+)?)|(?<Emote>(?:^| |\n):[\\w+-]*:?)',
|
||||
'gm'
|
||||
);
|
||||
|
||||
/** Regex Explained step-by-step:
|
||||
*
|
||||
* 1) (?<Name>....) = naming a match into a possible group (either Mention or Emote)
|
||||
* 2) (?:^| |\n) = only allow for: sentence beginning, space or newline before the match (no words or symbols)
|
||||
* 3) [^\s=&#$@%?:;/\\"<>%{}|^~[]* = anything, except the characters inside
|
||||
* 4) (?::[\w]+)? = A mention can be matched with a ':' as a claim modifier with words or digits after as ID digits,
|
||||
* or else it's everything before the ':' (will then match the winning uri for the mention behind since has no canonical ID)
|
||||
* 5) :\w*:? = the emote Regex, possible to be matched with a ':' at the end to consider previously typed emotes
|
||||
*
|
||||
*/
|
||||
|
||||
const SEARCH_SIZE = 10;
|
||||
const LIGHTHOUSE_MIN_CHARACTERS = 3;
|
||||
const INPUT_DEBOUNCE_MS = 1000;
|
||||
|
||||
const EMOJI_MIN_CHARACTERS = 2;
|
||||
|
||||
type Props = {
|
||||
canonicalCommentors?: Array<string>,
|
||||
canonicalCreatorUri?: string,
|
||||
canonicalSearch?: Array<string>,
|
||||
canonicalSubscriptions?: Array<string>,
|
||||
canonicalTop?: string,
|
||||
className?: string,
|
||||
commentorUris?: Array<string>,
|
||||
disabled?: boolean,
|
||||
hasNewResolvedResults?: boolean,
|
||||
id: string,
|
||||
inputRef: any,
|
||||
isLivestream?: boolean,
|
||||
maxLength?: number,
|
||||
placeholder?: string,
|
||||
searchQuery?: string,
|
||||
showMature: boolean,
|
||||
type?: string,
|
||||
uri?: string,
|
||||
value: any,
|
||||
doResolveUris: (Array<string>) => void,
|
||||
doSetMentionSearchResults: (string, Array<string>) => void,
|
||||
onBlur: (any) => any,
|
||||
onChange: (any) => any,
|
||||
onFocus: (any) => any,
|
||||
};
|
||||
|
||||
export default function TextareaWithSuggestions(props: Props) {
|
||||
const {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions: canonicalSubs,
|
||||
canonicalTop,
|
||||
className,
|
||||
commentorUris,
|
||||
disabled,
|
||||
hasNewResolvedResults,
|
||||
id,
|
||||
inputRef,
|
||||
isLivestream,
|
||||
maxLength,
|
||||
placeholder,
|
||||
searchQuery,
|
||||
showMature,
|
||||
type,
|
||||
value: messageValue,
|
||||
doResolveUris,
|
||||
doSetMentionSearchResults,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
} = props;
|
||||
|
||||
const inputDefaultProps = { className, placeholder, maxLength, type, disabled };
|
||||
|
||||
const [suggestionValue, setSuggestionValue] = React.useState(undefined);
|
||||
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
|
||||
const [shouldClose, setClose] = React.useState();
|
||||
const [debouncedTerm, setDebouncedTerm] = React.useState('');
|
||||
// const [mostSupported, setMostSupported] = React.useState('');
|
||||
|
||||
const suggestionTerm = suggestionValue && suggestionValue.term;
|
||||
const isEmote = suggestionValue && suggestionValue.isEmote;
|
||||
const isMention = suggestionValue && !suggestionValue.isEmote;
|
||||
const invalidTerm = suggestionTerm && isMention && suggestionTerm.charAt(1) === ':';
|
||||
|
||||
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
|
||||
const { results, loading } = useLighthouse(debouncedTerm, showMature, SEARCH_SIZE, additionalOptions, 0);
|
||||
const stringifiedResults = JSON.stringify(results);
|
||||
|
||||
const hasMinLength = suggestionTerm && isMention && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
|
||||
const isTyping = isMention && debouncedTerm !== suggestionTerm;
|
||||
const showPlaceholder =
|
||||
isMention && !invalidTerm && (isTyping || loading || (results && results.length > 0 && !hasNewResolvedResults));
|
||||
|
||||
const shouldFilter = (uri, previous) => uri !== canonicalCreatorUri && (!previous || !previous.includes(uri));
|
||||
const filteredCommentors = canonicalCommentors && canonicalCommentors.filter((uri) => shouldFilter(uri));
|
||||
const filteredSubs = canonicalSubs && canonicalSubs.filter((uri) => shouldFilter(uri, filteredCommentors));
|
||||
const filteredTop =
|
||||
canonicalTop &&
|
||||
shouldFilter(canonicalTop, filteredSubs) &&
|
||||
shouldFilter(canonicalTop, filteredCommentors) &&
|
||||
canonicalTop;
|
||||
const filteredSearch =
|
||||
canonicalSearch &&
|
||||
canonicalSearch.filter(
|
||||
(uri) => shouldFilter(uri, filteredSubs) && shouldFilter(uri, filteredCommentors) && uri !== filteredTop
|
||||
);
|
||||
|
||||
let emoteNames;
|
||||
let emojiNames;
|
||||
const allOptions = [];
|
||||
if (isEmote) {
|
||||
emoteNames = EMOTES.map(({ name }) => name.toLowerCase());
|
||||
const hasMinEmojiLength = suggestionTerm && suggestionTerm.length > EMOJI_MIN_CHARACTERS;
|
||||
// Filter because our emotes are priority from default emojis, like :eggplant:
|
||||
emojiNames = hasMinEmojiLength ? EMOJIS.names.filter((name) => !emoteNames.includes(`:${name}:`)) : [];
|
||||
const emotesAndEmojis = [...emoteNames, ...emojiNames];
|
||||
|
||||
allOptions.push(...emotesAndEmojis);
|
||||
} else {
|
||||
if (canonicalCreatorUri) allOptions.push(canonicalCreatorUri);
|
||||
if (filteredSubs) allOptions.push(...filteredSubs);
|
||||
if (filteredCommentors) allOptions.push(...filteredCommentors);
|
||||
if (filteredTop) allOptions.push(filteredTop);
|
||||
if (filteredSearch) allOptions.push(...filteredSearch);
|
||||
}
|
||||
|
||||
const allOptionsGrouped =
|
||||
allOptions.length > 0
|
||||
? allOptions.map((option) => {
|
||||
const groupName = isEmote
|
||||
? (emoteNames.includes(option) && __('Emotes')) || (emojiNames.includes(option) && __('Emojis'))
|
||||
: (canonicalCreatorUri === option && __('Creator')) ||
|
||||
(filteredSubs && filteredSubs.includes(option) && __('Following')) ||
|
||||
(filteredCommentors && filteredCommentors.includes(option) && __('From Comments')) ||
|
||||
(filteredTop && filteredTop === option && 'Top') ||
|
||||
(filteredSearch && filteredSearch.includes(option) && __('From Search'));
|
||||
|
||||
let emoteLabel;
|
||||
if (isEmote) {
|
||||
// $FlowFixMe
|
||||
emoteLabel = `:${option.replaceAll(':', '')}:`;
|
||||
}
|
||||
|
||||
return {
|
||||
label: emoteLabel || option.replace('lbry://', '').replace('#', ':'),
|
||||
group: groupName,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const allMatches =
|
||||
useSuggestionMatch(
|
||||
suggestionTerm || '',
|
||||
allOptionsGrouped.map(({ label }) => label)
|
||||
) || [];
|
||||
|
||||
/** --------- **/
|
||||
/** Functions **/
|
||||
/** --------- **/
|
||||
|
||||
function handleInputChange(value: string) {
|
||||
onChange({ target: { value } });
|
||||
|
||||
const cursorIndex = inputRef && inputRef.current && inputRef.current.selectionStart;
|
||||
|
||||
const suggestionMatches = value.match(SUGGESTION_REGEX);
|
||||
|
||||
if (!suggestionMatches) {
|
||||
if (suggestionValue) setSuggestionValue(null);
|
||||
return; // Exit here and avoid unnecessary behavior
|
||||
}
|
||||
|
||||
const exec = SUGGESTION_REGEX.exec(value);
|
||||
const groups = exec && exec.groups;
|
||||
const groupValue = groups && Object.keys(groups).find((group) => groups[group]);
|
||||
|
||||
const previousLastIndexes = [];
|
||||
let isEmote = groupValue && groupValue === 'Emote';
|
||||
let currentSuggestionIndex = exec && exec.index;
|
||||
let currentLastIndex = exec && SUGGESTION_REGEX.lastIndex;
|
||||
let currentSuggestionValue =
|
||||
cursorIndex >= currentSuggestionIndex &&
|
||||
cursorIndex <= currentLastIndex &&
|
||||
suggestionMatches &&
|
||||
suggestionMatches[0];
|
||||
|
||||
if (suggestionMatches && suggestionMatches.length > 1) {
|
||||
currentSuggestionValue = suggestionMatches.find((match, index) => {
|
||||
const previousLastIndex = previousLastIndexes[index - 1] || 0;
|
||||
const valueWithoutPrevious = value.substring(previousLastIndex);
|
||||
|
||||
const tempRe = new RegExp(SUGGESTION_REGEX);
|
||||
const tempExec = tempRe.exec(valueWithoutPrevious);
|
||||
const groups = tempExec && tempExec.groups;
|
||||
const groupValue = groups && Object.keys(groups).find((group) => groups[group]);
|
||||
|
||||
if (tempExec) {
|
||||
isEmote = groupValue && groupValue === 'Emote';
|
||||
currentSuggestionIndex = previousLastIndex + tempExec.index;
|
||||
currentLastIndex = previousLastIndex + tempRe.lastIndex;
|
||||
previousLastIndexes.push(currentLastIndex);
|
||||
}
|
||||
|
||||
// the current mention term will be the one on the text cursor's range,
|
||||
// in case of there being more in the same comment message
|
||||
if (previousLastIndexes) {
|
||||
return cursorIndex >= currentSuggestionIndex && cursorIndex <= currentLastIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (currentSuggestionValue) {
|
||||
const token = isEmote ? ':' : '@';
|
||||
const tokenIndex = currentSuggestionValue.indexOf(token);
|
||||
|
||||
// $FlowFixMe
|
||||
setSuggestionValue({
|
||||
beforeTerm: currentSuggestionValue.substring(0, tokenIndex), // in case of a space or newline
|
||||
term: currentSuggestionValue.substring(tokenIndex),
|
||||
index: currentSuggestionIndex,
|
||||
lastIndex: currentLastIndex,
|
||||
isEmote,
|
||||
});
|
||||
} else if (suggestionValue) {
|
||||
setSuggestionValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(selectedValue: string) => {
|
||||
if (!suggestionValue) return;
|
||||
|
||||
const elem = inputRef && inputRef.current;
|
||||
const newCursorPos = suggestionValue.beforeTerm.length + suggestionValue.index + selectedValue.length + 1;
|
||||
|
||||
const contentBegin = messageValue.substring(0, suggestionValue.index);
|
||||
const replaceValue = suggestionValue.beforeTerm + selectedValue;
|
||||
const contentEnd =
|
||||
messageValue.length > suggestionValue.lastIndex
|
||||
? messageValue.substring(suggestionValue.lastIndex, messageValue.length)
|
||||
: ' ';
|
||||
|
||||
const newValue = contentBegin + replaceValue + contentEnd;
|
||||
|
||||
onChange({ target: { value: newValue } });
|
||||
setSuggestionValue(null);
|
||||
elem.focus();
|
||||
elem.setSelectionRange(newCursorPos, newCursorPos);
|
||||
},
|
||||
[messageValue, inputRef, onChange, suggestionValue]
|
||||
);
|
||||
|
||||
/** ------- **/
|
||||
/** Effects **/
|
||||
/** ------- **/
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isMention) return;
|
||||
|
||||
if (isTyping && suggestionTerm && !invalidTerm) {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedTerm(!hasMinLength ? '' : suggestionTerm);
|
||||
}, INPUT_DEBOUNCE_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [hasMinLength, invalidTerm, isMention, isTyping, suggestionTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!stringifiedResults) return;
|
||||
|
||||
const arrayResults = JSON.parse(stringifiedResults);
|
||||
if (debouncedTerm && arrayResults && arrayResults.length > 0) {
|
||||
doResolveUris([debouncedTerm, ...arrayResults]);
|
||||
doSetMentionSearchResults(debouncedTerm, arrayResults);
|
||||
}
|
||||
}, [debouncedTerm, doResolveUris, doSetMentionSearchResults, stringifiedResults, suggestionTerm]);
|
||||
|
||||
// Disable sending on Enter on Livestream chat
|
||||
React.useEffect(() => {
|
||||
if (!isLivestream) return;
|
||||
|
||||
if (suggestionTerm && inputRef) {
|
||||
inputRef.current.setAttribute('term', suggestionTerm);
|
||||
} else {
|
||||
inputRef.current.removeAttribute('term');
|
||||
}
|
||||
}, [inputRef, isLivestream, suggestionTerm]);
|
||||
|
||||
// Only resolve commentors on Livestreams when first trying to mention/looking for it
|
||||
React.useEffect(() => {
|
||||
if (isLivestream && commentorUris && suggestionTerm) doResolveUris(commentorUris);
|
||||
}, [commentorUris, doResolveUris, isLivestream, suggestionTerm]);
|
||||
|
||||
// Allow selecting with TAB key
|
||||
React.useEffect(() => {
|
||||
if (!suggestionTerm) return; // only if there is a term, or else can't tab to navigate page
|
||||
|
||||
function handleKeyDown(e: SyntheticKeyboardEvent<*>) {
|
||||
const { keyCode } = e;
|
||||
|
||||
if (highlightedSuggestion && keyCode === KEYCODES.TAB) {
|
||||
e.preventDefault();
|
||||
handleSelect(highlightedSuggestion.label);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleSelect, highlightedSuggestion, suggestionTerm]);
|
||||
|
||||
/** ------ **/
|
||||
/** Render **/
|
||||
/** ------ **/
|
||||
|
||||
const renderGroup = (groupName: string, children: any) => (
|
||||
<div key={groupName} className="textareaSuggestions__group">
|
||||
<label className="textareaSuggestions__label">
|
||||
{groupName === 'Top' ? (
|
||||
<LbcSymbol prefix={__('Winning Search for %matching_term%', { matching_term: searchQuery })} />
|
||||
) : suggestionTerm && suggestionTerm.length > 1 ? (
|
||||
__('%group_name% matching %matching_term%', { group_name: groupName, matching_term: suggestionTerm })
|
||||
) : (
|
||||
groupName
|
||||
)}
|
||||
</label>
|
||||
{children}
|
||||
<hr className="textareaSuggestions__topSeparator" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderInput = (params: any) => {
|
||||
const { InputProps, disabled, fullWidth, id, inputProps: autocompleteInputProps } = params;
|
||||
const inputProps = { ...autocompleteInputProps, ...inputDefaultProps };
|
||||
const autocompleteProps = { InputProps, disabled, fullWidth, id, inputProps };
|
||||
|
||||
return <TextField inputRef={inputRef} multiline select={false} {...autocompleteProps} />;
|
||||
};
|
||||
|
||||
const renderOption = (optionProps: any, label: string) => {
|
||||
const emoteFound = isEmote && EMOTES.find(({ name }) => name.toLowerCase() === label);
|
||||
const emoteValue = emoteFound ? { name: label, url: emoteFound.url } : undefined;
|
||||
const emojiFound = isEmote && EMOJIS.getUnicode(label);
|
||||
const emojiValue = emojiFound ? { name: label, unicode: emojiFound } : undefined;
|
||||
|
||||
return <TextareaSuggestionsItem key={label} uri={label} emote={emoteValue || emojiValue} {...optionProps} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
PopperComponent={AutocompletePopper}
|
||||
autoHighlight
|
||||
disableClearable
|
||||
filterOptions={(options) => options.filter(({ label }) => allMatches.includes(label))}
|
||||
freeSolo
|
||||
fullWidth
|
||||
getOptionLabel={(option) => option.label || ''}
|
||||
groupBy={(option) => option.group}
|
||||
id={id}
|
||||
inputValue={messageValue}
|
||||
loading={allMatches.length === 0 || showPlaceholder}
|
||||
loadingText={showPlaceholder ? <BusyIndicator message={__('Searching...')} /> : __('Nothing found')}
|
||||
onBlur={() => onBlur && onBlur()}
|
||||
/* Different from onInputChange, onChange is only used for the selected value,
|
||||
so here it is acting simply as a selection handler (see it as onSelect) */
|
||||
onChange={(event, value) => handleSelect(value.label)}
|
||||
onClose={(event, reason) => reason !== 'selectOption' && setClose(true)}
|
||||
onFocus={() => onFocus && onFocus()}
|
||||
onHighlightChange={(event, option) => setHighlightedSuggestion(option)}
|
||||
onInputChange={(event, value, reason) => reason === 'input' && handleInputChange(value)}
|
||||
onOpen={() => suggestionTerm && setClose(false)}
|
||||
/* 'open' is for the popper box component, set to check for a valid term
|
||||
or else it will be displayed all the time as empty (no options) */
|
||||
open={!!suggestionTerm && !shouldClose}
|
||||
options={allOptionsGrouped}
|
||||
renderGroup={({ group, children }) => renderGroup(group, children)}
|
||||
renderInput={(params) => renderInput(params)}
|
||||
renderOption={(optionProps, option) => renderOption(optionProps, option.label)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AutocompletePopper(props: any) {
|
||||
return <Popper {...props} placement="top" />;
|
||||
}
|
||||
|
||||
function useSuggestionMatch(term: string, list: Array<string>) {
|
||||
const throttledTerm = useThrottle(term);
|
||||
|
||||
return React.useMemo(() => {
|
||||
return !throttledTerm || throttledTerm.trim() === ''
|
||||
? undefined
|
||||
: matchSorter(list, term, { keys: [(item) => item] });
|
||||
}, [list, term, throttledTerm]);
|
||||
}
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { selectVolume, selectMute } from 'redux/selectors/app';
|
||||
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||
import { makeSelectContentPositionForUri, makeSelectIsPlayerFloating, selectPlayingUri } from 'redux/selectors/content';
|
||||
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
||||
import { selectRecommendedContentForUri } from 'redux/selectors/search';
|
||||
import VideoViewer from './view';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
|
@ -41,7 +41,7 @@ const select = (state, props) => {
|
|||
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||
previousListUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||
} else {
|
||||
const recommendedContent = makeSelectRecommendedContentForUri(uri)(state);
|
||||
const recommendedContent = selectRecommendedContentForUri(state, uri);
|
||||
nextRecommendedUri = recommendedContent && recommendedContent[0];
|
||||
}
|
||||
|
||||
|
|
|
@ -232,6 +232,7 @@ export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
|
|||
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
||||
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
|
||||
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
||||
export const SET_MENTION_SEARCH_RESULTS = 'SET_MENTION_SEARCH_RESULTS';
|
||||
|
||||
// Settings
|
||||
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
const useEffectOnce = effect => {
|
||||
const useEffectOnce = (effect) => {
|
||||
React.useEffect(effect, []);
|
||||
};
|
||||
|
||||
|
@ -14,7 +14,7 @@ function useUnmount(fn: () => any): void {
|
|||
useEffectOnce(() => () => fnRef.current());
|
||||
}
|
||||
|
||||
export function useThrottle(value: string, ms: number = 200) {
|
||||
export default function useThrottle(value: string, ms: number = 200) {
|
||||
const [state, setState] = React.useState(value);
|
||||
const timeout = React.useRef();
|
||||
const nextValue = React.useRef(null);
|
||||
|
@ -37,7 +37,7 @@ export function useThrottle(value: string, ms: number = 200) {
|
|||
nextValue.current = value;
|
||||
hasNextValue.current = true;
|
||||
}
|
||||
}, [value]);
|
||||
}, [ms, value]);
|
||||
|
||||
useUnmount(() => {
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
|
@ -45,5 +45,3 @@ export function useThrottle(value: string, ms: number = 200) {
|
|||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default useThrottle;
|
||||
|
|
|
@ -87,7 +87,6 @@ function DiscoverPage(props: Props) {
|
|||
icon={ICONS.SUBSCRIBE}
|
||||
iconColor="red"
|
||||
onClick={handleFollowClick}
|
||||
requiresAuth={false}
|
||||
label={label}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -129,6 +129,13 @@ export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptio
|
|||
}
|
||||
};
|
||||
|
||||
export const doSetMentionSearchResults = (query: string, uris: Array<string>) => (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_MENTION_SEARCH_RESULTS,
|
||||
data: { query, uris },
|
||||
});
|
||||
};
|
||||
|
||||
export const doFetchRecommendedContent = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const claim = selectClaimForUri(state, uri);
|
||||
|
|
|
@ -20,6 +20,8 @@ const defaultState: SearchState = {
|
|||
resultsByQuery: {},
|
||||
hasReachedMaxResultsLength: {},
|
||||
searching: false,
|
||||
results: [],
|
||||
mentionQuery: '',
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
|
@ -66,6 +68,12 @@ export default handleActions(
|
|||
options,
|
||||
};
|
||||
},
|
||||
|
||||
[ACTIONS.SET_MENTION_SEARCH_RESULTS]: (state: SearchState, action: SearchSuccess): SearchState => ({
|
||||
...state,
|
||||
results: action.data.uris,
|
||||
mentionQuery: action.data.query,
|
||||
}),
|
||||
},
|
||||
defaultState
|
||||
);
|
||||
|
|
|
@ -3,11 +3,19 @@ import { createSelector } from 'reselect';
|
|||
import { createCachedSelector } from 're-reselect';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectMentionSearchResults, selectMentionQuery } from 'redux/selectors/search';
|
||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||
import { selectClaimsById, selectMyActiveClaims, selectClaimIdForUri } from 'redux/selectors/claims';
|
||||
import { isClaimNsfw } from 'util/claim';
|
||||
import {
|
||||
selectClaimsById,
|
||||
selectMyClaimIdsRaw,
|
||||
selectMyChannelClaimIds,
|
||||
selectClaimIdForUri,
|
||||
selectClaimIdsByUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { isClaimNsfw, getChannelFromClaim } from 'util/claim';
|
||||
import { selectSubscriptionUris } from 'redux/selectors/subscriptions';
|
||||
|
||||
type State = { comments: CommentsState, claims: any };
|
||||
type State = { claims: any, comments: CommentsState };
|
||||
|
||||
const selectState = (state) => state.comments || {};
|
||||
|
||||
|
@ -51,12 +59,11 @@ export const selectCommentsByUri = createSelector(selectState, (state) => {
|
|||
|
||||
export const selectPinnedCommentsById = (state: State) => selectState(state).pinnedCommentsById;
|
||||
export const selectPinnedCommentsForUri = createCachedSelector(
|
||||
selectCommentsByUri,
|
||||
selectClaimIdForUri,
|
||||
selectCommentsById,
|
||||
selectPinnedCommentsById,
|
||||
(state, uri) => uri,
|
||||
(byUri, byId, pinnedCommentsById, uri) => {
|
||||
const claimId = byUri[uri];
|
||||
(claimId, byId, pinnedCommentsById, uri) => {
|
||||
const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId];
|
||||
const pinnedComments = [];
|
||||
|
||||
|
@ -68,7 +75,7 @@ export const selectPinnedCommentsForUri = createCachedSelector(
|
|||
|
||||
return pinnedComments;
|
||||
}
|
||||
)((state, uri) => uri);
|
||||
)((state, uri) => String(uri));
|
||||
|
||||
export const selectModerationBlockList = createSelector(
|
||||
(state) => selectState(state).moderationBlockList,
|
||||
|
@ -128,7 +135,7 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment
|
|||
return comments;
|
||||
});
|
||||
|
||||
// no superchats?
|
||||
// no superchats
|
||||
export const selectSuperchatsByUri = (state: State) => selectState(state).superChatsByUri;
|
||||
|
||||
export const selectTopLevelCommentsByClaimId = createSelector(
|
||||
|
@ -180,6 +187,7 @@ export const selectCommentIdsForUri = (state: State, uri: string) => {
|
|||
return commentIdsByClaimId[claimId];
|
||||
};
|
||||
|
||||
// deprecated
|
||||
export const makeSelectCommentIdsForUri = (uri: string) =>
|
||||
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
|
||||
const claimId = byUri[uri];
|
||||
|
@ -188,7 +196,8 @@ export const makeSelectCommentIdsForUri = (uri: string) =>
|
|||
|
||||
const filterCommentsDepOnList = {
|
||||
claimsById: selectClaimsById,
|
||||
myClaims: selectMyActiveClaims,
|
||||
myClaimIds: selectMyClaimIdsRaw,
|
||||
myChannelClaimIds: selectMyChannelClaimIds,
|
||||
mutedChannels: selectMutedChannels,
|
||||
personalBlockList: selectModerationBlockList,
|
||||
blacklistedMap: selectBlacklistedOutpointMap,
|
||||
|
@ -206,28 +215,29 @@ export const selectFetchingBlockedWords = (state: State) => selectState(state).f
|
|||
export const selectCommentsForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectCommentsByClaimId,
|
||||
selectCommentsByUri,
|
||||
selectClaimIdForUri,
|
||||
...Object.values(filterCommentsDepOnList),
|
||||
(uri, byClaimId, byUri, ...filterInputs) => {
|
||||
const claimId = byUri[uri];
|
||||
(uri, byClaimId, claimId, ...filterInputs) => {
|
||||
const comments = byClaimId && byClaimId[claimId];
|
||||
return filterComments(comments, claimId, filterInputs);
|
||||
}
|
||||
)((state, uri) => uri);
|
||||
)((state, uri) => String(uri));
|
||||
|
||||
export const selectTopLevelCommentsForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
(state, uri, maxCount) => maxCount,
|
||||
selectTopLevelCommentsByClaimId,
|
||||
selectCommentsByUri,
|
||||
selectClaimIdForUri,
|
||||
...Object.values(filterCommentsDepOnList),
|
||||
(uri, maxCount = -1, byClaimId, byUri, ...filterInputs) => {
|
||||
const claimId = byUri[uri];
|
||||
(uri, maxCount = -1, byClaimId, claimId, ...filterInputs) => {
|
||||
const comments = byClaimId && byClaimId[claimId];
|
||||
const filtered = filterComments(comments, claimId, filterInputs);
|
||||
return maxCount > 0 ? filtered.slice(0, maxCount) : filtered;
|
||||
if (comments) {
|
||||
return filterComments(maxCount > 0 ? comments.slice(0, maxCount) : comments, claimId, filterInputs);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
)((state, uri, maxCount = -1) => `${uri}:${maxCount}`);
|
||||
)((state, uri, maxCount = -1) => `${String(uri)}:${maxCount}`);
|
||||
|
||||
export const makeSelectTopLevelTotalCommentsForUri = (uri: string) =>
|
||||
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||
|
@ -259,24 +269,25 @@ export const selectRepliesForParentId = createCachedSelector(
|
|||
|
||||
return filterComments(comments, undefined, filterInputs);
|
||||
}
|
||||
)((state, id: string) => id);
|
||||
)((state, id: string) => String(id));
|
||||
|
||||
/**
|
||||
* filterComments
|
||||
*
|
||||
* @param comments List of comments to filter.
|
||||
* @param claimId The claim that `comments` reside in.
|
||||
* @oaram filterInputs Values returned by filterCommentsDepOnList.
|
||||
* @param filterInputs Values returned by filterCommentsDepOnList.
|
||||
*/
|
||||
const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs: any) => {
|
||||
const filterProps = filterInputs.reduce(function (acc, cur, i) {
|
||||
const filterProps = filterInputs.reduce((acc, cur, i) => {
|
||||
acc[filterCommentsPropKeys[i]] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const {
|
||||
claimsById,
|
||||
myClaims,
|
||||
myClaimIds,
|
||||
myChannelClaimIds,
|
||||
mutedChannels,
|
||||
personalBlockList,
|
||||
blacklistedMap,
|
||||
|
@ -295,8 +306,12 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
|
|||
|
||||
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
||||
if (channelClaim) {
|
||||
if (myClaims && myClaims.size > 0) {
|
||||
const claimIsMine = channelClaim.is_my_output || myClaims.has(channelClaim.claim_id);
|
||||
if ((myClaimIds && myClaimIds.size > 0) || (myChannelClaimIds && myChannelClaimIds.length > 0)) {
|
||||
const claimIsMine =
|
||||
channelClaim.is_my_output ||
|
||||
myChannelClaimIds.includes(channelClaim.claim_id) ||
|
||||
myClaimIds.includes(channelClaim.claim_id);
|
||||
// TODO: I believe 'myClaimIds' does not include channels, so it seems wasteful to include it here? ^
|
||||
if (claimIsMine) {
|
||||
return true;
|
||||
}
|
||||
|
@ -316,7 +331,7 @@ const filterComments = (comments: Array<Comment>, claimId?: string, filterInputs
|
|||
}
|
||||
|
||||
if (claimId) {
|
||||
const claimIdIsMine = myClaims && myClaims.size > 0 && myClaims.has(claimId);
|
||||
const claimIdIsMine = myClaimIds && myClaimIds.size > 0 && myClaimIds.includes(claimId);
|
||||
if (!claimIdIsMine) {
|
||||
if (personalBlockList.includes(comment.channel_url)) {
|
||||
return false;
|
||||
|
@ -376,25 +391,88 @@ export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) =>
|
|||
return blockingByUri[uri] || unBlockingByUri[uri];
|
||||
});
|
||||
|
||||
export const makeSelectSuperChatDataForUri = (uri: string) =>
|
||||
createSelector(selectSuperchatsByUri, (byUri) => {
|
||||
return byUri[uri];
|
||||
});
|
||||
export const selectSuperChatDataForUri = (state: State, uri: string) => {
|
||||
const byUri = selectSuperchatsByUri(state);
|
||||
return byUri[uri];
|
||||
};
|
||||
|
||||
export const makeSelectSuperChatsForUri = (uri: string) =>
|
||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||
if (!superChatData) {
|
||||
return undefined;
|
||||
export const selectSuperChatsForUri = (state: State, uri: string) => {
|
||||
const superChatData = selectSuperChatDataForUri(state, uri);
|
||||
return superChatData ? superChatData.comments : undefined;
|
||||
};
|
||||
|
||||
export const selectSuperChatTotalAmountForUri = (state: State, uri: string) => {
|
||||
const superChatData = selectSuperChatDataForUri(state, uri);
|
||||
return superChatData ? superChatData.totalAmount : 0;
|
||||
};
|
||||
|
||||
export const selectChannelMentionData = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectClaimIdsByUri,
|
||||
selectClaimsById,
|
||||
selectTopLevelCommentsForUri,
|
||||
selectSubscriptionUris,
|
||||
selectMentionSearchResults,
|
||||
selectMentionQuery,
|
||||
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris, searchUris, query) => {
|
||||
let canonicalCreatorUri;
|
||||
const commentorUris = [];
|
||||
const canonicalCommentors = [];
|
||||
const canonicalSubscriptions = [];
|
||||
const canonicalSearch = [];
|
||||
|
||||
if (uri) {
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
const channelFromClaim = claim && getChannelFromClaim(claim);
|
||||
canonicalCreatorUri = channelFromClaim && channelFromClaim.canonical_url;
|
||||
|
||||
topLevelComments.forEach(({ channel_url: uri }) => {
|
||||
// Check: if there are duplicate commentors
|
||||
if (!commentorUris.includes(uri)) {
|
||||
// Update: commentorUris
|
||||
commentorUris.push(uri);
|
||||
|
||||
// Update: canonicalCommentors
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalCommentors.push(claim.canonical_url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return superChatData.comments;
|
||||
});
|
||||
subscriptionUris.forEach((uri) => {
|
||||
// Update: canonicalSubscriptions
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalSubscriptions.push(claim.canonical_url);
|
||||
}
|
||||
});
|
||||
|
||||
export const makeSelectSuperChatTotalAmountForUri = (uri: string) =>
|
||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||
if (!superChatData) {
|
||||
return 0;
|
||||
let hasNewResolvedResults = false;
|
||||
if (searchUris && searchUris.length > 0) {
|
||||
searchUris.forEach((uri) => {
|
||||
// Update: canonicalSubscriptions
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalSearch.push(claim.canonical_url);
|
||||
}
|
||||
});
|
||||
hasNewResolvedResults = canonicalSearch.length > 0;
|
||||
}
|
||||
|
||||
return superChatData.totalAmount;
|
||||
});
|
||||
return {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions,
|
||||
commentorUris,
|
||||
hasNewResolvedResults,
|
||||
query,
|
||||
};
|
||||
}
|
||||
)((state, uri, maxCount) => `${String(uri)}:${maxCount}`);
|
||||
|
|
|
@ -6,13 +6,14 @@ import {
|
|||
selectClaimsByUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectClaimIsNsfw,
|
||||
selectClaimIsNsfwForUri,
|
||||
makeSelectPendingClaimForUri,
|
||||
selectIsUriResolving,
|
||||
} from 'redux/selectors/claims';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { isClaimNsfw } from 'util/claim';
|
||||
import { createSelector } from 'reselect';
|
||||
import { createCachedSelector } from 're-reselect';
|
||||
import { createNormalizedSearchKey, getRecommendationSearchOptions } from 'util/search';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectHistory } from 'redux/selectors/content';
|
||||
|
@ -22,24 +23,16 @@ type State = { claims: any, search: SearchState };
|
|||
|
||||
export const selectState = (state: State): SearchState => state.search;
|
||||
|
||||
export const selectSearchValue: (state: State) => string = createSelector(selectState, (state) => state.searchQuery);
|
||||
|
||||
export const selectSearchOptions: (state: State) => SearchOptions = createSelector(
|
||||
selectState,
|
||||
(state) => state.options
|
||||
);
|
||||
|
||||
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching);
|
||||
|
||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
||||
selectState,
|
||||
(state) => state.resultsByQuery
|
||||
);
|
||||
|
||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector(
|
||||
selectState,
|
||||
(state) => state.hasReachedMaxResultsLength
|
||||
);
|
||||
// $FlowFixMe - 'searchQuery' is never populated. Something lost in a merge?
|
||||
export const selectSearchValue: (state: State) => string = (state) => selectState(state).searchQuery;
|
||||
export const selectSearchOptions: (state: State) => SearchOptions = (state) => selectState(state).options;
|
||||
export const selectIsSearching: (state: State) => boolean = (state) => selectState(state).searching;
|
||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = (state) =>
|
||||
selectState(state).resultsByQuery;
|
||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) =>
|
||||
selectState(state).hasReachedMaxResultsLength;
|
||||
export const selectMentionSearchResults: (state: State) => Array<string> = (state) => selectState(state).results;
|
||||
export const selectMentionQuery: (state: State) => string = (state) => selectState(state).mentionQuery;
|
||||
|
||||
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
|
||||
createSelector(selectSearchResultByQuery, (byQuery) => {
|
||||
|
@ -60,93 +53,93 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
|
|||
return hasReachedMaxResultsLength[query];
|
||||
});
|
||||
|
||||
export const makeSelectRecommendedContentForUri = (uri: string) =>
|
||||
createSelector(
|
||||
selectHistory,
|
||||
selectClaimsByUri,
|
||||
selectShowMatureContent,
|
||||
selectMutedChannels,
|
||||
selectAllCostInfoByUri,
|
||||
selectSearchResultByQuery,
|
||||
makeSelectClaimIsNsfw(uri),
|
||||
(history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
||||
const claim = claimsByUri[uri];
|
||||
export const selectRecommendedContentForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectHistory,
|
||||
selectClaimsByUri,
|
||||
selectShowMatureContent,
|
||||
selectMutedChannels,
|
||||
selectAllCostInfoByUri,
|
||||
selectSearchResultByQuery,
|
||||
selectClaimIsNsfwForUri, // (state, uri)
|
||||
(uri, history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
||||
const claim = claimsByUri[uri];
|
||||
|
||||
if (!claim) return;
|
||||
if (!claim) return;
|
||||
|
||||
let recommendedContent;
|
||||
// always grab the claimId - this value won't change for filtering
|
||||
const currentClaimId = claim.claim_id;
|
||||
let recommendedContent;
|
||||
// always grab the claimId - this value won't change for filtering
|
||||
const currentClaimId = claim.claim_id;
|
||||
|
||||
const { title } = claim.value;
|
||||
const { title } = claim.value;
|
||||
|
||||
if (!title) return;
|
||||
if (!title) return;
|
||||
|
||||
const options: {
|
||||
size: number,
|
||||
nsfw?: boolean,
|
||||
isBackgroundSearch?: boolean,
|
||||
} = { size: 20, nsfw: matureEnabled, isBackgroundSearch: true };
|
||||
const options: {
|
||||
size: number,
|
||||
nsfw?: boolean,
|
||||
isBackgroundSearch?: boolean,
|
||||
} = { size: 20, nsfw: matureEnabled, isBackgroundSearch: true };
|
||||
|
||||
if (matureEnabled || (!matureEnabled && !isMature)) {
|
||||
options[SEARCH_OPTIONS.RELATED_TO] = claim.claim_id;
|
||||
}
|
||||
|
||||
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
||||
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
||||
|
||||
let searchResult = searchUrisByQuery[normalizedSearchQuery];
|
||||
|
||||
if (searchResult) {
|
||||
// Filter from recommended: The same claim and blocked channels
|
||||
recommendedContent = searchResult['uris'].filter((searchUri) => {
|
||||
const searchClaim = claimsByUri[searchUri];
|
||||
|
||||
if (!searchClaim) return;
|
||||
|
||||
const signingChannel = searchClaim && searchClaim.signing_channel;
|
||||
const channelUri = signingChannel && signingChannel.canonical_url;
|
||||
const blockedMatch = blockedChannels.some((blockedUri) => blockedUri.includes(channelUri));
|
||||
|
||||
let isEqualUri;
|
||||
try {
|
||||
const { claimId: searchId } = parseURI(searchUri);
|
||||
isEqualUri = searchId === currentClaimId;
|
||||
} catch (e) {}
|
||||
|
||||
return !isEqualUri && !blockedMatch;
|
||||
});
|
||||
|
||||
// Claim to play next: playable and free claims not played before in history
|
||||
const nextUriToPlay = recommendedContent.filter((nextRecommendedUri) => {
|
||||
const costInfo = costInfoByUri[nextRecommendedUri] && costInfoByUri[nextRecommendedUri].cost;
|
||||
const recommendedClaim = claimsByUri[nextRecommendedUri];
|
||||
const isVideo = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'video';
|
||||
const isAudio = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'audio';
|
||||
|
||||
let historyMatch = false;
|
||||
try {
|
||||
const { claimId: nextRecommendedId } = parseURI(nextRecommendedUri);
|
||||
|
||||
historyMatch = history.some(
|
||||
(historyItem) =>
|
||||
(claimsByUri[historyItem.uri] && claimsByUri[historyItem.uri].claim_id) === nextRecommendedId
|
||||
);
|
||||
} catch (e) {}
|
||||
|
||||
return !historyMatch && costInfo === 0 && (isVideo || isAudio);
|
||||
})[0];
|
||||
|
||||
const index = recommendedContent.indexOf(nextUriToPlay);
|
||||
if (index > 0) {
|
||||
const a = recommendedContent[0];
|
||||
recommendedContent[0] = nextUriToPlay;
|
||||
recommendedContent[index] = a;
|
||||
}
|
||||
}
|
||||
return recommendedContent;
|
||||
if (matureEnabled || (!matureEnabled && !isMature)) {
|
||||
options[SEARCH_OPTIONS.RELATED_TO] = claim.claim_id;
|
||||
}
|
||||
);
|
||||
|
||||
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
||||
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
||||
|
||||
let searchResult = searchUrisByQuery[normalizedSearchQuery];
|
||||
|
||||
if (searchResult) {
|
||||
// Filter from recommended: The same claim and blocked channels
|
||||
recommendedContent = searchResult['uris'].filter((searchUri) => {
|
||||
const searchClaim = claimsByUri[searchUri];
|
||||
|
||||
if (!searchClaim) return;
|
||||
|
||||
const signingChannel = searchClaim && searchClaim.signing_channel;
|
||||
const channelUri = signingChannel && signingChannel.canonical_url;
|
||||
const blockedMatch = blockedChannels.some((blockedUri) => blockedUri.includes(channelUri));
|
||||
|
||||
let isEqualUri;
|
||||
try {
|
||||
const { claimId: searchId } = parseURI(searchUri);
|
||||
isEqualUri = searchId === currentClaimId;
|
||||
} catch (e) {}
|
||||
|
||||
return !isEqualUri && !blockedMatch;
|
||||
});
|
||||
|
||||
// Claim to play next: playable and free claims not played before in history
|
||||
const nextUriToPlay = recommendedContent.filter((nextRecommendedUri) => {
|
||||
const costInfo = costInfoByUri[nextRecommendedUri] && costInfoByUri[nextRecommendedUri].cost;
|
||||
const recommendedClaim = claimsByUri[nextRecommendedUri];
|
||||
const isVideo = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'video';
|
||||
const isAudio = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'audio';
|
||||
|
||||
let historyMatch = false;
|
||||
try {
|
||||
const { claimId: nextRecommendedId } = parseURI(nextRecommendedUri);
|
||||
|
||||
historyMatch = history.some(
|
||||
(historyItem) =>
|
||||
(claimsByUri[historyItem.uri] && claimsByUri[historyItem.uri].claim_id) === nextRecommendedId
|
||||
);
|
||||
} catch (e) {}
|
||||
|
||||
return !historyMatch && costInfo === 0 && (isVideo || isAudio);
|
||||
})[0];
|
||||
|
||||
const index = recommendedContent.indexOf(nextUriToPlay);
|
||||
if (index > 0) {
|
||||
const a = recommendedContent[0];
|
||||
recommendedContent[0] = nextUriToPlay;
|
||||
recommendedContent[index] = a;
|
||||
}
|
||||
}
|
||||
return recommendedContent;
|
||||
}
|
||||
)((state, uri) => String(uri));
|
||||
|
||||
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
||||
createSelector(
|
||||
|
|
|
@ -20,6 +20,11 @@ export const selectSubscriptions = createSelector(
|
|||
(state) => state.subscriptions && state.subscriptions.sort((a, b) => a.channelName.localeCompare(b.channelName))
|
||||
);
|
||||
|
||||
export const selectSubscriptionUris = createSelector(
|
||||
selectSubscriptions,
|
||||
(subscriptions) => subscriptions && subscriptions.map((sub) => sub.uri)
|
||||
);
|
||||
|
||||
export const selectFollowing = createSelector(selectState, (state) => state.following && state.following);
|
||||
|
||||
// Fetching list of users subscriptions
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
@import 'component/button';
|
||||
@import 'component/card';
|
||||
@import 'component/channel';
|
||||
@import 'component/channel-mention';
|
||||
@import 'component/_textarea-suggestions';
|
||||
@import 'component/claim-list';
|
||||
@import 'component/collection';
|
||||
@import 'component/comments';
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
$thumbnailWidth: 1.5rem;
|
||||
$thumbnailWidthSmall: 1rem;
|
||||
|
||||
.content_comment {
|
||||
.create__comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
|
@ -428,6 +428,7 @@
|
|||
max-width: 32rem;
|
||||
}
|
||||
|
||||
// maybe remove all REMOVE
|
||||
.main-wrapper--scrollbar {
|
||||
// The W3C future standard; currently supported by Firefox only.
|
||||
// It'll hopefully auto fallback to this when 'webkit-scrollbar' below is deprecated in the future.
|
||||
|
|
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;
|
||||
}
|
|
@ -28,6 +28,27 @@ body {
|
|||
font-weight: 400;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
|
||||
// The W3C future standard; currently supported by Firefox only.
|
||||
// It'll hopefully auto fallback to this when 'webkit-scrollbar' below is deprecated in the future.
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: var(--color-scrollbar-thumb-bg) var(--color-scrollbar-track-bg);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-track {
|
||||
background: var(--color-scrollbar-track-bg);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-thumb {
|
||||
// Don't set 'border-radius' because Firefox's 'scrollbar-xx'
|
||||
// standard currently doesn't support it. Stick with square
|
||||
// scrollbar for all browsers.
|
||||
background-color: var(--color-scrollbar-thumb-bg);
|
||||
}
|
||||
|
||||
hr {
|
||||
|
|
336
yarn.lock
336
yarn.lock
|
@ -275,6 +275,13 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.12.5"
|
||||
|
||||
"@babel/helper-module-imports@^7.12.13":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3"
|
||||
integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.0"
|
||||
|
||||
"@babel/helper-module-transforms@^7.11.0":
|
||||
version "7.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
|
||||
|
@ -438,7 +445,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
|
||||
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.14.9":
|
||||
"@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.15.7":
|
||||
version "7.15.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
|
||||
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
|
||||
|
@ -679,6 +686,13 @@
|
|||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.8.0"
|
||||
|
||||
"@babel/plugin-syntax-jsx@^7.12.13":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.0.tgz#f9624394317365a9a88c82358d3f8471154698f1"
|
||||
integrity sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
|
||||
"@babel/plugin-syntax-jsx@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94"
|
||||
|
@ -1160,6 +1174,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7":
|
||||
version "7.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.10.1", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
|
||||
|
@ -1257,6 +1278,14 @@
|
|||
"@babel/helper-validator-identifier" "^7.14.9"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba"
|
||||
integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.15.7"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@datapunt/matomo-tracker-js@^0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@datapunt/matomo-tracker-js/-/matomo-tracker-js-0.1.4.tgz#1226f0964d2c062bf9392e9c2fd89838262b10df"
|
||||
|
@ -1291,6 +1320,107 @@
|
|||
resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-2.0.1.tgz#810cbc595a21f0f94641eb2d7e8264063a3f84de"
|
||||
integrity sha512-bGX4/yB2bPZwXm1DsxgoABgH0Cz7oFtXJgkerB8VrStYdTyvhGAULzNLRn9rVmeAuC3VUDXaXpZIlZAZHpsLIA==
|
||||
|
||||
"@emotion/babel-plugin@^11.3.0":
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz#3a16850ba04d8d9651f07f3fb674b3436a4fb9d7"
|
||||
integrity sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.12.13"
|
||||
"@babel/plugin-syntax-jsx" "^7.12.13"
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@emotion/hash" "^0.8.0"
|
||||
"@emotion/memoize" "^0.7.5"
|
||||
"@emotion/serialize" "^1.0.2"
|
||||
babel-plugin-macros "^2.6.1"
|
||||
convert-source-map "^1.5.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
find-root "^1.1.0"
|
||||
source-map "^0.5.7"
|
||||
stylis "^4.0.3"
|
||||
|
||||
"@emotion/cache@^11.6.0":
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.6.0.tgz#65fbdbbe4382f1991d8b20853c38e63ecccec9a1"
|
||||
integrity sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.7.4"
|
||||
"@emotion/sheet" "^1.1.0"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
"@emotion/weak-memoize" "^0.2.5"
|
||||
stylis "^4.0.10"
|
||||
|
||||
"@emotion/hash@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
|
||||
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||
|
||||
"@emotion/is-prop-valid@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz#cbd843d409dfaad90f9404e7c0404c55eae8c134"
|
||||
integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.7.4"
|
||||
|
||||
"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
|
||||
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
|
||||
|
||||
"@emotion/react@^11.6.0":
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.6.0.tgz#61fcb95c1e01255734c2c721cb9beabcf521eb0f"
|
||||
integrity sha512-23MnRZFBN9+D1lHXC5pD6z4X9yhPxxtHr6f+iTGz6Fv6Rda0GdefPrsHL7otsEf+//7uqCdT5QtHeRxHCERzuw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@emotion/cache" "^11.6.0"
|
||||
"@emotion/serialize" "^1.0.2"
|
||||
"@emotion/sheet" "^1.1.0"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
"@emotion/weak-memoize" "^0.2.5"
|
||||
hoist-non-react-statics "^3.3.1"
|
||||
|
||||
"@emotion/serialize@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
|
||||
integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==
|
||||
dependencies:
|
||||
"@emotion/hash" "^0.8.0"
|
||||
"@emotion/memoize" "^0.7.4"
|
||||
"@emotion/unitless" "^0.7.5"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@emotion/sheet@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
|
||||
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
|
||||
|
||||
"@emotion/styled@^11.6.0":
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d"
|
||||
integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@emotion/babel-plugin" "^11.3.0"
|
||||
"@emotion/is-prop-valid" "^1.1.1"
|
||||
"@emotion/serialize" "^1.0.2"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
|
||||
"@emotion/unitless@^0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
|
||||
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
|
||||
|
||||
"@emotion/utils@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af"
|
||||
integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==
|
||||
|
||||
"@emotion/weak-memoize@^0.2.5":
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@gar/promisify@^1.0.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
|
||||
|
@ -1321,6 +1451,85 @@
|
|||
tough-cookie "^2.3.1"
|
||||
tough-cookie-web-storage-store "^1.0.0"
|
||||
|
||||
"@mui/base@5.0.0-alpha.57":
|
||||
version "5.0.0-alpha.57"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.57.tgz#1f9bc74da67eec8fbad54402b28c1356ec7c53ae"
|
||||
integrity sha512-UCJthNc4LGttoD/CxdCh8AaEu2B2uWNRW96J6PjlQ125+FEqO7+wuIGT98BNCGguVwetK/jTmo/fiHYDoW9gUA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@emotion/is-prop-valid" "^1.1.1"
|
||||
"@mui/utils" "^5.2.1"
|
||||
"@popperjs/core" "^2.4.4"
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
"@mui/material@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.2.1.tgz#ef75df33da37ed6de1aadf66d2e33fea9685cd0d"
|
||||
integrity sha512-y38+e1Qf95rVQ4lK8knYj4o1kB/WwJU0f/lMNmzlaenqGpyhd1M/e3BNwuYEDOLSPWUVeP2LvX2mL/IhKytA9A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@mui/base" "5.0.0-alpha.57"
|
||||
"@mui/system" "^5.2.1"
|
||||
"@mui/types" "^7.1.0"
|
||||
"@mui/utils" "^5.2.1"
|
||||
"@types/react-transition-group" "^4.4.4"
|
||||
clsx "^1.1.1"
|
||||
csstype "^3.0.10"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
react-transition-group "^4.4.2"
|
||||
|
||||
"@mui/private-theming@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.2.1.tgz#07537a065d752a0d6309ce0db42378f104d1885d"
|
||||
integrity sha512-+OfgeZzEjqwd7Vo1kYISJyLHM+3yUO8UoKhLMtZ1DAlZlqovN6jetPtT6o4BnHEAsc3YC3DET+KicwkRtuvxbw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@mui/utils" "^5.2.1"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/styled-engine@^5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.2.0.tgz#5c97e2b1b6c4c2d9991f07517ed862972d362b85"
|
||||
integrity sha512-NZ4pWYQcM5wreUfiXRd7IMFRF+Nq1vMzsIdXtXNjgctJTKHunrofasoBqv+cqevO+hqT75ezSbNHyaXzOXp6Mg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@emotion/cache" "^11.6.0"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/system@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.2.1.tgz#593e9e8d8ab3dd8946f4e98e5ad7feabbcdb8e80"
|
||||
integrity sha512-C1mva6Uyk2bGCaa/FiaFseSt2iJymxgA8KnJJyKAz8ZQZzuetUV8JbY1qtV9CG1VlJb+Ldm7pc6Px8t59lGfZw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@mui/private-theming" "^5.2.1"
|
||||
"@mui/styled-engine" "^5.2.0"
|
||||
"@mui/types" "^7.1.0"
|
||||
"@mui/utils" "^5.2.1"
|
||||
clsx "^1.1.1"
|
||||
csstype "^3.0.10"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/types@^7.1.0":
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.0.tgz#5ed928c5a41cfbf9a4be82ea3bbdc47bcc9610d5"
|
||||
integrity sha512-Hh7ALdq/GjfIwLvqH3XftuY3bcKhupktTm+S6qRIDGOtPtRuq2L21VWzOK4p7kblirK0XgGVH5BLwa6u8z/6QQ==
|
||||
|
||||
"@mui/utils@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.2.1.tgz#fdd70067f8fb2d73225d56f2e705afacdeb3255b"
|
||||
integrity sha512-JQH5ucBxBrubntrN2mvDcwkXlWaHuZGz5goxg9ixnZXRhlZ9Ed5knfsafrX4OFyXNT48DiZXaTRAEkqjyfCExQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.3"
|
||||
"@types/prop-types" "^15.7.4"
|
||||
"@types/react-is" "^16.7.1 || ^17.0.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
|
@ -1441,6 +1650,11 @@
|
|||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
|
||||
|
||||
"@popperjs/core@^2.4.4":
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
|
||||
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
|
||||
|
||||
"@reach/auto-id@0.12.1":
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.12.1.tgz#2e4a7250d2067ec16a9b4ea732695bc75572405c"
|
||||
|
@ -1794,6 +2008,16 @@
|
|||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||
|
||||
"@types/prop-types@*", "@types/prop-types@^15.7.4":
|
||||
version "15.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||
|
||||
"@types/q@^1.5.1":
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
||||
|
@ -1803,6 +2027,34 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/react-calendar/-/react-calendar-3.1.3.tgz#bd0947c28738f6419649be22d80624b05fde2fb9"
|
||||
integrity sha512-4kvDfKta9bNnuRieuGYPxdDlh3UqRUKE8+fMbmZGk0Z/MdUGHupxXwPCWLbVH7FZU48o4bhT+XX8rfZrexdnAw==
|
||||
|
||||
"@types/react-is@^16.7.1 || ^17.0.0":
|
||||
version "17.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
|
||||
integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.4.4":
|
||||
version "4.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
|
||||
integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*":
|
||||
version "17.0.37"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959"
|
||||
integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||
|
||||
"@types/semver@^7.1.0":
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.1.0.tgz#c8c630d4c18cd326beff77404887596f96408408"
|
||||
|
@ -2564,6 +2816,15 @@ babel-plugin-import-glob@^2.0.0:
|
|||
identifierfy "^1.1.0"
|
||||
minimatch-capture "^1.1.0"
|
||||
|
||||
babel-plugin-macros@^2.6.1:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
|
||||
integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.7.2"
|
||||
cosmiconfig "^6.0.0"
|
||||
resolve "^1.12.0"
|
||||
|
||||
babel-plugin-syntax-object-rest-spread@^6.8.0:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
|
||||
|
@ -3430,6 +3691,11 @@ clsx@^1.0.4:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
@ -3677,6 +3943,13 @@ content-type@~1.0.4:
|
|||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
|
||||
convert-source-map@^1.5.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
|
||||
integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.1"
|
||||
|
||||
convert-source-map@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
|
||||
|
@ -3767,6 +4040,17 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.0.2, cosmiconfig@^5.2.1:
|
|||
js-yaml "^3.13.1"
|
||||
parse-json "^4.0.0"
|
||||
|
||||
cosmiconfig@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
|
||||
integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
|
||||
dependencies:
|
||||
"@types/parse-json" "^4.0.0"
|
||||
import-fresh "^3.1.0"
|
||||
parse-json "^5.0.0"
|
||||
path-type "^4.0.0"
|
||||
yaml "^1.7.2"
|
||||
|
||||
country-data@^0.0.31:
|
||||
version "0.0.31"
|
||||
resolved "https://registry.yarnpkg.com/country-data/-/country-data-0.0.31.tgz#80966b8e1d147fa6d6a589d32933f8793774956d"
|
||||
|
@ -4076,6 +4360,11 @@ csso@^4.0.2:
|
|||
dependencies:
|
||||
css-tree "1.0.0-alpha.37"
|
||||
|
||||
csstype@^3.0.10, csstype@^3.0.2:
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
|
||||
integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
|
||||
|
||||
currency-symbol-map@~2:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/currency-symbol-map/-/currency-symbol-map-2.2.0.tgz#2b3c1872ff1ac2ce595d8273e58e1fff0272aea2"
|
||||
|
@ -4393,6 +4682,14 @@ dom-converter@^0.2:
|
|||
dependencies:
|
||||
utila "~0.4"
|
||||
|
||||
dom-helpers@^5.0.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^3.0.2"
|
||||
|
||||
dom-scroll-into-view@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz#e8f36732dd089b0201a88d7815dc3f88e6d66c7e"
|
||||
|
@ -6329,7 +6626,7 @@ hoek@4.x.x:
|
|||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
|
||||
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
|
||||
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
dependencies:
|
||||
|
@ -6650,6 +6947,14 @@ import-fresh@^3.0.0:
|
|||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
import-fresh@^3.1.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
||||
dependencies:
|
||||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
import-from@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
|
||||
|
@ -10010,6 +10315,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2:
|
|||
version "16.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"
|
||||
|
||||
react-is@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
|
@ -10103,6 +10413,16 @@ react-time-picker@^4.2.0:
|
|||
react-fit "^1.0.3"
|
||||
update-input-width "^1.1.1"
|
||||
|
||||
react-transition-group@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
|
||||
integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@^16.8.2:
|
||||
version "16.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||
|
@ -11192,7 +11512,7 @@ source-map-url@^0.4.0:
|
|||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
||||
|
||||
source-map@^0.5.0, source-map@^0.5.6:
|
||||
source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
||||
|
@ -11580,6 +11900,11 @@ stylehacks@^4.0.0:
|
|||
postcss "^7.0.0"
|
||||
postcss-selector-parser "^3.0.0"
|
||||
|
||||
stylis@^4.0.10, stylis@^4.0.3:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
|
||||
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
|
||||
|
||||
sumchecker@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
|
||||
|
@ -13095,6 +13420,11 @@ yallist@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yaml@^1.7.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yargs-parser@^11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
||||
|
|
Loading…
Reference in a new issue