2575c5d448
* Add ordering Icons * Refactor doCollectionEdit - It required claims as parameter, when only uris are used to populate the collection, so that was changed to pass down the uris instead. - There were unused and mostly unnecessary functions inside, for example the parameter claimIds was never used so it would never enter the claimSearch function which again would be used to generate uris, so it's better to just use uris as parameter * Add List Reordering changes * Add toggle button for list editing * Add toggle on content page collection sidebar * Enable drag-n-drop to re-order list items https://www.youtube.com/watch?v=aYZRRyukuIw * Allow removing all unavailable claims from a List * Fix <g> on icons * Fix section buttons positioning * Move preventDefault and stopPropagation to buttons div instead of each button, preventing clicking even if disabled opening the claim * Change dragging cursor * Fix sizing * Fix dragging component * Restrict dragging to vertical axis * Ignore shuffle state for ordering * Fix console errors * Mobile fixes * Fix sidebar spacing * Fix grey on mobile after click
289 lines
9.6 KiB
JavaScript
289 lines
9.6 KiB
JavaScript
// @flow
|
|
|
|
// $FlowFixMe
|
|
import { Draggable } from 'react-beautiful-dnd';
|
|
|
|
import { MAIN_CLASS } from 'constants/classnames';
|
|
import type { Node } from 'react';
|
|
import React, { useEffect } from 'react';
|
|
import classnames from 'classnames';
|
|
import ClaimPreview from 'component/claimPreview';
|
|
import Spinner from 'component/spinner';
|
|
import { FormField } from 'component/common/form';
|
|
import usePersistedState from 'effects/use-persisted-state';
|
|
import debounce from 'util/debounce';
|
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
|
|
|
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
|
const SORT_NEW = 'new';
|
|
const SORT_OLD = 'old';
|
|
|
|
type Props = {
|
|
uris: Array<string>,
|
|
prefixUris?: Array<string>,
|
|
header: Node | boolean,
|
|
headerAltControls: Node,
|
|
loading: boolean,
|
|
type: string,
|
|
activeUri?: string,
|
|
empty?: string,
|
|
defaultSort?: boolean,
|
|
onScrollBottom?: (any) => void,
|
|
page?: number,
|
|
pageSize?: number,
|
|
// If using the default header, this is a unique ID needed to persist the state of the filter setting
|
|
persistedStorageKey?: string,
|
|
showHiddenByUser: boolean,
|
|
showUnresolvedClaims?: boolean,
|
|
renderActions?: (Claim) => ?Node,
|
|
renderProperties?: (Claim) => ?Node,
|
|
includeSupportAction?: boolean,
|
|
injectedItem: ?Node,
|
|
timedOutMessage?: Node,
|
|
tileLayout?: boolean,
|
|
searchInLanguage: boolean,
|
|
hideMenu?: boolean,
|
|
claimSearchByQuery: { [string]: Array<string> },
|
|
claimsByUri: { [string]: any },
|
|
collectionId?: string,
|
|
showNoSourceClaims?: boolean,
|
|
onClick?: (e: any, claim?: ?Claim, index?: number) => void,
|
|
maxClaimRender?: number,
|
|
excludeUris?: Array<string>,
|
|
loadedCallback?: (number) => void,
|
|
swipeLayout: boolean,
|
|
showEdit?: boolean,
|
|
droppableProvided?: any,
|
|
unavailableUris?: Array<string>,
|
|
};
|
|
|
|
export default function ClaimList(props: Props) {
|
|
const {
|
|
activeUri,
|
|
uris,
|
|
prefixUris,
|
|
headerAltControls,
|
|
loading,
|
|
persistedStorageKey,
|
|
empty,
|
|
defaultSort,
|
|
type,
|
|
header,
|
|
onScrollBottom,
|
|
pageSize,
|
|
page,
|
|
showHiddenByUser,
|
|
showUnresolvedClaims,
|
|
includeSupportAction,
|
|
injectedItem,
|
|
timedOutMessage,
|
|
tileLayout = false,
|
|
renderActions,
|
|
renderProperties,
|
|
searchInLanguage,
|
|
hideMenu,
|
|
collectionId,
|
|
showNoSourceClaims,
|
|
onClick,
|
|
maxClaimRender,
|
|
excludeUris = [],
|
|
loadedCallback,
|
|
swipeLayout = false,
|
|
showEdit,
|
|
droppableProvided,
|
|
unavailableUris,
|
|
} = props;
|
|
|
|
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
|
|
|
// Exclude prefix uris in these results variables. We don't want to show
|
|
// anything if the search failed or timed out.
|
|
const timedOut = uris === null;
|
|
const urisLength = (uris && uris.length) || 0;
|
|
|
|
let tileUris = (prefixUris || []).concat(uris || []);
|
|
tileUris = tileUris.filter((uri) => !excludeUris.includes(uri));
|
|
|
|
const totalLength = tileUris.length;
|
|
|
|
if (maxClaimRender) tileUris = tileUris.slice(0, maxClaimRender);
|
|
|
|
let sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? tileUris : tileUris.slice().reverse())) || [];
|
|
|
|
React.useEffect(() => {
|
|
if (typeof loadedCallback === 'function') loadedCallback(totalLength);
|
|
}, [totalLength]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const noResultMsg = searchInLanguage
|
|
? __('No results. Contents may be hidden by the Language filter.')
|
|
: __('No results');
|
|
|
|
function handleSortChange() {
|
|
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
|
}
|
|
|
|
const handleClaimClicked = React.useCallback(
|
|
(e, claim, index) => {
|
|
if (onClick) {
|
|
onClick(e, claim, index);
|
|
}
|
|
},
|
|
[onClick]
|
|
);
|
|
|
|
const customShouldHide = React.useCallback((claim: StreamClaim) => {
|
|
// Hack to hide spee.ch thumbnail publishes
|
|
// If it meets these requirements, it was probably uploaded here:
|
|
// https://github.com/lbryio/lbry-redux/blob/master/src/redux/actions/publish.js#L74-L79
|
|
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const handleScroll = debounce((e) => {
|
|
if (page && pageSize && onScrollBottom) {
|
|
const mainEl = document.querySelector(`.${MAIN_CLASS}`);
|
|
if (mainEl && !loading && urisLength >= pageSize) {
|
|
const ROUGH_TILE_HEIGHT_PX = 200;
|
|
const mainBoundingRect = mainEl.getBoundingClientRect();
|
|
const contentWrapperAtBottomOfPage = mainBoundingRect.bottom - ROUGH_TILE_HEIGHT_PX <= window.innerHeight;
|
|
if (contentWrapperAtBottomOfPage) {
|
|
onScrollBottom();
|
|
}
|
|
}
|
|
}
|
|
}, DEBOUNCE_SCROLL_HANDLER_MS);
|
|
|
|
if (onScrollBottom) {
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}
|
|
}, [loading, onScrollBottom, urisLength, pageSize, page]);
|
|
|
|
const getClaimPreview = (uri: string, index: number, draggableProvided?: any) => (
|
|
<ClaimPreview
|
|
uri={uri}
|
|
indexInContainer={index}
|
|
type={type}
|
|
active={activeUri && uri === activeUri}
|
|
hideMenu={hideMenu}
|
|
includeSupportAction={includeSupportAction}
|
|
showUnresolvedClaim={showUnresolvedClaims}
|
|
properties={renderProperties || (type !== 'small' ? undefined : false)}
|
|
renderActions={renderActions}
|
|
showUserBlocked={showHiddenByUser}
|
|
showHiddenByUser={showHiddenByUser}
|
|
collectionId={collectionId}
|
|
showNoSourceClaims={showNoSourceClaims}
|
|
customShouldHide={customShouldHide}
|
|
onClick={handleClaimClicked}
|
|
swipeLayout={swipeLayout}
|
|
showEdit={showEdit}
|
|
dragHandleProps={draggableProvided && draggableProvided.dragHandleProps}
|
|
unavailableUris={unavailableUris}
|
|
/>
|
|
);
|
|
|
|
return tileLayout && !header ? (
|
|
<section className={classnames('claim-grid', { 'swipe-list': swipeLayout })}>
|
|
{urisLength > 0 &&
|
|
tileUris.map((uri) => (
|
|
<ClaimPreviewTile
|
|
key={uri}
|
|
uri={uri}
|
|
showHiddenByUser={showHiddenByUser}
|
|
properties={renderProperties}
|
|
collectionId={collectionId}
|
|
showNoSourceClaims={showNoSourceClaims}
|
|
swipeLayout={swipeLayout}
|
|
/>
|
|
))}
|
|
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
|
{timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
|
|
</section>
|
|
) : (
|
|
<section
|
|
className={classnames('claim-list', {
|
|
'claim-list--small': type === 'small',
|
|
})}
|
|
>
|
|
{header !== false && (
|
|
<React.Fragment>
|
|
{header && (
|
|
<div className={classnames('claim-list__header', { 'section__title--small': type === 'small' })}>
|
|
{header}
|
|
{loading && <Spinner type="small" />}
|
|
{(headerAltControls || defaultSort) && (
|
|
<div className="claim-list__alt-controls">
|
|
{headerAltControls}
|
|
{defaultSort && (
|
|
<FormField
|
|
className="claim-list__dropdown"
|
|
type="select"
|
|
name="file_sort"
|
|
value={currentSort}
|
|
onChange={handleSortChange}
|
|
>
|
|
<option value={SORT_NEW}>{__('Newest First')}</option>
|
|
<option value={SORT_OLD}>{__('Oldest First')}</option>
|
|
</FormField>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</React.Fragment>
|
|
)}
|
|
|
|
{urisLength > 0 && (
|
|
<ul
|
|
className={classnames('ul--no-style', {
|
|
card: !(tileLayout || swipeLayout || type === 'small'),
|
|
'claim-list--card-body': tileLayout,
|
|
'swipe-list': swipeLayout,
|
|
})}
|
|
{...(droppableProvided && droppableProvided.droppableProps)}
|
|
ref={droppableProvided && droppableProvided.innerRef}
|
|
>
|
|
{injectedItem && sortedUris.some((uri, index) => index === 4) && <li>{injectedItem}</li>}
|
|
|
|
{sortedUris.map((uri, index) =>
|
|
droppableProvided ? (
|
|
<Draggable key={uri} draggableId={uri} index={index}>
|
|
{(draggableProvided, draggableSnapshot) => {
|
|
// Restrict dragging to vertical axis
|
|
// https://github.com/atlassian/react-beautiful-dnd/issues/958#issuecomment-980548919
|
|
let transform = draggableProvided.draggableProps.style.transform;
|
|
|
|
if (draggableSnapshot.isDragging && transform) {
|
|
transform = transform.replace(/\(.+,/, '(0,');
|
|
}
|
|
|
|
const style = {
|
|
...draggableProvided.draggableProps.style,
|
|
transform,
|
|
};
|
|
|
|
return (
|
|
<li ref={draggableProvided.innerRef} {...draggableProvided.draggableProps} style={style}>
|
|
{/* https://github.com/atlassian/react-beautiful-dnd/issues/1756 */}
|
|
<div style={{ display: 'none' }} {...draggableProvided.dragHandleProps} />
|
|
|
|
{getClaimPreview(uri, index, draggableProvided)}
|
|
</li>
|
|
);
|
|
}}
|
|
</Draggable>
|
|
) : (
|
|
getClaimPreview(uri, index)
|
|
)
|
|
)}
|
|
|
|
{droppableProvided && droppableProvided.placeholder}
|
|
</ul>
|
|
)}
|
|
|
|
{!timedOut && urisLength === 0 && !loading && <div className="empty empty--centered">{empty || noResultMsg}</div>}
|
|
{!loading && timedOut && timedOutMessage && <div className="empty empty--centered">{timedOutMessage}</div>}
|
|
</section>
|
|
);
|
|
}
|