lbry-desktop/ui/component/claimListDiscover/view.jsx
infinite-persistence b0e88ff5d1
Add "Hide Repost" button in channel page (#1796)
* Re-organized per 'state->var->func->effect->return' structure.

* Add "Hide Repost" button in channel page

Ticket: 1762

## Change
For the placement of the button, putting it inside the expanded settings group feels the most natural, plus we then don't need to check whether the channel has reposts or not before displaying it (the expanded area is for stuff like this).

## Notes
The tricky part was making the code maintainable w.r.t to the global "Hide Repost" setting. Changed `forceShowReposts` to `hideRepostsOverride`, hopefully makes things more obvious.
- undefined = fallback to global setting
- true/false = use override
2022-07-05 13:20:10 -04:00

807 lines
26 KiB
JavaScript

// @flow
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
import type { Node } from 'react';
import * as CS from 'constants/claim_search';
import React from 'react';
import usePersistedState from 'effects/use-persisted-state';
import { withRouter } from 'react-router';
import { MATURE_TAGS } from 'constants/tags';
import { resolveLangForClaimSearch } from 'util/default-languages';
import { createNormalizedClaimSearchKey } from 'util/claim';
import { splitBySeparator } from 'util/lbryURI';
import Button from 'component/button';
import moment from 'moment';
import ClaimList from 'component/claimList';
import ClaimPreview from 'component/claimPreview';
import ClaimPreviewTile from 'component/claimPreviewTile';
import I18nMessage from 'component/i18nMessage';
import LangFilterIndicator from 'component/langFilterIndicator';
import ClaimListHeader from 'component/claimListHeader';
import useFetchViewCount from 'effects/use-fetch-view-count';
import useResolvePins from 'effects/use-resolve-pins';
import { useIsLargeScreen } from 'effects/use-screensize';
import useGetUserMemberships from 'effects/use-get-user-memberships';
type Props = {
uris: Array<string>,
prefixUris?: Array<string>,
pins?: { urls?: Array<string>, claimIds?: Array<string>, onlyPinForOrder?: string },
name?: string,
type: string,
pageSize?: number,
fetchViewCount?: boolean,
hideRepostsOverride?: boolean, // undefined = use SETTINGS.HIDE_REPOSTS; true/false: use this.
hasNoSource?: boolean,
hasSource?: boolean,
hideAdvancedFilter?: boolean,
hideFilters?: boolean,
includeSupportAction?: boolean,
infiniteScroll?: Boolean,
isChannel?: boolean,
personalView: boolean,
showHeader: boolean,
showHiddenByUser?: boolean,
showNoSourceClaims?: boolean,
tileLayout: boolean,
searchLanguages?: Array<string>,
ignoreSearchInLanguage?: boolean, // Negate the redux setting where it doesn't make sense.
orderBy?: Array<string>, // Trending, New, Top
defaultOrderBy?: string,
sortBy?: Array<string>, // Newest First, Oldest First
freshness?: string,
defaultFreshness?: string,
tags: string, // these are just going to be string. pass a CSV if you want multi
defaultTags: string,
claimType?: string | Array<string>,
defaultClaimType?: Array<string>,
streamType?: string | Array<string>,
defaultStreamType?: string | Array<string>,
empty?: string,
feeAmount?: string,
releaseTime?: string,
repostedClaimId?: string,
scrollAnchor?: string,
maxPages?: number,
limitClaimsPerChannel?: number,
channelIds?: Array<string>,
excludedChannelIds?: Array<string>,
claimIds?: Array<string>,
subscribedChannels: Array<Subscription>,
header?: Node,
headerLabel?: string | Node,
hiddenNsfwMessage?: Node,
injectedItem?: ListInjectedItem,
meta?: Node,
subSection?: Node, // Additional section below [Header|Meta]
renderProperties?: (Claim) => Node,
history: { action: string, push: (string) => void, replace: (string) => void },
location: { search: string, pathname: string },
// --- select ---
followedTags?: Array<Tag>,
claimSearchByQuery: { [string]: Array<string> },
claimSearchByQueryLastPageReached: { [string]: boolean },
claimsByUri: { [string]: any },
claimsById: { [string]: any },
loading: boolean,
showNsfw: boolean,
hideReposts: boolean,
languageSetting: string,
mutedUris: Array<string>,
blockedUris: Array<string>,
searchInLanguage: boolean,
// --- perform ---
doClaimSearch: ({}) => void,
doFetchViewCount: (claimIdCsv: string) => void,
doFetchUserMemberships: (claimIdCsv: string) => void,
doResolveClaimIds: (Array<string>) => Promise<any>,
doResolveUris: (Array<string>, boolean) => Promise<any>,
hideLayoutButton?: boolean,
loadedCallback?: (number) => void,
maxClaimRender?: number,
useSkeletonScreen?: boolean,
excludeUris?: Array<string>,
swipeLayout: boolean,
};
function ClaimListDiscover(props: Props) {
const {
doClaimSearch,
claimSearchByQuery,
showHeader = true,
type,
claimSearchByQueryLastPageReached,
tags,
defaultTags,
loading,
meta,
subSection,
channelIds,
excludedChannelIds,
showNsfw,
hideReposts,
fetchViewCount,
history,
location,
mutedUris,
blockedUris,
hiddenNsfwMessage,
defaultOrderBy,
sortBy,
orderBy,
headerLabel,
header,
name,
claimType,
defaultClaimType,
streamType,
defaultStreamType,
freshness,
defaultFreshness = CS.FRESH_WEEK,
renderProperties,
includeSupportAction,
repostedClaimId,
hideAdvancedFilter,
infiniteScroll = true,
followedTags,
injectedItem,
feeAmount,
uris,
prefixUris,
pins,
tileLayout,
hideFilters = false,
claimIds,
maxPages,
hideRepostsOverride,
languageSetting,
searchLanguages,
searchInLanguage,
ignoreSearchInLanguage,
limitClaimsPerChannel,
releaseTime,
scrollAnchor,
showHiddenByUser = false,
hasSource,
hasNoSource,
isChannel = false,
showNoSourceClaims,
empty,
claimsByUri,
claimsById,
doFetchViewCount,
hideLayoutButton = false,
loadedCallback,
maxClaimRender,
useSkeletonScreen = true,
excludeUris = [],
doFetchUserMemberships,
swipeLayout = false,
doResolveUris,
doResolveClaimIds,
} = props;
const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris });
const didNavigateForward = history.action === 'PUSH';
const { search } = location;
const prevUris = React.useRef();
const [page, setPage] = React.useState(1);
const [forceRefresh, setForceRefresh] = React.useState();
const isLargeScreen = useIsLargeScreen();
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
const followed = (followedTags && followedTags.map((t) => t.name)) || [];
const urlParams = new URLSearchParams(search);
const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
(tags && getParamFromTags(tags)) ||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
(defaultTags && getParamFromTags(defaultTags));
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
const sortByParam = sortBy || urlParams.get(CS.SORT_BY_KEY) || CS.SORT_BY.NEWEST.key;
const mutedAndBlockedChannelIds = Array.from(
new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1]))
);
const [hiddenBuffer, setHiddenBuffer] = React.useState([]);
const hideRepostsEffective = resolveHideReposts(hideReposts, hideRepostsOverride);
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
const searchInSelectedLang = searchInLanguage && !ignoreSearchInLanguage;
const languageParams = resolveLangForClaimSearch(languageSetting, searchInSelectedLang, searchLanguages, langParam);
let claimTypeParam = claimType || defaultClaimType || null;
let streamTypeParam = streamType || defaultStreamType || null;
const contentTypeParam = urlParams.get(CS.CONTENT_KEY);
if (contentTypeParam) {
switch (contentTypeParam) {
case CS.CLAIM_COLLECTION:
claimTypeParam = contentTypeParam;
streamTypeParam = undefined;
break;
case CS.CLAIM_REPOST:
claimTypeParam = contentTypeParam;
break;
case CS.CLAIM_CHANNEL:
claimTypeParam = CS.CLAIM_CHANNEL;
streamTypeParam = undefined;
break;
case CS.FILE_VIDEO:
case CS.FILE_AUDIO:
case CS.FILE_IMAGE:
case CS.FILE_MODEL:
case CS.FILE_BINARY:
case CS.FILE_DOCUMENT:
streamTypeParam = contentTypeParam;
break;
case CS.CONTENT_ALL:
claimTypeParam = undefined;
streamTypeParam = undefined;
break;
default:
console.log('Invalid or unhandled CONTENT_KEY:', contentTypeParam); // eslint-disable-line no-console
break;
}
}
const durationParam = urlParams.get(CS.DURATION_KEY) || null;
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
const excludedIdsParam = excludedChannelIds;
const feeAmountParam = urlParams.get('fee_amount') || feeAmount;
const originalPageSize = 12;
const dynamicPageSize = isLargeScreen ? Math.ceil((originalPageSize / 2) * 6) : Math.ceil((originalPageSize / 2) * 4);
const historyAction = history.action;
let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy || orderParamEntry;
if (!orderParam) {
if (historyAction === 'POP') {
// Reaching here means user have popped back to the page's entry point (e.g. '/$/tags' without any '?order=').
orderParam = orderParamEntry;
} else {
// This is the direct entry into the page, so we load the user's previous value.
orderParam = orderParamUser;
}
}
React.useEffect(() => {
setOrderParamUser(orderParam);
}, [orderParam, setOrderParamUser]);
React.useEffect(() => {
// One-time update to stash the finalized 'orderParam' at entry.
if (historyAction !== 'POP') {
setOrderParamEntry(orderParam);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [historyAction, setOrderParamEntry]);
let options: {
page_size: number,
page: number,
no_totals: boolean,
any_tags?: Array<string>,
any_languages?: Array<string>,
not_tags: Array<string>,
channel_ids?: Array<string>,
claim_ids?: Array<string>,
not_channel_ids?: Array<string>,
order_by: Array<string>,
release_time?: string,
claim_type?: string | Array<string>,
name?: string,
duration?: string,
reposted_claim_id?: string,
stream_types?: any,
fee_amount?: string,
has_source?: boolean,
has_no_source?: boolean,
limit_claims_per_channel?: number,
remove_duplicates?: boolean,
} = {
page_size: dynamicPageSize,
page,
name,
claim_type: claimType || ['stream', 'repost', 'channel'],
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination
// it's faster, but we will need to remove it if we start using total_pages
no_totals: true,
not_channel_ids: isChannel ? undefined : mutedAndBlockedChannelIds,
not_tags: !showNsfw ? MATURE_TAGS : [],
order_by: resolveOrderByOption(orderParam, sortByParam),
remove_duplicates: isChannel ? undefined : true,
};
if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) {
options.has_no_source = true;
} else if (hasSource || (!ENABLE_NO_SOURCE_CLAIMS && (!claimType || claimType === 'stream'))) {
options.has_source = true;
}
if (limitClaimsPerChannel) {
options.limit_claims_per_channel = limitClaimsPerChannel;
}
if (feeAmountParam && claimType !== CS.CLAIM_CHANNEL) {
options.fee_amount = feeAmountParam;
}
if (claimIds) {
options.claim_ids = claimIds;
}
if (channelIdsParam) {
options.channel_ids = channelIdsParam;
}
if (excludedIdsParam) {
options.not_channel_ids = (options.not_channel_ids || []).concat(excludedIdsParam);
}
if (tagsParam) {
if (tagsParam !== CS.TAGS_ALL && tagsParam !== '') {
if (tagsParam === CS.TAGS_FOLLOWED) {
options.any_tags = followed;
} else if (Array.isArray(tagsParam)) {
options.any_tags = tagsParam;
} else {
options.any_tags = tagsParam.split(',');
}
}
}
if (repostedClaimId) {
// SDK chokes on reposted_claim_id of null or false, needs to not be present if no value
options.reposted_claim_id = repostedClaimId;
}
// IF release time, set it, else set fallback release times using the hack below.
if (releaseTime && claimTypeParam !== CS.CLAIM_CHANNEL) {
options.release_time = releaseTime;
} else if (claimTypeParam !== CS.CLAIM_CHANNEL) {
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`;
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
// Warning - hack below
// If users are following more than 10 channels or tags, limit results to stuff less than a year old
// For more than 20, drop it down to 6 months
// This helps with timeout issues for users that are following a ton of stuff
// https://github.com/lbryio/lbry-sdk/issues/2420
if (
(options.channel_ids && options.channel_ids.length > 20) ||
(options.any_tags && options.any_tags.length > 20)
) {
options.release_time = `>${Math.floor(moment().subtract(3, CS.FRESH_MONTH).startOf('week').unix())}`;
} else if (
(options.channel_ids && options.channel_ids.length > 10) ||
(options.any_tags && options.any_tags.length > 10)
) {
options.release_time = `>${Math.floor(moment().subtract(1, CS.FRESH_YEAR).startOf('week').unix())}`;
} else {
// Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
options.release_time = `<${Math.floor(moment().startOf('minute').unix())}`;
}
}
}
if (durationParam) {
if (durationParam === CS.DURATION_SHORT) {
options.duration = '<=240';
} else if (durationParam === CS.DURATION_LONG) {
options.duration = '>=1200';
}
}
if (streamTypeParam && streamTypeParam !== CS.CONTENT_ALL && claimType !== CS.CLAIM_CHANNEL) {
options.stream_types = typeof streamTypeParam === 'string' ? [streamTypeParam] : streamTypeParam;
}
if (claimTypeParam) {
if (claimTypeParam !== CS.CONTENT_ALL) {
if (Array.isArray(claimTypeParam)) {
options.claim_type = claimTypeParam;
} else {
options.claim_type = [claimTypeParam];
}
}
}
if (languageParams) {
if (languageParams !== CS.LANGUAGES_ALL) {
options.any_languages = languageParams.split(',');
}
}
if (tagsParam) {
if (tagsParam !== CS.TAGS_ALL && tagsParam !== '') {
if (tagsParam === CS.TAGS_FOLLOWED) {
options.any_tags = followed;
} else if (Array.isArray(tagsParam)) {
options.any_tags = tagsParam;
} else {
options.any_tags = tagsParam.split(',');
}
}
}
if (hideRepostsEffective && !options.reposted_claim_id) {
if (Array.isArray(options.claim_type)) {
if (options.claim_type.length > 1) {
options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost');
}
} else {
options.claim_type = ['stream', 'channel'];
}
}
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
const searchKey = createNormalizedClaimSearchKey(options);
const claimSearchResult = claimSearchByQuery[searchKey];
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[searchKey];
const isUnfetchedClaimSearch = claimSearchResult === undefined;
// uncomment to fix an item on a page
// const fixUri = 'lbry://@corbettreport#0/lbryodysee#5';
// if (
// orderParam === CS.ORDER_BY_NEW &&
// claimSearchResult &&
// claimSearchResult.length > 2 &&
// window.location.pathname === '/$/rabbithole'
// ) {
// if (claimSearchResult.indexOf(fixUri) !== -1) {
// claimSearchResult.splice(claimSearchResult.indexOf(fixUri), 1);
// } else {
// claimSearchResult.pop();
// }
// claimSearchResult.splice(2, 0, fixUri);
// }
const [prevOptions, setPrevOptions] = React.useState(null);
if (!isJustScrollingToNewPage(prevOptions, options)) {
// --- New search, or search options changed.
setPrevOptions(options);
if (didNavigateForward) {
// --- Reset the page.
options.page = 1;
setPage(options.page);
} else if (claimSearchResult) {
// --- Update 'page' based on retrieved 'claimSearchResult'.
options.page = Math.ceil(claimSearchResult.length / dynamicPageSize);
if (options.page !== page) {
setPage(options.page);
}
}
}
const shouldPerformSearch =
!uris &&
(claimSearchResult === undefined ||
didNavigateForward ||
(!loading &&
!claimSearchResultLastPageReached &&
claimSearchResult &&
claimSearchResult.length &&
claimSearchResult.length < dynamicPageSize * options.page &&
claimSearchResult.length % dynamicPageSize === 0));
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
const optionsStringForEffect = JSON.stringify(options);
const timedOutMessage = (
<div>
<p>
<I18nMessage
tokens={{
again: (
<Button
button="link"
label={__('try again in a few seconds.')}
onClick={() => setForceRefresh(Date.now())}
/>
),
}}
>
Sorry, your request timed out. Modify your options or %again%
</I18nMessage>
</p>
<p>
<I18nMessage
tokens={{
contact_support: (
<Button button="link" label={__('contact support')} href="https://odysee.com/@OdyseeHelp:b?view=about" />
),
}}
>
If you continue to have issues, please %contact_support%.
</I18nMessage>
</p>
</div>
);
// **************************************************************************
// **************************************************************************
let finalUris;
if (uris) {
// --- direct uris
finalUris = uris;
injectPinUrls(finalUris, orderParam, pins, resolvedPinUris);
finalUris = filterExcludedUris(finalUris, excludeUris);
} else {
// --- searched uris
if (isUnfetchedClaimSearch && prevUris.current) {
finalUris = prevUris.current;
} else {
finalUris = claimSearchResult;
injectPinUrls(finalUris, orderParam, pins, resolvedPinUris);
finalUris = filterExcludedUris(finalUris, excludeUris);
prevUris.current = finalUris;
}
}
// **************************************************************************
// Helpers
// **************************************************************************
// Returns true if the change in 'options' indicate that we are simply scrolling
// down to a new page; false otherwise.
function isJustScrollingToNewPage(prevOptions, options) {
if (!prevOptions) {
// It's a new search, or we just popped back from a different view.
return false;
}
// Compare every field except for 'page' and 'release_time'.
// There might be better ways to achieve this.
let tmpPrevOptions = { ...prevOptions };
tmpPrevOptions.page = -1;
tmpPrevOptions.release_time = '';
let tmpOptions = { ...options };
tmpOptions.page = -1;
tmpOptions.release_time = '';
return JSON.stringify(tmpOptions) === JSON.stringify(tmpPrevOptions);
}
function getParamFromTags(t) {
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
return t;
} else if (Array.isArray(t)) {
return t.join(',');
}
}
function handleScrollBottom() {
if (maxPages !== undefined && page === maxPages) {
return;
}
if (!loading && infiniteScroll) {
if (claimSearchResult && !claimSearchResultLastPageReached) {
setPage(page + 1);
}
}
}
function resolveHideReposts(hideRepostSetting, hideRepostOverride) {
if (hideRepostOverride === undefined || hideRepostOverride === null) {
return hideRepostSetting;
} else {
return hideRepostOverride;
}
}
function resolveOrderByOption(orderBy: string | Array<string>, sortBy: string | Array<string>) {
let order_by;
switch (orderBy) {
case CS.ORDER_BY_TRENDING:
order_by = CS.ORDER_BY_TRENDING_VALUE;
break;
case CS.ORDER_BY_NEW:
order_by = CS.ORDER_BY_NEW_VALUE;
break;
case CS.ORDER_BY_NEW_ASC:
order_by = CS.ORDER_BY_NEW_ASC_VALUE;
break;
case CS.ORDER_BY_NAME_ASC:
order_by = CS.ORDER_BY_NAME_ASC_VALUE;
break;
default:
order_by = CS.ORDER_BY_TOP_VALUE;
}
if (orderBy === CS.ORDER_BY_NEW && sortBy === CS.SORT_BY.OLDEST.key) {
return order_by.map((x) => `${CS.SORT_BY.OLDEST.opt}${x}`);
}
return order_by;
}
function injectPinUrls(uris, order, pins, resolvedPinUris) {
if (!pins || !uris || uris.length <= 2 || (pins.onlyPinForOrder && pins.onlyPinForOrder !== order)) {
return;
}
if (resolvedPinUris) {
resolvedPinUris.forEach((pin) => {
if (uris.includes(pin)) {
uris.splice(uris.indexOf(pin), 1);
} else {
uris.pop();
}
});
uris.splice(2, 0, ...resolvedPinUris);
}
}
function filterExcludedUris(uris, excludeUris) {
if (uris && excludeUris && excludeUris.length) {
return uris.filter((uri) => !excludeUris.includes(uri));
}
return uris;
}
function onHidden(uri) {
if (hiddenBuffer.indexOf(uri) === -1) {
let newBuffer = hiddenBuffer;
newBuffer.push(uri);
setHiddenBuffer(newBuffer);
}
}
// **************************************************************************
// **************************************************************************
useFetchViewCount(fetchViewCount, finalUris, claimsByUri, doFetchViewCount);
useGetUserMemberships(true, finalUris, claimsByUri, doFetchUserMemberships);
React.useEffect(() => {
if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect);
doClaimSearch(searchOptions);
}
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]);
const headerToUse = header || (
<ClaimListHeader
channelIds={channelIds}
defaultTags={defaultTags}
tags={tags}
freshness={freshness}
defaultFreshness={defaultFreshness}
claimType={claimType}
streamType={streamType}
defaultStreamType={defaultStreamType}
feeAmount={feeAmount} // ENABLE_PAID_CONTENT_DISCOVER or something
orderBy={orderBy}
defaultOrderBy={defaultOrderBy}
hideAdvancedFilter={hideAdvancedFilter}
hasMatureTags={hasMatureTags}
hiddenNsfwMessage={hiddenNsfwMessage}
setPage={setPage}
tileLayout={tileLayout}
hideLayoutButton={hideLayoutButton}
hideFilters={hideFilters}
scrollAnchor={scrollAnchor}
/>
);
return (
<React.Fragment>
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
{tileLayout ? (
<div>
{!repostedClaimId && showHeader && (
<div className="section__header--actions">
<div className="section__actions">
{headerToUse}
{searchInSelectedLang && <LangFilterIndicator />}
</div>
{meta && <div className="section__actions--no-margin">{meta}</div>}
</div>
)}
{subSection && <div>{subSection}</div>}
<ClaimList
tileLayout
loading={loading}
uris={finalUris}
prefixUris={prefixUris}
onScrollBottom={handleScrollBottom}
page={page}
pageSize={dynamicPageSize}
timedOutMessage={timedOutMessage}
renderProperties={renderProperties}
includeSupportAction={includeSupportAction}
injectedItem={injectedItem}
showHiddenByUser={showHiddenByUser}
searchOptions={options}
showNoSourceClaims={showNoSourceClaims}
empty={empty}
maxClaimRender={maxClaimRender}
loadedCallback={loadedCallback}
swipeLayout={swipeLayout}
onHidden={onHidden}
/>
{loading && useSkeletonScreen && (
<div className="claim-grid">
{new Array(dynamicPageSize).fill(1).map((x, i) => (
<ClaimPreviewTile key={i} placeholder="loading" pulse />
))}
</div>
)}
</div>
) : (
<div>
{showHeader && (
<div className="section__header--actions">
<div className="section__actions">
{headerToUse}
{searchInSelectedLang && <LangFilterIndicator />}
</div>
{meta && <div className="section__actions--no-margin">{meta}</div>}
</div>
)}
{subSection && <div>{subSection}</div>}
<ClaimList
type={type}
loading={loading}
uris={finalUris}
prefixUris={prefixUris}
onScrollBottom={handleScrollBottom}
page={page}
pageSize={dynamicPageSize}
timedOutMessage={timedOutMessage}
renderProperties={renderProperties}
includeSupportAction={includeSupportAction}
injectedItem={injectedItem}
showHiddenByUser={showHiddenByUser}
searchOptions={options}
showNoSourceClaims={hasNoSource || showNoSourceClaims}
empty={empty}
maxClaimRender={maxClaimRender}
loadedCallback={loadedCallback}
swipeLayout={swipeLayout}
onHidden={onHidden}
/>
{loading &&
useSkeletonScreen &&
new Array(dynamicPageSize)
.fill(1)
.map((x, i) => (
<ClaimPreview
showNoSourceClaims={hasNoSource || showNoSourceClaims}
key={i}
placeholder="loading"
type={type}
/>
))}
</div>
)}
</React.Fragment>
);
}
export default withRouter(ClaimListDiscover);