lbry-desktop/ui/component/recommendedPersonal/view.jsx
infinite-persistence a96db41782 FYP: show placeholder tiles when fetching membership or fyp itself.
This prevents the "no membership" and "no recommendations" message from flashing during load.
2022-03-23 23:21:46 -04:00

177 lines
5.6 KiB
JavaScript

// @flow
import React from 'react';
import Button from 'component/button';
import ClaimList from 'component/claimList';
import ClaimPreviewTile from 'component/claimPreviewTile';
import I18nMessage from 'component/i18nMessage';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import { useIsLargeScreen, useIsMediumScreen } from 'effects/use-screensize';
// TODO: recsysFyp will be moved into 'RecSys', so the redux import in a jsx
// violation is just temporary.
import { recsysFyp } from 'redux/actions/search';
// ****************************************************************************
// RecommendedPersonal
// ****************************************************************************
const VIEW = { ALL_VISIBLE: 0, COLLAPSED: 1, EXPANDED: 2 };
function getSuitablePageSizeForScreen(defaultSize, isLargeScreen, isMediumScreen) {
return isMediumScreen ? 6 : isLargeScreen ? Math.ceil(defaultSize * (3 / 2)) : defaultSize;
}
type Props = {
header: React$Node,
onLoad: (displayed: boolean) => void,
// --- redux ---
userId: ?string,
personalRecommendations: { gid: string, uris: Array<string>, fetched: boolean },
hasMembership: ?boolean,
doFetchPersonalRecommendations: () => void,
};
export default function RecommendedPersonal(props: Props) {
const { header, onLoad, userId, personalRecommendations, hasMembership, doFetchPersonalRecommendations } = props;
const ref = React.useRef();
const [markedGid, setMarkedGid] = React.useState('');
const [view, setView] = React.useState(VIEW.ALL_VISIBLE);
const isLargeScreen = useIsLargeScreen();
const isMediumScreen = useIsMediumScreen();
const count = personalRecommendations.uris.length;
const countCollapsed = getSuitablePageSizeForScreen(8, isLargeScreen, isMediumScreen);
const finalCount = view === VIEW.ALL_VISIBLE ? count : view === VIEW.COLLAPSED ? countCollapsed : count;
// **************************************************************************
// Effects
// **************************************************************************
React.useEffect(() => {
// -- Update parent's callback request
if (typeof onLoad === 'function') {
onLoad(count > 0);
}
}, [count, onLoad]);
React.useEffect(() => {
// -- Resolve the view state:
let newView;
if (count <= countCollapsed) {
newView = VIEW.ALL_VISIBLE;
} else {
if (view === VIEW.ALL_VISIBLE) {
newView = VIEW.COLLAPSED;
}
}
if (newView && newView !== view) {
setView(newView);
}
}, [count, countCollapsed, view, setView]);
React.useEffect(() => {
// -- Mark recommendations when rendered:
if (userId && markedGid !== personalRecommendations.gid) {
setMarkedGid(personalRecommendations.gid);
recsysFyp.markPersonalRecommendations(userId, personalRecommendations.gid);
}
}, [userId, markedGid, personalRecommendations.gid]);
React.useEffect(() => {
// -- Fetch FYP
if (hasMembership) {
doFetchPersonalRecommendations();
}
}, [hasMembership, doFetchPersonalRecommendations]);
// **************************************************************************
// **************************************************************************
if (hasMembership === undefined || !personalRecommendations.fetched) {
return (
<>
{header}
<ul className="claim-grid">
{new Array(countCollapsed).fill(1).map((x, i) => (
<ClaimPreviewTile key={i} placeholder />
))}
</ul>
<div className="livestream-list--view-more" style={{ visibility: 'hidden' }}>
<Button
label='"View More" dummy to reduce layout shift'
button="link"
className="claim-grid__title--secondary"
/>
</div>
</>
);
}
if (!hasMembership) {
return (
<div>
{header}
<div className="empty empty--centered-tight">
<I18nMessage
tokens={{ learn_more: <Button button="link" navigate={`/$/${PAGES.FYP}`} label={__('learn more')} /> }}
>
Premium membership required. Become a member, or %learn_more%.
</I18nMessage>
</div>
</div>
);
}
if (count < 1) {
return (
<div>
{header}
<div className="empty empty--centered-tight">
<I18nMessage
tokens={{ learn_more: <Button button="link" navigate={`/$/${PAGES.FYP}`} label={__('Learn More')} /> }}
>
No recommendations available at the moment. %learn_more%
</I18nMessage>
</div>
</div>
);
}
return (
<div ref={ref}>
{header}
<ClaimList
tileLayout
uris={personalRecommendations.uris.slice(0, finalCount)}
fypId={personalRecommendations.gid}
/>
{view !== VIEW.ALL_VISIBLE && (
<div className="livestream-list--view-more">
<Button
label={view === VIEW.COLLAPSED ? __('Show more') : __('Show less')}
button="link"
iconRight={view === VIEW.COLLAPSED ? ICONS.DOWN : ICONS.UP}
className="claim-grid__title--secondary"
onClick={() => {
if (view === VIEW.COLLAPSED) {
setView(VIEW.EXPANDED);
} else {
setView(VIEW.COLLAPSED);
if (ref.current) {
ref.current.scrollIntoView({ block: 'start', behavior: 'smooth' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' }); // fallback, unlikely.
}
}
}}
/>
</div>
)}
</div>
);
}