// @flow import type { Node } from 'react'; import React, { useEffect, forwardRef } from 'react'; import { NavLink, withRouter } from 'react-router-dom'; import { useImage } from 'react-image'; import classnames from 'classnames'; import { CHANNEL_THUMBNAIL_FALLBACK, STREAM_THUMBNAIL_FALLBACK } from 'config'; import { parseURI, convertToShareLink } from 'lbry-redux'; import { openCopyLinkMenu } from 'util/context-menu'; import { formatLbryUrlForWeb } from 'util/url'; import { isEmpty } from 'util/object'; import FileThumbnail from 'component/fileThumbnail'; import UriIndicator from 'component/uriIndicator'; import FileProperties from 'component/fileProperties'; import ClaimTags from 'component/claimTags'; import SubscribeButton from 'component/subscribeButton'; import ChannelThumbnail from 'component/channelThumbnail'; import BlockButton from 'component/blockButton'; import ClaimSupportButton from 'component/claimSupportButton'; import useGetThumbnail from 'effects/use-get-thumbnail'; import ClaimPreviewTitle from 'component/claimPreviewTitle'; import ClaimPreviewSubtitle from 'component/claimPreviewSubtitle'; import ClaimRepostAuthor from 'component/claimRepostAuthor'; import FileDownloadLink from 'component/fileDownloadLink'; import AbandonedChannelPreview from 'component/abandonedChannelPreview'; import PublishPending from 'component/publishPending'; import ClaimPreviewLoading from './claim-preview-loading'; import ClaimPreviewHidden from './claim-preview-no-mature'; import ClaimPreviewNoContent from './claim-preview-no-content'; type Props = { uri: string, claim: ?Claim, obscureNsfw: boolean, showUserBlocked: boolean, claimIsMine: boolean, pending?: boolean, reflectingProgress?: any, // fxme resolveUri: string => void, isResolvingUri: boolean, history: { push: string => void }, title: string, nsfw: boolean, placeholder: string, type: string, hasVisitedUri: boolean, blackListedOutpoints: Array<{ txid: string, nout: number, }>, filteredOutpoints: Array<{ txid: string, nout: number, }>, blockedChannelUris: Array, channelIsBlocked: boolean, isSubscribed: boolean, actions: boolean | Node | string | number, properties: boolean | Node | string | number | (Claim => Node), empty?: Node, onClick?: any => any, hideBlock?: boolean, streamingUrl: ?string, getFile: string => void, customShouldHide?: Claim => boolean, showUnresolvedClaim?: boolean, showNullPlaceholder?: boolean, includeSupportAction?: boolean, hideActions?: boolean, renderActions?: Claim => ?Node, wrapperElement?: string, hideRepostLabel?: boolean, repostUrl?: string, }; const ClaimPreview = forwardRef((props: Props, ref: any) => { const { // core uri, claim, isResolvingUri, // core actions getFile, resolveUri, // claim properties // is the claim consider nsfw? nsfw, claimIsMine, isSubscribed, streamingUrl, // user properties channelIsBlocked, hasVisitedUri, // component history, wrapperElement, type, placeholder, // pending reflectingProgress, pending, empty, // modifiers customShouldHide, showNullPlaceholder, // value from show mature content user setting // true if the user doesn't wanna see nsfw content obscureNsfw, showUserBlocked, showUnresolvedClaim, hideRepostLabel = false, hideActions = false, properties, onClick, hideBlock, actions, blockedChannelUris, blackListedOutpoints, filteredOutpoints, includeSupportAction, renderActions, // repostUrl, } = props; const WrapperElement = wrapperElement || 'li'; const shouldFetch = claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending); const abandoned = !isResolvingUri && !claim; const shouldHideActions = hideActions || type === 'small' || type === 'tooltip'; const canonicalUrl = claim && claim.canonical_url; let isValid = false; if (uri) { try { parseURI(uri); isValid = true; } catch (e) { isValid = false; } } const isRepost = claim && claim.repost_url; const contentUri = claim && isRepost ? claim.canonical_url || claim.permanent_url : uri; const isChannelUri = isValid ? parseURI(contentUri).isChannel : false; const signingChannel = claim && claim.signing_channel; const navigateUrl = formatLbryUrlForWeb((claim && claim.canonical_url) || uri || '/'); const navLinkProps = { to: navigateUrl, onClick: e => e.stopPropagation(), }; // do not block abandoned and nsfw claims if showUserBlocked is passed let shouldHide = placeholder !== 'loading' && !showUserBlocked && ((abandoned && !showUnresolvedClaim) || (!claimIsMine && obscureNsfw && nsfw)); // This will be replaced once blocking is done at the wallet server level if (claim && !claimIsMine && !shouldHide && blackListedOutpoints) { shouldHide = blackListedOutpoints.some( outpoint => (signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) || (outpoint.txid === claim.txid && outpoint.nout === claim.nout) ); } // We're checking to see if the stream outpoint // or signing channel outpoint is in the filter list if (claim && !claimIsMine && !shouldHide && filteredOutpoints) { shouldHide = filteredOutpoints.some( outpoint => (signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) || (outpoint.txid === claim.txid && outpoint.nout === claim.nout) ); } // block stream claims if (claim && !shouldHide && !showUserBlocked && blockedChannelUris.length && signingChannel) { shouldHide = blockedChannelUris.some(blockedUri => blockedUri === signingChannel.permanent_url); } // block channel claims if we can't control for them in claim search // e.g. fetchRecommendedSubscriptions if (claim && isChannelUri && !shouldHide && !showUserBlocked && blockedChannelUris.length) { shouldHide = blockedChannelUris.some(blockedUri => blockedUri === claim.permanent_url); } if (!shouldHide && customShouldHide && claim) { if (customShouldHide(claim)) { shouldHide = true; } } // Weird placement warning // Make sure this happens after we figure out if this claim needs to be hidden const thumbnailUrl = useGetThumbnail(contentUri, claim, streamingUrl, getFile, shouldHide); const isStream = claim && claim.value_type === 'stream'; const { src: thumbnailUrlWithFallback } = useImage({ srcList: [thumbnailUrl, isStream ? STREAM_THUMBNAIL_FALLBACK : CHANNEL_THUMBNAIL_FALLBACK], useSuspense: false, }); function handleContextMenu(e) { // @if TARGET='app' e.preventDefault(); e.stopPropagation(); if (claim) { const shareLink = convertToShareLink(claim.canonical_url || claim.permanent_url); openCopyLinkMenu(shareLink.replace(/#/g, ':'), e); } // @endif } function handleOnClick(e) { if (onClick) { onClick(e); } if (claim && !pending) { history.push(navigateUrl); } } useEffect(() => { if (isValid && !isResolvingUri && shouldFetch && uri) { resolveUri(uri); } }, [isValid, uri, isResolvingUri, shouldFetch]); if (shouldHide && !showNullPlaceholder) { return null; } if (placeholder === 'loading' || (uri && !claim && isResolvingUri)) { return ; } if (claim && showNullPlaceholder && shouldHide && nsfw && obscureNsfw) { return (