Bringing in emotes, stickers, and refactors from ody #7435
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!",
|
||||
{
|
||||
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, () => {
|
||||
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,6 +237,8 @@ export class FormField extends React.PureComponent<Props> {
|
|||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
|
@ -238,6 +246,17 @@ export class FormField extends React.PureComponent<Props> {
|
|||
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) => {
|
||||
export const selectSuperChatDataForUri = (state: State, uri: string) => {
|
||||
const byUri = selectSuperchatsByUri(state);
|
||||
return byUri[uri];
|
||||
});
|
||||
};
|
||||
|
||||
export const makeSelectSuperChatsForUri = (uri: string) =>
|
||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||
if (!superChatData) {
|
||||
return undefined;
|
||||
export const selectSuperChatsForUri = (state: State, uri: string) => {
|
||||
const superChatData = selectSuperChatDataForUri(state, uri);
|
||||
return superChatData ? superChatData.comments : undefined;
|
||||
};
|
||||
|
||||
export const selectSuperChatTotalAmountForUri = (state: State, uri: string) => {
|
||||
const superChatData = selectSuperChatDataForUri(state, uri);
|
||||
return superChatData ? superChatData.totalAmount : 0;
|
||||
};
|
||||
|
||||
export const selectChannelMentionData = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectClaimIdsByUri,
|
||||
selectClaimsById,
|
||||
selectTopLevelCommentsForUri,
|
||||
selectSubscriptionUris,
|
||||
selectMentionSearchResults,
|
||||
selectMentionQuery,
|
||||
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris, searchUris, query) => {
|
||||
let canonicalCreatorUri;
|
||||
const commentorUris = [];
|
||||
const canonicalCommentors = [];
|
||||
const canonicalSubscriptions = [];
|
||||
const canonicalSearch = [];
|
||||
|
||||
if (uri) {
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
const channelFromClaim = claim && getChannelFromClaim(claim);
|
||||
canonicalCreatorUri = channelFromClaim && channelFromClaim.canonical_url;
|
||||
|
||||
topLevelComments.forEach(({ channel_url: uri }) => {
|
||||
// Check: if there are duplicate commentors
|
||||
if (!commentorUris.includes(uri)) {
|
||||
// Update: commentorUris
|
||||
commentorUris.push(uri);
|
||||
|
||||
// Update: canonicalCommentors
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalCommentors.push(claim.canonical_url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return superChatData.comments;
|
||||
subscriptionUris.forEach((uri) => {
|
||||
// Update: canonicalSubscriptions
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalSubscriptions.push(claim.canonical_url);
|
||||
}
|
||||
});
|
||||
|
||||
export const makeSelectSuperChatTotalAmountForUri = (uri: string) =>
|
||||
createSelector(makeSelectSuperChatDataForUri(uri), (superChatData) => {
|
||||
if (!superChatData) {
|
||||
return 0;
|
||||
let hasNewResolvedResults = false;
|
||||
if (searchUris && searchUris.length > 0) {
|
||||
searchUris.forEach((uri) => {
|
||||
// Update: canonicalSubscriptions
|
||||
const claimId = claimIdsByUri[uri];
|
||||
const claim = claimsById[claimId];
|
||||
if (claim && claim.canonical_url) {
|
||||
canonicalSearch.push(claim.canonical_url);
|
||||
}
|
||||
});
|
||||
hasNewResolvedResults = canonicalSearch.length > 0;
|
||||
}
|
||||
|
||||
return superChatData.totalAmount;
|
||||
});
|
||||
return {
|
||||
canonicalCommentors,
|
||||
canonicalCreatorUri,
|
||||
canonicalSearch,
|
||||
canonicalSubscriptions,
|
||||
commentorUris,
|
||||
hasNewResolvedResults,
|
||||
query,
|
||||
};
|
||||
}
|
||||
)((state, uri, maxCount) => `${String(uri)}:${maxCount}`);
|
||||
|
|
|
@ -6,13 +6,14 @@ import {
|
|||
selectClaimsByUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectClaimIsNsfw,
|
||||
selectClaimIsNsfwForUri,
|
||||
makeSelectPendingClaimForUri,
|
||||
selectIsUriResolving,
|
||||
} from 'redux/selectors/claims';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { isClaimNsfw } from 'util/claim';
|
||||
import { createSelector } from 'reselect';
|
||||
import { createCachedSelector } from 're-reselect';
|
||||
import { createNormalizedSearchKey, getRecommendationSearchOptions } from 'util/search';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectHistory } from 'redux/selectors/content';
|
||||
|
@ -22,24 +23,16 @@ type State = { claims: any, search: SearchState };
|
|||
|
||||
export const selectState = (state: State): SearchState => state.search;
|
||||
|
||||
export const selectSearchValue: (state: State) => string = createSelector(selectState, (state) => state.searchQuery);
|
||||
|
||||
export const selectSearchOptions: (state: State) => SearchOptions = createSelector(
|
||||
selectState,
|
||||
(state) => state.options
|
||||
);
|
||||
|
||||
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching);
|
||||
|
||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
||||
selectState,
|
||||
(state) => state.resultsByQuery
|
||||
);
|
||||
|
||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector(
|
||||
selectState,
|
||||
(state) => state.hasReachedMaxResultsLength
|
||||
);
|
||||
// $FlowFixMe - 'searchQuery' is never populated. Something lost in a merge?
|
||||
export const selectSearchValue: (state: State) => string = (state) => selectState(state).searchQuery;
|
||||
export const selectSearchOptions: (state: State) => SearchOptions = (state) => selectState(state).options;
|
||||
export const selectIsSearching: (state: State) => boolean = (state) => selectState(state).searching;
|
||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = (state) =>
|
||||
selectState(state).resultsByQuery;
|
||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) =>
|
||||
selectState(state).hasReachedMaxResultsLength;
|
||||
export const selectMentionSearchResults: (state: State) => Array<string> = (state) => selectState(state).results;
|
||||
export const selectMentionQuery: (state: State) => string = (state) => selectState(state).mentionQuery;
|
||||
|
||||
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
|
||||
createSelector(selectSearchResultByQuery, (byQuery) => {
|
||||
|
@ -60,16 +53,16 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
|
|||
return hasReachedMaxResultsLength[query];
|
||||
});
|
||||
|
||||
export const makeSelectRecommendedContentForUri = (uri: string) =>
|
||||
createSelector(
|
||||
export const selectRecommendedContentForUri = createCachedSelector(
|
||||
(state, uri) => uri,
|
||||
selectHistory,
|
||||
selectClaimsByUri,
|
||||
selectShowMatureContent,
|
||||
selectMutedChannels,
|
||||
selectAllCostInfoByUri,
|
||||
selectSearchResultByQuery,
|
||||
makeSelectClaimIsNsfw(uri),
|
||||
(history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
||||
selectClaimIsNsfwForUri, // (state, uri)
|
||||
(uri, history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
||||
const claim = claimsByUri[uri];
|
||||
|
||||
if (!claim) return;
|
||||
|
@ -146,7 +139,7 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
|
|||
}
|
||||
return recommendedContent;
|
||||
}
|
||||
);
|
||||
)((state, uri) => String(uri));
|
||||
|
||||
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
||||
createSelector(
|
||||
|
|
|
@ -20,6 +20,11 @@ export const selectSubscriptions = createSelector(
|
|||
(state) => state.subscriptions && state.subscriptions.sort((a, b) => a.channelName.localeCompare(b.channelName))
|
||||
);
|
||||
|
||||
export const selectSubscriptionUris = createSelector(
|
||||
selectSubscriptions,
|
||||
(subscriptions) => subscriptions && subscriptions.map((sub) => sub.uri)
|
||||
);
|
||||
|
||||
export const selectFollowing = createSelector(selectState, (state) => state.following && state.following);
|
||||
|
||||
// Fetching list of users subscriptions
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
@import 'component/button';
|
||||
@import 'component/card';
|
||||
@import 'component/channel';
|
||||
@import 'component/channel-mention';
|
||||
@import 'component/_textarea-suggestions';
|
||||
@import 'component/claim-list';
|
||||
@import 'component/collection';
|
||||
@import 'component/comments';
|
||||
|
|
|
@ -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