Add horizontal layout (#636)

* Test out a horizontal scroll for upcoming (tile only for now)

* - add support for list layout
- add following label on home page
- clan up css and naming conventions

* Update header type + show only if scheduled streams are showing
This commit is contained in:
Dan Peterson 2022-01-06 15:13:26 -06:00 committed by zeppi
parent 618ab5e195
commit a26305d75a
12 changed files with 260 additions and 49 deletions

View file

@ -44,7 +44,10 @@ type Props = {
collectionId?: string,
showNoSourceClaims?: boolean,
onClick?: (e: any, claim?: ?Claim, index?: number) => void,
noEmpty: boolean,
maxClaimRender?: number,
excludeUris?: Array<string>,
loadedCallback?: (number) => void,
swipeLayout: boolean,
};
export default function ClaimList(props: Props) {
@ -75,7 +78,10 @@ export default function ClaimList(props: Props) {
collectionId,
showNoSourceClaims,
onClick,
noEmpty,
maxClaimRender,
excludeUris = [],
loadedCallback,
swipeLayout = false,
} = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
@ -85,8 +91,18 @@ export default function ClaimList(props: Props) {
const timedOut = uris === null;
const urisLength = (uris && uris.length) || 0;
const tileUris = (prefixUris || []).concat(uris);
const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? tileUris : tileUris.slice().reverse())) || [];
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.')
@ -96,11 +112,21 @@ export default function ClaimList(props: Props) {
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
}
function handleClaimClicked(e, claim, index) {
if (onClick) {
onClick(e, claim, index);
}
}
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) => {
@ -124,7 +150,7 @@ export default function ClaimList(props: Props) {
}, [loading, onScrollBottom, urisLength, pageSize, page]);
return tileLayout && !header ? (
<section className="claim-grid">
<section className={classnames('claim-grid', { 'swipe-list': swipeLayout })}>
{urisLength > 0 &&
tileUris.map((uri) => (
<ClaimPreviewTile
@ -134,11 +160,10 @@ export default function ClaimList(props: Props) {
properties={renderProperties}
collectionId={collectionId}
showNoSourceClaims={showNoSourceClaims}
swipeLayout={swipeLayout}
/>
))}
{!timedOut && urisLength === 0 && !loading && !noEmpty && (
<div className="empty main--empty">{empty || noResultMsg}</div>
)}
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
{timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
</section>
) : (
@ -178,8 +203,9 @@ export default function ClaimList(props: Props) {
{urisLength > 0 && (
<ul
className={classnames('ul--no-style', {
card: !(tileLayout || type === 'small'),
card: !(tileLayout || swipeLayout || type === 'small'),
'claim-list--card-body': tileLayout,
'swipe-list': swipeLayout,
})}
>
{sortedUris.map((uri, index) => (
@ -199,22 +225,16 @@ export default function ClaimList(props: Props) {
showHiddenByUser={showHiddenByUser}
collectionId={collectionId}
showNoSourceClaims={showNoSourceClaims}
customShouldHide={(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';
}}
onClick={(e, claim, index) => handleClaimClicked(e, claim, index)}
customShouldHide={customShouldHide}
onClick={handleClaimClicked}
swipeLayout={swipeLayout}
/>
</React.Fragment>
))}
</ul>
)}
{!timedOut && urisLength === 0 && !loading && !noEmpty && (
<div className="empty empty--centered">{empty || noResultMsg}</div>
)}
{!timedOut && urisLength === 0 && !loading && <div className="empty empty--centered">{empty || noResultMsg}</div>}
{!loading && timedOut && timedOutMessage && <div className="empty empty--centered">{timedOutMessage}</div>}
</section>
);

View file

@ -94,6 +94,13 @@ type Props = {
doClaimSearch: ({}) => void,
doToggleTagFollowDesktop: (string) => void,
doFetchViewCount: (claimIdCsv: string) => void,
loadedCallback?: (number) => void,
maxClaimRender?: number,
useSkeletonScreen?: boolean,
excludeUris?: Array<string>,
swipeLayout: boolean,
};
function ClaimListDiscover(props: Props) {
@ -157,6 +164,11 @@ function ClaimListDiscover(props: Props) {
empty,
claimsByUri,
doFetchViewCount,
loadedCallback,
maxClaimRender,
useSkeletonScreen = true,
excludeUris = [],
swipeLayout = false,
} = props;
const didNavigateForward = history.action === 'PUSH';
const { search } = location;
@ -493,6 +505,21 @@ function ClaimListDiscover(props: Props) {
}
function resolveOrderByOption(orderBy: string | Array<string>, sortBy: string | Array<string>) {
// let order_by; // peterson 038692cafc793616cceaf10b88909fecde07ad0b
//
// 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;
// default:
// order_by = CS.ORDER_BY_TOP_VALUE;
// }
const order_by =
orderBy === CS.ORDER_BY_TRENDING
? CS.ORDER_BY_TRENDING_VALUE
@ -569,8 +596,12 @@ function ClaimListDiscover(props: Props) {
searchOptions={options}
showNoSourceClaims={showNoSourceClaims}
empty={empty}
maxClaimRender={maxClaimRender}
excludeUris={excludeUris}
loadedCallback={loadedCallback}
swipeLayout={swipeLayout}
/>
{loading && (
{loading && useSkeletonScreen && (
<div className="claim-grid">
{new Array(dynamicPageSize).fill(1).map((x, i) => (
<ClaimPreviewTile key={i} placeholder="loading" />
@ -602,8 +633,13 @@ function ClaimListDiscover(props: Props) {
searchOptions={options}
showNoSourceClaims={hasNoSource || showNoSourceClaims}
empty={empty}
maxClaimRender={maxClaimRender}
excludeUris={excludeUris}
loadedCallback={loadedCallback}
swipeLayout={swipeLayout}
/>
{loading &&
useSkeletonScreen &&
new Array(dynamicPageSize)
.fill(1)
.map((x, i) => (

View file

@ -6,6 +6,7 @@ import { isEmpty } from 'util/object';
import classnames from 'classnames';
import { isURIValid } from 'util/lbryURI';
import * as COLLECTIONS_CONSTS from 'constants/collections';
import { isChannelClaim } from 'util/claim';
import { formatLbryUrlForWeb } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
import FileThumbnail from 'component/fileThumbnail';
@ -49,7 +50,7 @@ type Props = {
type: string,
banState: { blacklisted?: boolean, filtered?: boolean, muted?: boolean, blocked?: boolean },
hasVisitedUri: boolean,
channelIsBlocked: boolean,
blockedUris: Array<string>,
actions: boolean | Node | string | number,
properties: boolean | Node | string | number | ((Claim) => Node),
empty?: Node,
@ -77,6 +78,7 @@ type Props = {
date?: any,
indexInContainer?: number, // The index order of this component within 'containerId'.
channelSubCount?: number,
swipeLayout: boolean,
};
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -97,7 +99,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
streamingUrl,
mediaDuration,
// user properties
channelIsBlocked,
hasVisitedUri,
// component
history,
@ -136,10 +137,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
disableNavigation,
indexInContainer,
channelSubCount,
swipeLayout = false,
} = props;
const isCollection = claim && claim.value_type === 'collection';
const collectionClaimId = isCollection && claim && claim.claim_id;
const listId = collectionId || collectionClaimId || null;
const listId = collectionId || collectionClaimId;
const WrapperElement = wrapperElement || 'li';
const shouldFetch =
claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending);
@ -170,7 +172,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
claim.value.stream_type &&
// $FlowFixMe
(claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
const isChannelUri = claim ? claim.value_type === 'channel' : false;
const isChannelUri = isChannelClaim(claim, uri);
const signingChannel = claim && claim.signing_channel;
const repostedChannelUri =
claim && claim.repost_channel_url && claim.value_type === 'channel'
@ -321,6 +323,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
'claim-preview--visited': !isChannelUri && !claimIsMine && hasVisitedUri,
'claim-preview--pending': pending,
'claim-preview--collection-mine': isMyCollection && listId && type === 'listview',
'swipe-list__item': swipeLayout,
})}
>
{isMyCollection && listId && type === 'listview' && (
@ -391,8 +394,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall />
</div>
)}
{isChannelUri && !channelIsBlocked && !claimIsMine && (
{isChannelUri && !banState.muted && !claimIsMine && (
<SubscribeButton
uri={repostedChannelUri || (uri.startsWith('lbry://') ? uri : `lbry://${uri}`)}
/>

View file

@ -42,6 +42,7 @@ type Props = {
properties?: (Claim) => void,
collectionId?: string,
viewCount: string,
swipeLayout: boolean,
};
// preview image cards used in related video functionality, channel overview page and homepage
@ -66,6 +67,7 @@ function ClaimPreviewTile(props: Props) {
collectionId,
mediaDuration,
viewCount,
swipeLayout = false,
} = props;
const isRepost = claim && claim.repost_channel_url;
const isCollection = claim && claim.value_type === 'collection';
@ -168,6 +170,7 @@ function ClaimPreviewTile(props: Props) {
onClick={handleClick}
className={classnames('card claim-preview--tile', {
'claim-preview__wrapper--channel': isChannel,
'swipe-list__item claim-preview--horizontal-tile': swipeLayout,
})}
>
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>

View file

@ -8,6 +8,7 @@ import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
import { parseURI, isURIValid } from 'util/lbryURI';
import { WELCOME_VERSION } from 'config';
import { GetLinksData } from 'util/buildHomepage';
import { useIsLargeScreen } from 'effects/use-screensize';
import HomePage from 'page/home';
import BackupPage from 'page/backup';
@ -125,8 +126,8 @@ function AppRouter(props: Props) {
const urlParams = new URLSearchParams(search);
const resetScroll = urlParams.get('reset_scroll');
const hasLinkedCommentInUrl = urlParams.get(LINKED_COMMENT_QUERY_PARAM);
const dynamicRoutes = GetLinksData(homepageData).filter(
const isLargeScreen = useIsLargeScreen();
const dynamicRoutes = GetLinksData(homepageData, isLargeScreen).filter(
(potentialRoute: any) => potentialRoute && potentialRoute.route
);

View file

@ -1,7 +1,7 @@
// @flow
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import { SITE_NAME, ENABLE_NO_SOURCE_CLAIMS } from 'config';
import { SITE_NAME } from 'config';
import React from 'react';
import Page from 'component/page';
import Button from 'component/button';
@ -9,6 +9,7 @@ import ClaimTilesDiscover from 'component/claimTilesDiscover';
import ClaimPreviewTile from 'component/claimPreviewTile';
import Icon from 'component/common/icon';
import WaitUntilOnPage from 'component/common/wait-until-on-page';
import { useIsLargeScreen } from 'effects/use-screensize'; // have this?
import { GetLinksData } from 'util/buildHomepage';
type Props = {
@ -24,9 +25,11 @@ function HomePage(props: Props) {
const showPersonalizedChannels = subscribedChannels && subscribedChannels.length > 0;
const showPersonalizedTags = followedTags && followedTags.length > 0;
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
const isLargeScreen = useIsLargeScreen();
const rowData: Array<RowDataItem> = GetLinksData(
homepageData,
isLargeScreen,
true,
authenticated,
showPersonalizedChannels,
@ -37,29 +40,40 @@ function HomePage(props: Props) {
showNsfw
);
type SectionHeaderProps = {
title: string,
navigate?: string,
icon?: string,
help?: string,
};
const SectionHeader = ({ title, navigate = '/', icon = '', help }: SectionHeaderProps) => {
return (
<h1 className="claim-grid__header">
<Button navigate={navigate} button="link">
<Icon className="claim-grid__header-icon" sectionIcon icon={icon} size={20} />
<span className="claim-grid__title">{title}</span>
{help}
</Button>
</h1>
);
};
function getRowElements(title, route, link, icon, help, options, index, pinUrls) {
const tilePlaceholder = (
<ul className="claim-grid">
{new Array(options.pageSize || 8).fill(1).map((x, i) => (
<ClaimPreviewTile showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS} key={i} placeholder />
<ClaimPreviewTile key={i} placeholder />
))}
</ul>
);
const claimTiles = (
<ClaimTilesDiscover {...options} showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS} hasSource pinUrls={pinUrls} />
);
const claimTiles = <ClaimTilesDiscover {...options} hasSource pinUrls={pinUrls} />;
return (
<div key={title} className="claim-grid__wrapper">
{/* category header */}
{index !== 0 && title && typeof title === 'string' && (
<h1 className="claim-grid__header">
<Button navigate={route || link} button="link">
{icon && <Icon className="claim-grid__header-icon" sectionIcon icon={icon} size={20} />}
<span className="claim-grid__title">{title}</span>
{help}
</Button>
</h1>
<SectionHeader title={__(title)} navigate={route || link} icon={icon} help={help} />
)}
{index === 0 && <>{claimTiles}</>}
@ -69,6 +83,7 @@ function HomePage(props: Props) {
</WaitUntilOnPage>
)}
{/* view more button */}
{(route || link) && (
<Button
className="claim-grid__title--secondary"

View file

@ -66,3 +66,5 @@
@import 'component/empty';
@import 'component/stripe-card';
@import 'component/wallet-tip-send';
@import 'component/swipe-list';
@import 'component/utils';

View file

@ -541,6 +541,12 @@
}
}
.claim-preview--horizontal-tile {
&:not(:first-child) {
margin-top: 0;
}
}
.claim-tile__title {
position: relative;
padding: var(--spacing-s);
@ -551,7 +557,7 @@
font-weight: 600;
color: var(--color-text);
font-size: var(--font-small);
min-height: 2rem;
min-height: 3.2rem;
@media (min-width: $breakpoint-small) {
min-height: 2.5rem;

View file

@ -0,0 +1,13 @@
.swipe-list {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.swipe-list__item {
width: 80vw;
margin-right: var(--spacing-s);
flex-shrink: 0;
scroll-snap-align: start;
}

View file

@ -0,0 +1,92 @@
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.w-full {
width: 100%;
}
.opacity-40 {
opacity: 0.4;
}
.h-12 {
height: 3rem;
}
.mt-0 {
margin-top: 0px;
}
.mt-s {
margin-top: var(--spacing-s);
}
.mt-m {
margin-top: var(--spacing-m);
}
.mb-m {
margin-bottom: var(--spacing-m);
}
.mb-xl {
margin-bottom: var(--spacing-xl);
}
.ml-s {
margin-left: var(--spacing-s);
}
.ml-m {
margin-left: var(--spacing-m);
}
.mr-m {
margin-right: var(--spacing-m);
}
.mb-0 {
margin-bottom: 0;
}
.text-s {
font-size: var(--font-small);
}
@media (min-width: $breakpoint-small) {
.md\:items-center {
align-items: center;
}
.md\:flex-col {
flex-direction: column;
}
.md\:ml-m {
margin-left: var(--spacing-m);
}
.md\:flex-row {
flex-direction: row;
}
.md\:w-auto {
width: auto;
}
.md\:mt-0 {
margin-top: 0;
}
.md\:mb-xl {
margin-bottom: var(--spacing-xl);
}
.md\:h-12 {
height: 3rem;
}
}

View file

@ -5,7 +5,6 @@ import * as CS from 'constants/claim_search';
import { parseURI } from 'util/lbryURI';
import moment from 'moment';
import { toCapitalCase } from 'util/string';
import { useIsLargeScreen } from 'effects/use-screensize';
import { CUSTOM_HOMEPAGE } from 'config';
export type RowDataItem = {
@ -125,6 +124,7 @@ export const getHomepageRowForCat = (cat: HomepageCat) => {
export function GetLinksData(
all: any, // HomepageData type?
isLargeScreen: boolean,
isHomepage?: boolean = false,
authenticated?: boolean,
showPersonalizedChannels?: boolean,
@ -134,8 +134,6 @@ export function GetLinksData(
showIndividualTags?: boolean,
showNsfw?: boolean
) {
const isLargeScreen = useIsLargeScreen();
function getPageSize(originalSize) {
return isLargeScreen ? originalSize * (3 / 2) : originalSize;
}

View file

@ -1,5 +1,6 @@
// @flow
import { MATURE_TAGS } from 'constants/tags';
import { parseURI } from 'util/lbryURI';
const matureTagMap = MATURE_TAGS.reduce((acc, tag) => ({ ...acc, [tag]: true }), {});
@ -66,6 +67,28 @@ export function filterClaims(claims: Array<Claim>, query: ?string): Array<Claim>
return claims;
}
/**
* Determines if the claim is a channel.
*
* @param claim
* @param uri An abandoned claim will be null, so provide the `uri` as a fallback to parse.
*/
export function isChannelClaim(claim: ?Claim, uri?: string) {
// 1. parseURI can't resolve a repost's channel, so a `claim` will be needed.
// 2. parseURI is still needed to cover the case of abandoned claims.
if (claim) {
return claim.value_type === 'channel';
} else if (uri) {
try {
return Boolean(parseURI(uri).isChannel);
} catch (err) {
return false;
}
} else {
return false;
}
}
export function getChannelIdFromClaim(claim: ?Claim) {
if (claim) {
if (claim.value_type === 'channel') {