From 19fb7d7f06f9d4f22c06b5fc8de52b9cc79ca4ee Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Fri, 21 Aug 2020 11:49:13 -0400 Subject: [PATCH] =?UTF-8?q?new=20layout=20=F0=9F=95=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom/homepage.example.js | 2 +- ui/component/claimList/view.jsx | 17 +- ui/component/claimListDiscover/view.jsx | 450 +++-------------- ui/component/claimListHeader/index.js | 21 + ui/component/claimListHeader/view.jsx | 459 ++++++++++++++++++ ui/component/common/icon-custom.jsx | 7 + .../notificationHeaderButton/view.jsx | 8 +- ui/component/page/view.jsx | 14 +- ui/constants/icons.js | 1 + ui/effects/use-media.js | 34 -- ui/effects/use-screensize.js | 35 +- ui/index.jsx | 26 +- ui/page/channelNew/view.jsx | 2 +- ui/page/channels/view.jsx | 2 +- ui/page/channelsFollowing/index.js | 4 + ui/page/channelsFollowing/view.jsx | 6 +- ui/page/checkoutPage/view.jsx | 2 +- ui/page/discover/index.js | 6 +- ui/page/discover/view.jsx | 5 +- ui/page/fileListDownloaded/view.jsx | 1 - ui/page/fileListPublished/view.jsx | 2 +- ui/page/home/view.jsx | 2 +- ui/page/invited/view.jsx | 2 +- ui/page/listBlocked/view.jsx | 2 +- ui/page/passwordReset/view.jsx | 2 +- ui/page/passwordSet/view.jsx | 2 +- ui/page/signIn/view.jsx | 2 +- ui/page/signInVerify/view.jsx | 2 +- ui/page/signUp/view.jsx | 2 +- ui/page/tagsFollowing/view.jsx | 6 +- ui/page/welcome/view.jsx | 2 +- ui/redux/reducers/settings.js | 1 + ui/scss/component/_claim-list.scss | 37 +- ui/scss/component/_claim-search.scss | 2 +- ui/scss/component/_main.scss | 15 +- ui/scss/component/section.scss | 7 + ui/util/homepage.js | 22 +- 37 files changed, 728 insertions(+), 484 deletions(-) create mode 100644 ui/component/claimListHeader/index.js create mode 100644 ui/component/claimListHeader/view.jsx delete mode 100644 ui/effects/use-media.js diff --git a/custom/homepage.example.js b/custom/homepage.example.js index d81c90866..8d605e96b 100644 --- a/custom/homepage.example.js +++ b/custom/homepage.example.js @@ -70,7 +70,7 @@ type RowDataItem = { options?: {}, }; -export default function getHomePageRowData( +export default function GetHomePageRowData( authenticated: boolean, showPersonalizedChannels: boolean, showPersonalizedTags: boolean, diff --git a/ui/component/claimList/view.jsx b/ui/component/claimList/view.jsx index 0b38ee537..38bdb341c 100644 --- a/ui/component/claimList/view.jsx +++ b/ui/component/claimList/view.jsx @@ -8,6 +8,7 @@ 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'; @@ -34,7 +35,7 @@ type Props = { hideBlock: boolean, injectedItem: ?Node, timedOutMessage?: Node, - isCardBody?: boolean, + tileLayout?: boolean, }; export default function ClaimList(props: Props) { @@ -57,8 +58,9 @@ export default function ClaimList(props: Props) { hideBlock, injectedItem, timedOutMessage, - isCardBody = false, + tileLayout = false, } = props; + const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW); const timedOut = uris === null; const urisLength = (uris && uris.length) || 0; @@ -89,7 +91,11 @@ export default function ClaimList(props: Props) { } }, [loading, onScrollBottom, urisLength, pageSize, page]); - return ( + return tileLayout && !header ? ( +
+ {urisLength > 0 && uris.map(uri => )} +
+ ) : (
0 && ( )} + {!timedOut && urisLength === 0 && !loading && (
{empty || __('No results')}
)} diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index 8f29a7a2b..2c6a71868 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -1,20 +1,16 @@ // @flow import type { Node } from 'react'; -import classnames from 'classnames'; -import React, { Fragment, useEffect, useState } 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 * as CS from 'constants/claim_search'; import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux'; -import { FormField } from 'component/common/form'; import Button from 'component/button'; import moment from 'moment'; import ClaimList from 'component/claimList'; import ClaimPreview from 'component/claimPreview'; -import { toCapitalCase } from 'util/string'; import I18nMessage from 'component/i18nMessage'; -import * as ICONS from 'constants/icons'; -import Card from 'component/common/card'; +import ClaimListHeader from 'component/claimListHeader'; type Props = { uris: Array, @@ -58,6 +54,7 @@ type Props = { injectedItem: ?Node, infiniteScroll?: Boolean, feeAmount?: string, + tileLayout: boolean, }; function ClaimListDiscover(props: Props) { @@ -98,13 +95,12 @@ function ClaimListDiscover(props: Props) { injectedItem, feeAmount, uris, + tileLayout, } = props; const didNavigateForward = history.action === 'PUSH'; const { search } = location; - - const [page, setPage] = useState(1); - const [forceRefresh, setForceRefresh] = useState(); - const [expanded, setExpanded] = usePersistedState(`expanded-${location.pathname}`, false); + const [page, setPage] = React.useState(1); + const [forceRefresh, setForceRefresh] = React.useState(); 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)) || []; @@ -123,22 +119,6 @@ function ClaimListDiscover(props: Props) { const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY); const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds; const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY; - const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL); - const isFiltered = () => - Boolean( - urlParams.get(CS.FRESH_KEY) || - urlParams.get(CS.CONTENT_KEY) || - urlParams.get(CS.DURATION_KEY) || - urlParams.get(CS.TAGS_KEY) || - urlParams.get(CS.FEE_AMOUNT_KEY) - ); - - useEffect(() => { - if (history.action !== 'POP' && isFiltered()) { - setExpanded(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy; if (!orderParam) { @@ -151,11 +131,11 @@ function ClaimListDiscover(props: Props) { } } - useEffect(() => { + React.useEffect(() => { setOrderParamUser(orderParam); }, [orderParam]); - useEffect(() => { + React.useEffect(() => { // One-time update to stash the finalized 'orderParam' at entry. if (history.action !== 'POP') { setOrderParamEntry(orderParam); @@ -303,7 +283,7 @@ function ClaimListDiscover(props: Props) { const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery]; const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery]; - const [prevOptions, setPrevOptions] = useState(null); + const [prevOptions, setPrevOptions] = React.useState(null); if (!isJustScrollingToNewPage(prevOptions, options)) { // --- New search, or search options changed. @@ -385,21 +365,6 @@ function ClaimListDiscover(props: Props) { return JSON.stringify(tmpOptions) === JSON.stringify(tmpPrevOptions); } - function handleChange(change) { - const url = buildUrl(change); - setPage(1); - history.push(url); - } - - function handleAdvancedReset() { - const newUrlParams = new URLSearchParams(search); - newUrlParams.delete('claim_type'); - newUrlParams.delete('channel_ids'); - const newSearch = `?${newUrlParams.toString()}`; - - history.push(newSearch); - } - function getParamFromTags(t) { if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) { return t; @@ -408,69 +373,6 @@ function ClaimListDiscover(props: Props) { } } - function buildUrl(delta) { - const newUrlParams = new URLSearchParams(location.search); - CS.KEYS.forEach(k => { - // $FlowFixMe append() can't take null as second arg, but get() can return null - if (urlParams.get(k) !== null) newUrlParams.append(k, urlParams.get(k)); - }); - - switch (delta.key) { - case CS.ORDER_BY_KEY: - newUrlParams.set(CS.ORDER_BY_KEY, delta.value); - break; - case CS.FRESH_KEY: - if (delta.value === defaultFreshness || delta.value === CS.FRESH_DEFAULT) { - newUrlParams.delete(CS.FRESH_KEY); - } else { - newUrlParams.set(CS.FRESH_KEY, delta.value); - } - break; - case CS.CONTENT_KEY: - if (delta.value === CS.CLAIM_CHANNEL || delta.value === CS.CLAIM_REPOST) { - newUrlParams.delete(CS.DURATION_KEY); - newUrlParams.set(CS.CONTENT_KEY, delta.value); - } else if (delta.value === CS.CONTENT_ALL) { - newUrlParams.delete(CS.CONTENT_KEY); - } else { - newUrlParams.set(CS.CONTENT_KEY, delta.value); - } - break; - case CS.DURATION_KEY: - if (delta.value === CS.DURATION_ALL) { - newUrlParams.delete(CS.DURATION_KEY); - } else { - newUrlParams.set(CS.DURATION_KEY, delta.value); - } - break; - case CS.TAGS_KEY: - if (delta.value === CS.TAGS_ALL) { - if (defaultTags === CS.TAGS_ALL) { - newUrlParams.delete(CS.TAGS_KEY); - } else { - newUrlParams.set(CS.TAGS_KEY, delta.value); - } - } else if (delta.value === CS.TAGS_FOLLOWED) { - if (defaultTags === CS.TAGS_FOLLOWED) { - newUrlParams.delete(CS.TAGS_KEY); - } else { - newUrlParams.set(CS.TAGS_KEY, delta.value); // redundant but special - } - } else { - newUrlParams.set(CS.TAGS_KEY, delta.value); - } - break; - case CS.FEE_AMOUNT_KEY: - if (delta.value === CS.FEE_AMOUNT_ANY) { - newUrlParams.delete(CS.FEE_AMOUNT_KEY); - } else { - newUrlParams.set(CS.FEE_AMOUNT_KEY, delta.value); - } - break; - } - return `?${newUrlParams.toString()}`; - } - function handleScrollBottom() { if (!loading && infiniteScroll) { if (claimSearchResult && !claimSearchResultLastPageReached) { @@ -479,282 +381,84 @@ function ClaimListDiscover(props: Props) { } } - useEffect(() => { + React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); doClaimSearch(searchOptions); } }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]); - const defaultHeader = repostedClaimId ? null : ( - -
-
-
- {CS.ORDER_BY_TYPES.map(type => ( -
-
- {!hideFilter && ( -
-
- {expanded && ( - <> -
- {/* FRESHNESS FIELD */} - {orderParam === CS.ORDER_BY_TOP && ( -
- - handleChange({ - key: CS.FRESH_KEY, - value: e.target.value, - }) - } - > - {CS.FRESH_TYPES.map(time => ( - - ))} - -
- )} - - {/* CONTENT_TYPES FIELD */} - {!claimType && ( -
- - handleChange({ - key: CS.CONTENT_KEY, - value: e.target.value, - }) - } - > - {CS.CONTENT_TYPES.map(type => { - if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIdsParam)) { - return ( - - ); - } - })} - -
- )} - - {/* DURATIONS FIELD */} - {showDuration && ( -
- - handleChange({ - key: CS.DURATION_KEY, - value: e.target.value, - }) - } - > - {CS.DURATION_TYPES.map(dur => ( - - ))} - -
- )} - - {/* TAGS FIELD */} - {!tags && ( -
- - handleChange({ - key: CS.TAGS_KEY, - value: e.target.value, - }) - } - > - {[ - CS.TAGS_ALL, - CS.TAGS_FOLLOWED, - ...followed, - ...(followed.includes(tagsParam) || tagsParam === CS.TAGS_ALL || tagsParam === CS.TAGS_FOLLOWED - ? [] - : [tagsParam]), // if they unfollow while filtered, add Other - ].map(tag => ( - - ))} - -
- )} - - {/* PAID FIELD */} -
- - handleChange({ - key: CS.FEE_AMOUNT_KEY, - value: e.target.value, - }) - } - > - - - - ))} - -
- - {channelIdsInUrl && ( -
- -
- )} -
- - )} -
- - {hasMatureTags && hiddenNsfwMessage} -
+ const headerToUse = header || ( + ); return ( {headerLabel && } - {meta}} - isBodyList - body={ - <> - - {loading && - new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => )} - - } - /> + {tileLayout ? ( +
+ {!repostedClaimId && ( +
+ {headerToUse} + {meta &&
{meta}
} +
+ )} + +
+ ) : ( +
+
+ {headerToUse} + {meta &&
{meta}
} +
+ + + {loading && + new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => )} +
+ )}
); } diff --git a/ui/component/claimListHeader/index.js b/ui/component/claimListHeader/index.js new file mode 100644 index 000000000..76701d135 --- /dev/null +++ b/ui/component/claimListHeader/index.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { selectFetchingClaimSearch, SETTINGS, selectFollowedTags } from 'lbry-redux'; +import { doToggleTagFollowDesktop } from 'redux/actions/tags'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; +import { doSetClientSetting, doSyncClientSettings } from 'redux/actions/settings'; + +import ClaimListDiscover from './view'; + +const select = state => ({ + followedTags: selectFollowedTags(state), + loading: selectFetchingClaimSearch(state), + showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state), +}); + +const perform = { + doToggleTagFollowDesktop, + doSetClientSetting, + doSyncClientSettings, +}; + +export default connect(select, perform)(ClaimListDiscover); diff --git a/ui/component/claimListHeader/view.jsx b/ui/component/claimListHeader/view.jsx new file mode 100644 index 000000000..fdb82e1fc --- /dev/null +++ b/ui/component/claimListHeader/view.jsx @@ -0,0 +1,459 @@ +// @flow +import { SIMPLE_SITE } from 'config'; +import * as CS from 'constants/claim_search'; +import * as ICONS from 'constants/icons'; +import type { Node } from 'react'; +import classnames from 'classnames'; +import React from 'react'; +import usePersistedState from 'effects/use-persisted-state'; +import { useHistory } from 'react-router'; +import { SETTINGS } from 'lbry-redux'; +import { FormField } from 'component/common/form'; +import Button from 'component/button'; +import { toCapitalCase } from 'util/string'; + +type Props = { + defaultTags: string, + followedTags?: Array, + tags: string, + freshness?: string, + defaultFreshness?: string, + claimType?: Array, + streamType?: string | Array, + defaultStreamType?: string | Array, + feeAmount: string, + orderBy?: Array, + defaultOrderBy?: string, + hideFilter: boolean, + hasMatureTags: boolean, + hiddenNsfwMessage?: Node, + channelIds?: Array, + tileLayout: boolean, + doSetClientSetting: (string, boolean) => void, + setPage: number => void, + doSyncClientSettings: () => void, +}; + +function ClaimListHeader(props: Props) { + const { + defaultTags, + followedTags, + tags, + freshness, + defaultFreshness, + claimType, + streamType, + defaultStreamType, + feeAmount, + orderBy, + defaultOrderBy, + hideFilter, + hasMatureTags, + hiddenNsfwMessage, + channelIds, + tileLayout, + doSetClientSetting, + doSyncClientSettings, + setPage, + } = props; + const { action, push, location } = useHistory(); + const { search } = location; + const [expanded, setExpanded] = usePersistedState(`expanded-${location.pathname}`, false); + 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 contentTypeParam = urlParams.get(CS.CONTENT_KEY); + const streamTypeParam = + streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null; + const durationParam = urlParams.get(CS.DURATION_KEY) || null; + const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY); + const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds; + const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY; + const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL); + const isFiltered = () => + Boolean( + urlParams.get(CS.FRESH_KEY) || + urlParams.get(CS.CONTENT_KEY) || + urlParams.get(CS.DURATION_KEY) || + urlParams.get(CS.TAGS_KEY) || + urlParams.get(CS.FEE_AMOUNT_KEY) + ); + + React.useEffect(() => { + if (action !== 'POP' && isFiltered()) { + setExpanded(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy; + if (!orderParam) { + if (action === '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]); + + React.useEffect(() => { + // One-time update to stash the finalized 'orderParam' at entry. + if (action !== 'POP') { + setOrderParamEntry(orderParam); + } + }, []); + + function handleChange(change) { + const url = buildUrl(change); + setPage(1); + push(url); + } + + function handleAdvancedReset() { + const newUrlParams = new URLSearchParams(search); + newUrlParams.delete('claim_type'); + newUrlParams.delete('channel_ids'); + const newSearch = `?${newUrlParams.toString()}`; + + push(newSearch); + } + + function getParamFromTags(t) { + if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) { + return t; + } else if (Array.isArray(t)) { + return t.join(','); + } + } + + function buildUrl(delta) { + const newUrlParams = new URLSearchParams(location.search); + CS.KEYS.forEach(k => { + // $FlowFixMe append() can't take null as second arg, but get() can return null + if (urlParams.get(k) !== null) newUrlParams.append(k, urlParams.get(k)); + }); + + switch (delta.key) { + case CS.ORDER_BY_KEY: + newUrlParams.set(CS.ORDER_BY_KEY, delta.value); + break; + case CS.FRESH_KEY: + if (delta.value === defaultFreshness || delta.value === CS.FRESH_DEFAULT) { + newUrlParams.delete(CS.FRESH_KEY); + } else { + newUrlParams.set(CS.FRESH_KEY, delta.value); + } + break; + case CS.CONTENT_KEY: + if (delta.value === CS.CLAIM_CHANNEL || delta.value === CS.CLAIM_REPOST) { + newUrlParams.delete(CS.DURATION_KEY); + newUrlParams.set(CS.CONTENT_KEY, delta.value); + } else if (delta.value === CS.CONTENT_ALL) { + newUrlParams.delete(CS.CONTENT_KEY); + } else { + newUrlParams.set(CS.CONTENT_KEY, delta.value); + } + break; + case CS.DURATION_KEY: + if (delta.value === CS.DURATION_ALL) { + newUrlParams.delete(CS.DURATION_KEY); + } else { + newUrlParams.set(CS.DURATION_KEY, delta.value); + } + break; + case CS.TAGS_KEY: + if (delta.value === CS.TAGS_ALL) { + if (defaultTags === CS.TAGS_ALL) { + newUrlParams.delete(CS.TAGS_KEY); + } else { + newUrlParams.set(CS.TAGS_KEY, delta.value); + } + } else if (delta.value === CS.TAGS_FOLLOWED) { + if (defaultTags === CS.TAGS_FOLLOWED) { + newUrlParams.delete(CS.TAGS_KEY); + } else { + newUrlParams.set(CS.TAGS_KEY, delta.value); // redundant but special + } + } else { + newUrlParams.set(CS.TAGS_KEY, delta.value); + } + break; + case CS.FEE_AMOUNT_KEY: + if (delta.value === CS.FEE_AMOUNT_ANY) { + newUrlParams.delete(CS.FEE_AMOUNT_KEY); + } else { + newUrlParams.set(CS.FEE_AMOUNT_KEY, delta.value); + } + break; + } + return `?${newUrlParams.toString()}`; + } + + return ( + <> +
+
+
+ {CS.ORDER_BY_TYPES.map(type => ( +
+ +
+ {!hideFilter && !SIMPLE_SITE && ( +
+
+ {expanded && !SIMPLE_SITE && ( + <> +
+ {/* FRESHNESS FIELD */} + {orderParam === CS.ORDER_BY_TOP && ( +
+ + handleChange({ + key: CS.FRESH_KEY, + value: e.target.value, + }) + } + > + {CS.FRESH_TYPES.map(time => ( + + ))} + +
+ )} + + {/* CONTENT_TYPES FIELD */} + {!claimType && ( +
+ + handleChange({ + key: CS.CONTENT_KEY, + value: e.target.value, + }) + } + > + {CS.CONTENT_TYPES.map(type => { + if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIdsParam)) { + return ( + + ); + } + })} + +
+ )} + + {/* DURATIONS FIELD */} + {showDuration && ( +
+ + handleChange({ + key: CS.DURATION_KEY, + value: e.target.value, + }) + } + > + {CS.DURATION_TYPES.map(dur => ( + + ))} + +
+ )} + + {/* TAGS FIELD */} + {!tags && ( +
+ + handleChange({ + key: CS.TAGS_KEY, + value: e.target.value, + }) + } + > + {[ + CS.TAGS_ALL, + CS.TAGS_FOLLOWED, + ...followed, + ...(followed.includes(tagsParam) || tagsParam === CS.TAGS_ALL || tagsParam === CS.TAGS_FOLLOWED + ? [] + : [tagsParam]), // if they unfollow while filtered, add Other + ].map(tag => ( + + ))} + +
+ )} + + {/* PAID FIELD */} +
+ + handleChange({ + key: CS.FEE_AMOUNT_KEY, + value: e.target.value, + }) + } + > + + + + ))} + +
+ + {channelIdsInUrl && ( +
+ +
+ )} +
+ + )} +
+ + {hasMatureTags && hiddenNsfwMessage} + + ); +} + +export default ClaimListHeader; diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx index 8ed450411..e158b4e0f 100644 --- a/ui/component/common/icon-custom.jsx +++ b/ui/component/common/icon-custom.jsx @@ -714,4 +714,11 @@ export const icons = { ), + [ICONS.LAYOUT]: buildIcon( + + + + + + ), }; diff --git a/ui/component/notificationHeaderButton/view.jsx b/ui/component/notificationHeaderButton/view.jsx index 0174105ac..cd320a31d 100644 --- a/ui/component/notificationHeaderButton/view.jsx +++ b/ui/component/notificationHeaderButton/view.jsx @@ -14,7 +14,13 @@ type Props = { }; export default function NotificationHeaderButton(props: Props) { - const { unreadCount, doReadNotifications, user } = props; + const { + unreadCount, + // notifications, + // fetching, + doReadNotifications, + user, + } = props; const notificationsEnabled = user && user.experimental_ui; const { push } = useHistory(); diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx index aa414f009..f8db99c32 100644 --- a/ui/component/page/view.jsx +++ b/ui/component/page/view.jsx @@ -21,10 +21,11 @@ type Props = { isUpgradeAvailable: boolean, authPage: boolean, filePage: boolean, + homePage: boolean, noHeader: boolean, noFooter: boolean, noSideNavigation: boolean, - fullWidth: boolean, + fullWidthPage: boolean, backout: { backLabel?: string, backNavDefault?: string, @@ -37,12 +38,12 @@ function Page(props: Props) { const { children, className, - authPage = false, filePage = false, + authPage = false, + fullWidthPage = false, noHeader = false, noFooter = false, noSideNavigation = false, - backout, } = props; const { @@ -51,6 +52,7 @@ function Page(props: Props) { const [sidebarOpen, setSidebarOpen] = usePersistedState('sidebar', true); const isMediumScreen = useIsMediumScreen(); const isMobile = useIsMobile(); + let isOnFilePage = false; try { const url = pathname.slice(1).replace(/:/g, '#'); @@ -89,7 +91,11 @@ function Page(props: Props) { /> )}
{children}
diff --git a/ui/constants/icons.js b/ui/constants/icons.js index 2d7331e5f..aa5fc1c7a 100644 --- a/ui/constants/icons.js +++ b/ui/constants/icons.js @@ -112,3 +112,4 @@ export const OPEN_LOG = 'FilePlus'; export const OPEN_LOG_FOLDER = 'Folder'; export const LBRY_STATUS = 'BarChart'; export const NOTIFICATION = 'Bell'; +export const LAYOUT = 'Layout'; diff --git a/ui/effects/use-media.js b/ui/effects/use-media.js deleted file mode 100644 index 3bc5f672e..000000000 --- a/ui/effects/use-media.js +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect } from 'react'; - -// https://usehooks.com/useMedia/ -export default function useMedia(queries, values, defaultValue) { - // Array containing a media query list for each query - const mediaQueryLists = queries.map(q => window.matchMedia(q)); - - // Function that gets value based on matching media query - const getValue = () => { - // Get index of first media query that matches - const index = mediaQueryLists.findIndex(mql => mql.matches); - // Return related value or defaultValue if none - return typeof values[index] !== 'undefined' ? values[index] : defaultValue; - }; - - // State and setter for matched value - const [value, setValue] = useState(getValue); - - useEffect( - () => { - // Event listener callback - // Note: By defining getValue outside of useEffect we ensure that it has ... - // ... current values of hook args (as this hook callback is created once on mount). - const handler = () => setValue(getValue); - // Set a listener for each media query with above handler as callback. - mediaQueryLists.forEach(mql => mql.addListener(handler)); - // Remove listeners on cleanup - return () => mediaQueryLists.forEach(mql => mql.removeListener(handler)); - }, - [] // Empty array ensures effect is only run on mount and unmount - ); - - return value; -} diff --git a/ui/effects/use-screensize.js b/ui/effects/use-screensize.js index badba5b03..f0b315f5c 100644 --- a/ui/effects/use-screensize.js +++ b/ui/effects/use-screensize.js @@ -1,11 +1,36 @@ -import useMedia from './use-media'; +// Widths are taken from "ui/scss/init/vars.scss" +import React from 'react'; + +function useWindowSize() { + const isWindowClient = typeof window === 'object'; + const [windowSize, setWindowSize] = React.useState(isWindowClient ? window.innerWidth : undefined); + + React.useEffect(() => { + function setSize() { + setWindowSize(window.innerWidth); + } + + if (isWindowClient) { + window.addEventListener('resize', setSize); + + return () => window.removeEventListener('resize', setSize); + } + }, [isWindowClient, setWindowSize]); + + return windowSize; +} export function useIsMobile() { - const isMobile = useMedia(['(min-width: 901px)'], [false], true); - return isMobile; + const windowSize = useWindowSize(); + return windowSize < 901; } export function useIsMediumScreen() { - const isMobile = useMedia(['(min-width: 1151px)'], [false], true); - return isMobile; + const windowSize = useWindowSize(); + return windowSize < 1151; +} + +export function useIsLargeScreen() { + const windowSize = useWindowSize(); + return windowSize > 1600; } diff --git a/ui/index.jsx b/ui/index.jsx index 52e072158..f74b4f5ca 100644 --- a/ui/index.jsx +++ b/ui/index.jsx @@ -197,6 +197,18 @@ remote.getCurrentWindow().on('leave-full-screen', event => { document.webkitExitFullscreen(); }); +document.addEventListener('click', event => { + let { target } = event; + + while (target && target !== document) { + if (target.matches('a[href^="http"]') || target.matches('a[href^="mailto"]')) { + event.preventDefault(); + shell.openExternal(target.href); + return; + } + target = target.parentNode; + } +}); // @endif document.addEventListener('dragover', event => { @@ -205,20 +217,6 @@ document.addEventListener('dragover', event => { document.addEventListener('drop', event => { event.preventDefault(); }); -document.addEventListener('click', event => { - let { target } = event; - - while (target && target !== document) { - if (target.matches('a[href^="http"]') || target.matches('a[href^="mailto"]')) { - // @if TARGET='app' - event.preventDefault(); - shell.openExternal(target.href); - return; - // @endif - } - target = target.parentNode; - } -}); function AppWrapper() { // Splash screen and sdk setup not needed on web diff --git a/ui/page/channelNew/view.jsx b/ui/page/channelNew/view.jsx index 74ae3e7e8..d34ccd04a 100644 --- a/ui/page/channelNew/view.jsx +++ b/ui/page/channelNew/view.jsx @@ -12,7 +12,7 @@ type Props = { function ChannelNew(props: Props) { const { history } = props; return ( - + history.push(`/$/${PAGES.CHANNELS}`)} /> ); diff --git a/ui/page/channels/view.jsx b/ui/page/channels/view.jsx index 671861f2d..b80e22fa8 100644 --- a/ui/page/channels/view.jsx +++ b/ui/page/channels/view.jsx @@ -45,7 +45,7 @@ export default function ChannelsPage(props: Props) { } isBodyList - body={} + body={} /> )} diff --git a/ui/page/channelsFollowing/index.js b/ui/page/channelsFollowing/index.js index 1d811bee6..599cbf841 100644 --- a/ui/page/channelsFollowing/index.js +++ b/ui/page/channelsFollowing/index.js @@ -1,9 +1,13 @@ import { connect } from 'react-redux'; +import { SETTINGS } from 'lbry-redux'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; + import ChannelsFollowingPage from './view'; const select = state => ({ subscribedChannels: selectSubscriptions(state), + tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state), }); export default connect(select)(ChannelsFollowingPage); diff --git a/ui/page/channelsFollowing/view.jsx b/ui/page/channelsFollowing/view.jsx index ef16e20d6..9cdff0831 100644 --- a/ui/page/channelsFollowing/view.jsx +++ b/ui/page/channelsFollowing/view.jsx @@ -11,17 +11,19 @@ import Icon from 'component/common/icon'; type Props = { subscribedChannels: Array, + tileLayout: boolean, }; function ChannelsFollowingPage(props: Props) { - const { subscribedChannels } = props; + const { subscribedChannels, tileLayout } = props; const hasSubsribedChannels = subscribedChannels.length > 0; return !hasSubsribedChannels ? ( ) : ( - + diff --git a/ui/page/checkoutPage/view.jsx b/ui/page/checkoutPage/view.jsx index 8208091c6..f3ba15ea4 100644 --- a/ui/page/checkoutPage/view.jsx +++ b/ui/page/checkoutPage/view.jsx @@ -7,7 +7,7 @@ import CreditCards from './credit-card-logos.png'; export default function CheckoutPage() { return ( - + { @@ -15,6 +16,7 @@ const select = (state, props) => { repostedUri: repostedUri, repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null, isAuthenticated: selectUserVerifiedEmail(state), + tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state), }; }; diff --git a/ui/page/discover/view.jsx b/ui/page/discover/view.jsx index c608a02c9..5d06fc20d 100644 --- a/ui/page/discover/view.jsx +++ b/ui/page/discover/view.jsx @@ -21,6 +21,7 @@ type Props = { doToggleTagFollowDesktop: string => void, doResolveUri: string => void, isAuthenticated: boolean, + tileLayout: boolean, }; function DiscoverPage(props: Props) { @@ -32,6 +33,7 @@ function DiscoverPage(props: Props) { doToggleTagFollowDesktop, doResolveUri, isAuthenticated, + tileLayout, } = props; const buttonRef = useRef(); const isHovering = useHover(buttonRef); @@ -88,8 +90,9 @@ function DiscoverPage(props: Props) { } return ( - + null} empty={ viewMode === VIEW_PURCHASES && !query ? ( diff --git a/ui/page/fileListPublished/view.jsx b/ui/page/fileListPublished/view.jsx index 9b1e498e3..f68f91364 100644 --- a/ui/page/fileListPublished/view.jsx +++ b/ui/page/fileListPublished/view.jsx @@ -85,7 +85,7 @@ function FileListPublished(props: Props) { isBodyList body={
- + 0 ? Math.ceil(urlTotal / Number(pageSize)) : 1} />
} diff --git a/ui/page/home/view.jsx b/ui/page/home/view.jsx index 7935e381d..a910b8459 100644 --- a/ui/page/home/view.jsx +++ b/ui/page/home/view.jsx @@ -37,7 +37,7 @@ function HomePage(props: Props) { ); return ( - + {(authenticated || !IS_WEB) && !subscribedChannels.length && (

diff --git a/ui/page/invited/view.jsx b/ui/page/invited/view.jsx index 8dd8f8e50..9b4a01055 100644 --- a/ui/page/invited/view.jsx +++ b/ui/page/invited/view.jsx @@ -11,7 +11,7 @@ export default function ReferredPage(props: Props) { const { fullUri, referrer } = props; return ( - + ); diff --git a/ui/page/listBlocked/view.jsx b/ui/page/listBlocked/view.jsx index 9be6d3015..c42815bd7 100644 --- a/ui/page/listBlocked/view.jsx +++ b/ui/page/listBlocked/view.jsx @@ -17,7 +17,7 @@ function ListBlocked(props: Props) { } + body={} /> ) : (
diff --git a/ui/page/passwordReset/view.jsx b/ui/page/passwordReset/view.jsx index 3fe6f33e5..eb9715ee1 100644 --- a/ui/page/passwordReset/view.jsx +++ b/ui/page/passwordReset/view.jsx @@ -5,7 +5,7 @@ import Page from 'component/page'; export default function PasswordResetPage() { return ( - + ); diff --git a/ui/page/passwordSet/view.jsx b/ui/page/passwordSet/view.jsx index eecf71b15..f88f9ad37 100644 --- a/ui/page/passwordSet/view.jsx +++ b/ui/page/passwordSet/view.jsx @@ -5,7 +5,7 @@ import Page from 'component/page'; export default function PasswordSetPage() { return ( - + ); diff --git a/ui/page/signIn/view.jsx b/ui/page/signIn/view.jsx index ed8cf9848..87b3323e0 100644 --- a/ui/page/signIn/view.jsx +++ b/ui/page/signIn/view.jsx @@ -5,7 +5,7 @@ import Page from 'component/page'; export default function SignInPage() { return ( - + ); diff --git a/ui/page/signInVerify/view.jsx b/ui/page/signInVerify/view.jsx index 49d944d44..b36ad1293 100644 --- a/ui/page/signInVerify/view.jsx +++ b/ui/page/signInVerify/view.jsx @@ -88,7 +88,7 @@ function SignInVerifyPage(props: Props) { } return ( - +
+ ); diff --git a/ui/page/tagsFollowing/view.jsx b/ui/page/tagsFollowing/view.jsx index aa2839530..fe19b1e14 100644 --- a/ui/page/tagsFollowing/view.jsx +++ b/ui/page/tagsFollowing/view.jsx @@ -8,9 +8,9 @@ import Button from 'component/button'; import Icon from 'component/common/icon'; import * as CS from 'constants/claim_search'; -function DiscoverPage() { +function TagsFollowingPage() { return ( - + @@ -34,4 +34,4 @@ function DiscoverPage() { ); } -export default DiscoverPage; +export default TagsFollowingPage; diff --git a/ui/page/welcome/view.jsx b/ui/page/welcome/view.jsx index 7a8a96e77..c5a794a40 100644 --- a/ui/page/welcome/view.jsx +++ b/ui/page/welcome/view.jsx @@ -5,7 +5,7 @@ import Page from 'component/page'; export default function Welcome() { return ( - + ); diff --git a/ui/redux/reducers/settings.js b/ui/redux/reducers/settings.js index db2b2e4c7..b113144f3 100644 --- a/ui/redux/reducers/settings.js +++ b/ui/redux/reducers/settings.js @@ -40,6 +40,7 @@ const defaultState = { [SETTINGS.HIDE_BALANCE]: false, [SETTINGS.OS_NOTIFICATIONS_ENABLED]: true, [SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: false, + [SETTINGS.TILE_LAYOUT]: true, [SETTINGS.DARK_MODE_TIMES]: { from: { hour: '21', min: '00', formattedTime: '21:00' }, diff --git a/ui/scss/component/_claim-list.scss b/ui/scss/component/_claim-list.scss index 4a5b89197..982cca732 100644 --- a/ui/scss/component/_claim-list.scss +++ b/ui/scss/component/_claim-list.scss @@ -323,34 +323,45 @@ } .claim-preview--tile { - $width: calc((100% - var(--spacing-m) * 3) / 4); - width: $width; - @include handleClaimTileGifThumbnail($width); - margin-bottom: var(--spacing-l); margin-right: 0; margin-top: 0; margin-left: var(--spacing-m); justify-content: flex-start; - @media (min-width: $breakpoint-medium) { - &:first-child, - &:nth-child(4n + 1) { - margin-left: 0; - } + .media__thumb { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } &:hover { cursor: pointer; } - .media__thumb { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; + @media (min-width: $breakpoint-large) { + $width: calc((100% - var(--spacing-m) * 5) / 6); + width: $width; + @include handleClaimTileGifThumbnail($width); + + &:first-child, + &:nth-child(6n + 1) { + margin-left: 0; + } + } + + @media (max-width: $breakpoint-large) and (min-width: $breakpoint-medium) { + $width: calc((100% - var(--spacing-m) * 3) / 4); + width: $width; + @include handleClaimTileGifThumbnail($width); + + &:first-child, + &:nth-child(4n + 1) { + margin-left: 0; + } } @media (max-width: $breakpoint-medium) and (min-width: $breakpoint-small) { - $width: calc((100vw - var(--side-nav-width--micro) - (var(--spacing-l) * 3)) / 3); + $width: calc((100vw - var(--side-nav-width--micro) - var(--spacing-l) * 3) / 3); width: $width; @include handleClaimTileGifThumbnail($width); diff --git a/ui/scss/component/_claim-search.scss b/ui/scss/component/_claim-search.scss index fd1495a5e..74a9049dc 100644 --- a/ui/scss/component/_claim-search.scss +++ b/ui/scss/component/_claim-search.scss @@ -77,7 +77,7 @@ .claim-search__top > div { @media (max-width: $breakpoint-small) { - margin: var(--spacing-xxs) 0; + margin-bottom: var(--spacing-xxs); } } diff --git a/ui/scss/component/_main.scss b/ui/scss/component/_main.scss index cfcc9ba4a..75fa2ad6d 100644 --- a/ui/scss/component/_main.scss +++ b/ui/scss/component/_main.scss @@ -95,7 +95,18 @@ } } +.main--full-width { + @extend .main; + + @media (min-width: $breakpoint-large) { + max-width: none; + width: 100%; + padding: 0 var(--spacing-l); + } +} + .main--auth-page { + width: 100%; max-width: 70rem; margin-top: var(--spacing-main-padding); margin-left: auto; @@ -160,10 +171,6 @@ } } -.main--full-width { - width: 100%; -} - .main__sign-in, .main__sign-up { max-width: 27rem; diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss index 360139dd8..d9f246575 100644 --- a/ui/scss/component/section.scss +++ b/ui/scss/component/section.scss @@ -18,6 +18,13 @@ margin-bottom: var(--spacing-l); } +.section__header--actions { + margin-bottom: var(--spacing-m); + display: flex; + align-items: flex-start; + justify-content: space-between; +} + .section__flex { display: flex; align-items: flex-start; diff --git a/ui/util/homepage.js b/ui/util/homepage.js index 51876ba66..15216f002 100644 --- a/ui/util/homepage.js +++ b/ui/util/homepage.js @@ -4,6 +4,7 @@ import * as CS from 'constants/claim_search'; import { parseURI } from 'lbry-redux'; import moment from 'moment'; import { toCapitalCase } from 'util/string'; +import { useIsLargeScreen } from 'effects/use-screensize'; type RowDataItem = { title: string, @@ -12,7 +13,7 @@ type RowDataItem = { options?: {}, }; -export default function getHomePageRowData( +export default function GetHomePageRowData( authenticated: boolean, showPersonalizedChannels: boolean, showPersonalizedTags: boolean, @@ -20,6 +21,12 @@ export default function getHomePageRowData( followedTags: Array, showIndividualTags: boolean ) { + const isLargeScreen = useIsLargeScreen(); + + function getPageSize(originalSize) { + return isLargeScreen ? originalSize * (3 / 2) : originalSize; + } + let rowData: Array = []; const individualTagDataItems: Array = []; const YOUTUBER_CHANNEL_IDS = [ @@ -114,7 +121,7 @@ export default function getHomePageRowData( options: { claimType: ['stream'], orderBy: ['release_time'], - pageSize: 12, + pageSize: getPageSize(12), channelIds: YOUTUBER_CHANNEL_IDS, limitClaimsPerChannel: 1, releaseTime: `>${Math.floor( @@ -160,7 +167,7 @@ export default function getHomePageRowData( .startOf('week') .unix() )}`, - pageSize: subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4, + pageSize: getPageSize(subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4), channelIds: subscribedChannels.map((subscription: Subscription) => { const { channelClaimId } = parseURI(subscription.uri); return channelClaimId; @@ -172,7 +179,7 @@ export default function getHomePageRowData( title: __('Top Content from Today'), link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TOP}&${CS.FRESH_KEY}=${CS.FRESH_DAY}`, options: { - pageSize: showPersonalizedChannels || showPersonalizedTags ? 4 : 8, + pageSize: getPageSize(showPersonalizedChannels || showPersonalizedTags ? 4 : 8), orderBy: ['effective_amount'], claimType: ['stream'], limitClaimsPerChannel: 2, @@ -198,7 +205,7 @@ export default function getHomePageRowData( title: __('Trending Classics'), link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TRENDING}&${CS.FRESH_KEY}=${CS.FRESH_WEEK}`, options: { - pageSize: 4, + pageSize: getPageSize(4), claimType: ['stream'], limitClaimsPerChannel: 1, releaseTime: `<${Math.floor( @@ -222,6 +229,7 @@ export default function getHomePageRowData( title: __('Trending For Your Tags'), link: `/$/${PAGES.TAGS_FOLLOWING}`, options: { + pageSize: getPageSize(4), tags: followedTags.map(tag => tag.name), claimType: ['stream'], limitClaimsPerChannel: 2, @@ -233,7 +241,7 @@ export default function getHomePageRowData( link: `/@lbry:3f`, options: { orderBy: ['release_time'], - pageSize: 4, + pageSize: getPageSize(4), channelIds: ['3fda836a92faaceedfe398225fb9b2ee2ed1f01a'], }, }; @@ -243,7 +251,7 @@ export default function getHomePageRowData( link: `/@lbrycast:4`, options: { orderBy: ['release_time'], - pageSize: 4, + pageSize: getPageSize(4), channelIds: ['4c29f8b013adea4d5cca1861fb2161d5089613ea'], }, };