lbry-desktop/ui/component/claimPreviewTile/view.jsx
infinite-persistence fc7edc875b
ChannelThumbnail improvements
- [x] (6332) The IntersectionObserver method of lazy-loading loads cached images visibly late on slower devices. Previously, it was also showing the "broken image" icon briefly, which we mended by placing a dummy transparent image as the initial src.
  - Reverted that ugly transparent image fix.
  - Use the browser's built-in `loading="lazy"` instead. Sorry, Safari.

- [x] Size-optimization did not take "device pixel ratio" into account.
  - When resizing an image through the CDN, we can't just take the dimensions of the tag in pixels directly -- we need to take zooming into account, otherwise the image ends up blurry.
  - Previously, we quickly disabled optimization for the channel avatar in the Channel Page because of this. Now that we know the root-cause, the change was reverted and we now go through the CDN with appropriate sizes. This also improves our Web Vital scores.

- [x] Size-optimization wasn't really implemented for all ChannelThumbnail instances.
  - The CDN-optimized size was hardcoded to the largest instance, so small images like sidebar thumbnails are still loading images that are unnecessarily larger.
  - There's a little-bit of hardcoding of values from CSS here, but I think it's a ok compromise (not something we change often). It also doesn't need to be exact -- the "device pixel ratio" calculate will ensure it's slightly larger than what we need.

- [x] Set `width` and `height` of `<img>` to improve CLS.
  - Addresses Ligthhouse complaints, although technically the shifting was addressed at the `ClaimPreviewTile` level (sub-container dimensions are well defined).
  - Notes: the values don't need to be the final CSS-adjusted sizes. It just needs to be in the right aspect ratio to help the browser pre-allocate space to avoid shifts.

- [x] Add option to disable lazy-load Channel Thumbnails
  - The guidelines mentioned that items that are already in the viewport should not enable `loading="lazy"`.
  - We have a few areas where it doesn't make sense to lazy-load (e.g. thumbnail in Header, channel selector dropdown, publish preview, etc.).
2021-07-05 16:04:10 +08:00

249 lines
7.8 KiB
JavaScript

// @flow
import React from 'react';
import classnames from 'classnames';
import { NavLink, withRouter } from 'react-router-dom';
import FileThumbnail from 'component/fileThumbnail';
import UriIndicator from 'component/uriIndicator';
import TruncatedText from 'component/common/truncated-text';
import DateTime from 'component/dateTime';
import ChannelThumbnail from 'component/channelThumbnail';
import SubscribeButton from 'component/subscribeButton';
import useGetThumbnail from 'effects/use-get-thumbnail';
import { formatLbryUrlForWeb } from 'util/url';
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
import PreviewOverlayProperties from 'component/previewOverlayProperties';
import FileDownloadLink from 'component/fileDownloadLink';
import ClaimRepostAuthor from 'component/claimRepostAuthor';
import ClaimMenuList from 'component/claimMenuList';
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
type Props = {
uri: string,
claim: ?Claim,
resolveUri: (string) => void,
isResolvingUri: boolean,
history: { push: (string) => void },
thumbnail: string,
title: string,
placeholder: boolean,
blackListedOutpoints: Array<{
txid: string,
nout: number,
}>,
filteredOutpoints: Array<{
txid: string,
nout: number,
}>,
blockedChannelUris: Array<string>,
getFile: (string) => void,
placeholder: boolean,
streamingUrl: string,
isMature: boolean,
showMature: boolean,
showHiddenByUser?: boolean,
properties?: (Claim) => void,
live?: boolean,
collectionId?: string,
};
function ClaimPreviewTile(props: Props) {
const {
history,
uri,
isResolvingUri,
thumbnail,
title,
resolveUri,
claim,
placeholder,
blackListedOutpoints,
filteredOutpoints,
getFile,
streamingUrl,
blockedChannelUris,
isMature,
showMature,
showHiddenByUser,
properties,
live,
collectionId,
} = props;
const isRepost = claim && claim.repost_channel_url;
const isCollection = claim && claim.value_type === 'collection';
const isStream = claim && claim.value_type === 'stream';
const collectionClaimId = isCollection && claim && claim.claim_id;
const shouldFetch = claim === undefined;
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail;
const canonicalUrl = claim && claim.canonical_url;
let navigateUrl = formatLbryUrlForWeb(canonicalUrl || uri || '/');
const listId = collectionId || collectionClaimId;
if (listId) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
navigateUrl = navigateUrl + `?` + collectionParams.toString();
}
const navLinkProps = {
to: navigateUrl,
onClick: (e) => e.stopPropagation(),
};
let isChannel;
let isValid = false;
if (uri) {
try {
({ isChannel } = parseURI(uri));
isValid = true;
} catch (e) {
isValid = false;
}
}
const signingChannel = claim && claim.signing_channel;
const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url;
function handleClick(e) {
if (navigateUrl) {
history.push(navigateUrl);
}
}
React.useEffect(() => {
if (isValid && !isResolvingUri && shouldFetch && uri) {
resolveUri(uri);
}
}, [isValid, isResolvingUri, uri, resolveUri, shouldFetch]);
let shouldHide = false;
if (isMature && !showMature) {
// Unfortunately needed until this is resolved
// https://github.com/lbryio/lbry-sdk/issues/2785
shouldHide = true;
}
// This will be replaced once blocking is done at the wallet server level
if (claim && !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 && !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 && !showHiddenByUser && 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 && isChannel && !shouldHide && !showHiddenByUser && blockedChannelUris.length) {
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url);
}
if (shouldHide) {
return null;
}
if (placeholder || (!claim && isResolvingUri)) {
return (
<li className={classnames('claim-preview--tile', {})}>
<div className="placeholder media__thumb" />
<div className="placeholder__wrapper">
<div className="placeholder claim-tile__title" />
<div className="placeholder claim-tile__info" />
</div>
</li>
);
}
let liveProperty = null;
if (live === true) {
liveProperty = (claim) => <>LIVE</>;
}
return (
<li
role="link"
onClick={handleClick}
className={classnames('card claim-preview--tile', {
'claim-preview__wrapper--channel': isChannel,
'claim-preview__live': live,
})}
>
<NavLink {...navLinkProps}>
<FileThumbnail thumbnail={thumbnailUrl} allowGifs>
{!isChannel && (
<React.Fragment>
{/* @if TARGET='app' */}
{isStream && (
<div className="claim-preview__hover-actions">
<FileDownloadLink uri={canonicalUrl} hideOpenButton />
</div>
)}
{/* @endif */}
<div className="claim-preview__file-property-overlay">
<PreviewOverlayProperties uri={uri} properties={liveProperty || properties} />
</div>
</React.Fragment>
)}
{isCollection && (
<React.Fragment>
<div className="claim-preview__collection-wrapper">
<CollectionPreviewOverlay collectionId={listId} uri={uri} />
</div>
</React.Fragment>
)}
</FileThumbnail>
</NavLink>
<NavLink {...navLinkProps}>
<h2 className="claim-tile__title">
<TruncatedText text={title || (claim && claim.name)} lines={isChannel ? 1 : 2} />
{isChannel && (
<div className="claim-tile__about">
<UriIndicator uri={uri} />
</div>
)}
{/* CHECK CLAIM MENU LIST PARAMS (IS REPOST?) */}
<ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} isRepost={isRepost} />
</h2>
</NavLink>
<div>
<div className="claim-tile__info">
{isChannel ? (
<div className="claim-tile__about--channel">
<SubscribeButton uri={uri} />
</div>
) : (
<React.Fragment>
<UriIndicator uri={uri} link hideAnonymous>
<ChannelThumbnail uri={channelUri} xsmall />
</UriIndicator>
<div className="claim-tile__about">
<UriIndicator uri={uri} link />
<DateTime timeAgo uri={uri} />
</div>
</React.Fragment>
)}
</div>
{isRepost && (
<div className="claim-tile__repost-author">
<ClaimRepostAuthor uri={uri} />
</div>
)}
</div>
</li>
);
}
export default withRouter(ClaimPreviewTile);