diff --git a/flow-typed/search.js b/flow-typed/search.js index b795f9c12..26f4cbba9 100644 --- a/flow-typed/search.js +++ b/flow-typed/search.js @@ -29,8 +29,10 @@ declare type SearchOptions = { declare type SearchState = { options: SearchOptions, resultsByQuery: {}, + results: Array, hasReachedMaxResultsLength: {}, searching: boolean, + mentionQuery: string, }; declare type SearchSuccess = { @@ -41,6 +43,7 @@ declare type SearchSuccess = { size: number, uris: Array, recsys: string, + query: string, }, }; diff --git a/package.json b/package.json index 49e2f572b..9a83474d8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/ui/component/app/view.jsx b/ui/component/app/view.jsx index aeb10d572..e442a7fcd 100644 --- a/ui/component/app/view.jsx +++ b/ui/component/app/view.jsx @@ -68,8 +68,8 @@ type Props = { syncLoop: (?boolean) => void, currentModal: any, syncFatalError: boolean, - activeChannelClaim: ?ChannelClaim, - myChannelUrls: ?Array, + activeChannelId: ?string, + myChannelClaimIds: ?Array, subscriptions: Array, 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(() => { diff --git a/ui/component/channelMentionSuggestion/index.js b/ui/component/channelMentionSuggestion/index.js deleted file mode 100644 index 787502cc3..000000000 --- a/ui/component/channelMentionSuggestion/index.js +++ /dev/null @@ -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); diff --git a/ui/component/channelMentionSuggestion/view.jsx b/ui/component/channelMentionSuggestion/view.jsx deleted file mode 100644 index 9d7a2afdc..000000000 --- a/ui/component/channelMentionSuggestion/view.jsx +++ /dev/null @@ -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 : ( - - {isResolvingUri ? ( -
-
-
- ) : ( -
- - -
{(claim.value && claim.value.title) || claim.name}
-
{claim.name}
-
-
- )} - - ); -} diff --git a/ui/component/channelMentionSuggestions/index.js b/ui/component/channelMentionSuggestions/index.js deleted file mode 100644 index c1f635011..000000000 --- a/ui/component/channelMentionSuggestions/index.js +++ /dev/null @@ -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)); diff --git a/ui/component/channelMentionSuggestions/view.jsx b/ui/component/channelMentionSuggestions/view.jsx deleted file mode 100644 index bb04de507..000000000 --- a/ui/component/channelMentionSuggestions/view.jsx +++ /dev/null @@ -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, - subscriptionUris: Array, - unresolvedSubscriptions: Array, - canonicalCreator: string, - canonicalCommentors: Array, - canonicalSubscriptions: Array, - doResolveUris: (Array) => 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 = React.useRef(); - const comboboxListRef: ElementRef = 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, - canonical: Array, - 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 : ( - <> -
{label}
- {suggestions.map((uri) => ( - - ))} - {hasSuggestionsBelow &&
} - - ); - }; - - return isRefFocused(inputRef) || isRefFocused(comboboxInputRef) ? ( -
handleSelect(mentionTerm)}> - - - {mentionTerm && isUriFromTermValid && ( - - - {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 ? ( - - ) : ( - results && ( - <> - {!noTopSuggestion && ( - setMostSupported(winningUri)} - /> - )} - {suggestionsRow(__('From search'), results, canonicalResults, false)} - - ) - ))} - - - )} - -
- ) : null; -} diff --git a/ui/component/channelMentionTopSuggestion/index.js b/ui/component/channelMentionTopSuggestion/index.js deleted file mode 100644 index 2d21fda09..000000000 --- a/ui/component/channelMentionTopSuggestion/index.js +++ /dev/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); diff --git a/ui/component/channelMentionTopSuggestion/view.jsx b/ui/component/channelMentionTopSuggestion/view.jsx deleted file mode 100644 index 6ea79c233..000000000 --- a/ui/component/channelMentionTopSuggestion/view.jsx +++ /dev/null @@ -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, - 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 ( -
-
- -
-
-
-
-
-
- ); - } - - return !winningUri || shownUris.includes(winningUri) ? null : ( - <> -
- -
- -
- - ); -} diff --git a/ui/component/commentCreate/index.js b/ui/component/commentCreate/index.js index 09f1e8441..add51ccae 100644 --- a/ui/component/commentCreate/index.js +++ b/ui/component/commentCreate/index.js @@ -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); diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 7f1ebedc5..66135c871 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -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 = React.useRef(); - const formFieldInputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input; - const selectionIndex = formFieldInputRef && formFieldInputRef.current && formFieldInputRef.current.selectionStart; const buttonRef: ElementRef = React.useRef(); const { @@ -115,33 +112,14 @@ export function CommentCreate(props: Props) { const [activeTab, setActiveTab] = React.useState(); const [tipError, setTipError] = React.useState(); const [deletedComment, setDeletedComment] = React.useState(false); - const [pauseQuickSend, setPauseQuickSend] = React.useState(false); const [showEmotes, setShowEmotes] = React.useState(false); const [disableReviewButton, setDisableReviewButton] = React.useState(); const [exchangeRate, setExchangeRate] = React.useState(); const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(undefined); - const selectedMentionIndex = - commentValue.indexOf('@', selectionIndex) === selectionIndex - ? commentValue.indexOf('@', selectionIndex) - : commentValue.lastIndexOf('@', selectionIndex); - const modifierIndex = commentValue.indexOf(':', selectedMentionIndex); - const spaceIndex = commentValue.indexOf(' ', selectedMentionIndex); - const mentionLengthIndex = - modifierIndex >= 0 && (spaceIndex === -1 || modifierIndex < spaceIndex) - ? modifierIndex - : spaceIndex >= 0 && (modifierIndex === -1 || spaceIndex < modifierIndex) - ? spaceIndex - : commentValue.length; - const channelMention = - selectedMentionIndex >= 0 && selectionIndex <= mentionLengthIndex - ? commentValue.substring(selectedMentionIndex, mentionLengthIndex) - : ''; - const claimId = claim && claim.claim_id; - const channelUri = claim && (claim.signing_channel ? claim.signing_channel.permanent_url : claim.permanent_url); const charCount = commentValue ? commentValue.length : 0; - const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length || pauseQuickSend; + const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length; const channelId = getChannelIdFromClaim(claim); const channelSettings = channelId ? settingsByChannelId[channelId] : undefined; const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0; @@ -181,19 +159,6 @@ export function CommentCreate(props: Props) { setCommentValue(commentValue); } - function handleSelectMention(mentionValue, key) { - let newMentionValue = mentionValue.replace('lbry://', ''); - if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':'); - - setCommentValue( - commentValue.substring(0, selectedMentionIndex) + - `${newMentionValue}` + - (commentValue.length > mentionLengthIndex + 1 - ? commentValue.substring(mentionLengthIndex, commentValue.length) - : ' ') - ); - } - function altEnterListener(e: SyntheticKeyboardEvent<*>) { if ((e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) { e.preventDefault(); @@ -247,7 +212,9 @@ export function CommentCreate(props: Props) { } function doSubmitTip() { - if (!activeChannelClaim) return; + if (!activeChannelClaim || isSubmitting) return; + + setSubmitting(true); const params = { amount: tipAmount, claim_id: claimId, channel_id: activeChannelClaim.claim_id }; // FIAT ONLY - REMOVE @@ -258,8 +225,6 @@ export function CommentCreate(props: Props) { // const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : claim.claim_id; const tipChannelName = claim.signing_channel ? claim.signing_channel.name : claim.name; - setSubmitting(true); - if (activeTab === TAB_LBC) { // call sendTip and then run the callback from the response // second parameter is callback @@ -273,13 +238,10 @@ export function CommentCreate(props: Props) { }, 1500); doToast({ - message: __( - "You sent %tipAmount% LBRY Credits as a tip to %tipChannelName%, I'm sure they appreciate it!", - { - tipAmount: tipAmount, // force show decimal places - tipChannelName, - } - ), + message: __("You sent %tipAmount% Credits as a tip to %tipChannelName%, I'm sure they appreciate it!", { + tipAmount: tipAmount, // force show decimal places + tipChannelName, + }), }); setSuccessTip({ txid, tipAmount }); @@ -314,8 +276,11 @@ export function CommentCreate(props: Props) { * @param {string} [environment] Optional environment for Stripe (test|live) */ function handleCreateComment(txid, payment_intent_id, environment) { + if (isSubmitting) return; + setShowEmotes(false); setSubmitting(true); + const stickerValue = selectedSticker && buildValidSticker(selectedSticker.name); createComment(stickerValue || commentValue, claimId, parentId, txid, payment_intent_id, environment, !!stickerValue) @@ -366,18 +331,6 @@ export function CommentCreate(props: Props) { } }, [fetchComment, shouldFetchComment, parentId]); - // Debounce for disabling the submit button when mentioning a user with Enter - // so that the comment isn't sent at the same time - React.useEffect(() => { - const timer = setTimeout(() => { - if (pauseQuickSend) { - setPauseQuickSend(false); - } - }, MENTION_DEBOUNCE_MS); - - return () => clearTimeout(timer); - }, [pauseQuickSend]); - // Stickers: Get LBC-USD exchange rate if hasn't yet and selected a paid sticker React.useEffect(() => { if (stickerPrice && !exchangeRate) Lbryio.getExchangeRates().then(({ LBC_USD }) => setExchangeRate(LBC_USD)); @@ -411,6 +364,30 @@ export function CommentCreate(props: Props) { .catch(() => {}); }, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]); + // Handle keyboard shortcut comment creation + // React.useEffect(() => { + // function altEnterListener(e: SyntheticKeyboardEvent<*>) { + // const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input; + // + // if (inputRef && inputRef.current === document.activeElement) { + // // $FlowFixMe + // const isTyping = e.target.attributes['term']; + // + // if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) { + // e.preventDefault(); + // buttonRef.current.click(); + // } + // } + // } + // + // window.addEventListener('keydown', altEnterListener); + // + // // removes the listener so it doesn't cause problems elsewhere in the app + // return () => { + // window.removeEventListener('keydown', altEnterListener); + // }; + // }, [isLivestream]); + // ************************************************************************** // Render // ************************************************************************** @@ -492,37 +469,29 @@ export function CommentCreate(props: Props) { closeSelector={() => setShowEmotes(false)} /> )} - {!advancedEditor && ( - - )} + -
{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}
+
+ {(isReply ? __('Replying as') : __('Comment as')) + ' '} - +
} + name={isReply ? 'content_reply' : 'content_description'} quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')} - quickActionHandler={() => setAdvancedEditor(!advancedEditor)} + ref={formFieldRef} + onChange={handleCommentChange} openEmoteMenu={() => setShowEmotes(!showEmotes)} + quickActionHandler={() => setAdvancedEditor(!advancedEditor)} onFocus={onTextareaFocus} onBlur={onTextareaBlur} placeholder={__('Say something about this...')} value={commentValue} - charCount={charCount} - onChange={handleCommentChange} - autoFocus={isReply} + type={advancedEditor ? 'markdown' : 'textarea'} textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT} /> @@ -587,7 +556,6 @@ export function CommentCreate(props: Props) { icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE} // only LBC label={__('Review')} onClick={() => setReviewingSupportComment(true)} - requiresAuth /> ) : ( (!minTip || claimIsMine) && ( @@ -605,7 +573,6 @@ export function CommentCreate(props: Props) { ? __('Commenting...') : __('Comment --[button to submit something]--') } - requiresAuth onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()} /> ) @@ -628,10 +595,15 @@ export function CommentCreate(props: Props) { {!claimIsMine && ( <> {(!isSupportComment || activeTab !== TAB_LBC) && - getActionButton(__('LBC'), isSupportComment ? __('Switch to LBC') : undefined, ICONS.LBC, () => { - setIsSupportComment(true); - setActiveTab(TAB_LBC); - })} + getActionButton( + __('Credits'), + isSupportComment ? __('Switch to Credits') : undefined, + ICONS.LBC, + () => { + setIsSupportComment(true); + setActiveTab(TAB_LBC); + } + )} {stripeEnvironment && (!isSupportComment || activeTab !== TAB_FIAT) && diff --git a/ui/component/common/form-components/form-field.jsx b/ui/component/common/form-components/form-field.jsx index 4a4a028ed..9dac1df87 100644 --- a/ui/component/common/form-components/form-field.jsx +++ b/ui/component/common/form-components/form-field.jsx @@ -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 { @@ -61,27 +65,29 @@ export class FormField extends React.PureComponent { render() { const { - label, - prefix, - postfix, - error, - helper, - name, - type, - children, - stretch, affixClass, autoFocus, - inputButton, - labelOnLeft, blockWrap, charCount, - textAreaMaxLength, - quickActionLabel, + children, + error, + helper, + hideSuggestions, + inputButton, + isLivestream, + label, + labelOnLeft, + name, noEmojis, - render, - quickActionHandler, + postfix, + prefix, + quickActionLabel, + stretch, + textAreaMaxLength, + type, openEmoteMenu, + quickActionHandler, + render, ...inputProps } = this.props; @@ -231,13 +237,26 @@ export class FormField extends React.PureComponent { {quickAction}
)} -