diff --git a/flow-typed/homepage.js b/flow-typed/homepage.js index 211044b88..bf6daa8d2 100644 --- a/flow-typed/homepage.js +++ b/flow-typed/homepage.js @@ -20,6 +20,7 @@ declare type RowDataItem = { extra?: any, pinnedUrls?: Array, pinnedClaimIds?: Array, // takes precedence over pinnedUrls + hideByDefault?: boolean, options?: { channelIds?: Array, excludedChannelIds?: Array, diff --git a/static/app-strings.json b/static/app-strings.json index 99dd48592..5677923c5 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2275,5 +2275,7 @@ "Watch History": "Watch History", "Currently, your watch history is only saved locally.": "Currently, your watch history is only saved locally.", "Clear History": "Clear History", + "Reset homepage to defaults?": "Reset homepage to defaults?", + "Homepage restored to default.": "Homepage restored to default.", "--end--": "--end--" } diff --git a/ui/component/homepageSort/view.jsx b/ui/component/homepageSort/view.jsx index 86bce0011..90cee5145 100644 --- a/ui/component/homepageSort/view.jsx +++ b/ui/component/homepageSort/view.jsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import classnames from 'classnames'; import Icon from 'component/common/icon'; -import { HOMEPAGE_EXCLUDED_CATEGORIES } from 'constants/homepage_languages'; import * as ICONS from 'constants/icons'; import 'scss/component/homepage-sort.scss'; @@ -49,27 +48,26 @@ function getInitialList(listId, savedOrder, homepageSections) { const savedHiddenOrder = savedOrder.hidden || []; const sectionKeys = Object.keys(homepageSections); - if (listId === 'ACTIVE') { - // Start with saved order, excluding obsolete items (i.e. category removed or not available in non-English) - const finalOrder = savedActiveOrder.filter((x) => sectionKeys.includes(x)); + // From the saved entries, trim those that no longer exists in the latest (or different) Homepage. + let activeOrder: Array = savedActiveOrder.filter((x) => sectionKeys.includes(x)); + let hiddenOrder: Array = savedHiddenOrder.filter((x) => sectionKeys.includes(x)); - // Add new categories not seen previously. - sectionKeys.forEach((x) => { - if (!finalOrder.includes(x)) { - finalOrder.push(x); + // Add any new categories found into 'active' ... + sectionKeys.forEach((key: string) => { + if (!activeOrder.includes(key) && !hiddenOrder.includes(key)) { + if (homepageSections[key].hideByDefault) { + // ... unless it is a 'hideByDefault' category. + hiddenOrder.push(key); + } else { + activeOrder.push(key); } - }); + } + }); - // Exclude items that were moved to Hidden, or intentionally excluded from Homepage. - return finalOrder - .filter((x) => !savedHiddenOrder.includes(x)) - .filter((x) => !HOMEPAGE_EXCLUDED_CATEGORIES.includes(x)); - } else { - console.assert(listId === 'HIDDEN', `Unhandled listId: ${listId}`); - return savedHiddenOrder - .filter((x) => sectionKeys.includes(x)) - .filter((x) => !HOMEPAGE_EXCLUDED_CATEGORIES.includes(x)); - } + // Final check to exclude items that were previously moved to Hidden. + activeOrder = activeOrder.filter((x) => !hiddenOrder.includes(x)); + + return listId === 'ACTIVE' ? activeOrder : hiddenOrder; } // **************************************************************************** diff --git a/ui/component/sideNavigation/view.jsx b/ui/component/sideNavigation/view.jsx index 456c9e0c6..8b574df68 100644 --- a/ui/component/sideNavigation/view.jsx +++ b/ui/component/sideNavigation/view.jsx @@ -175,7 +175,7 @@ function SideNavigation(props: Props) { const isLargeScreen = useIsLargeScreen(); const EXTRA_SIDEBAR_LINKS = GetLinksData(homepageData, isLargeScreen).map( - ({ pinnedUrls, pinnedClaimIds, ...theRest }) => theRest + ({ pinnedUrls, pinnedClaimIds, hideByDefault, ...theRest }) => theRest ); const MOBILE_LINKS: Array = [ diff --git a/ui/constants/homepage_languages.js b/ui/constants/homepage_languages.js index eb7d065ac..d67f55aec 100644 --- a/ui/constants/homepage_languages.js +++ b/ui/constants/homepage_languages.js @@ -19,5 +19,3 @@ export function getHomepageLanguage(code) { } export default HOMEPAGE_LANGUAGES; - -export const HOMEPAGE_EXCLUDED_CATEGORIES = Object.freeze(['NEWS', 'WILD_WEST']); diff --git a/ui/modal/modalCustomizeHomepage/index.js b/ui/modal/modalCustomizeHomepage/index.js index 48a9e7dae..e5be74b00 100644 --- a/ui/modal/modalCustomizeHomepage/index.js +++ b/ui/modal/modalCustomizeHomepage/index.js @@ -1,7 +1,8 @@ import { connect } from 'react-redux'; import ModalCustomizeHomepage from './view'; import * as SETTINGS from 'constants/settings'; -import { doHideModal } from 'redux/actions/app'; +import { doHideModal, doOpenModal } from 'redux/actions/app'; +import { doToast } from 'redux/actions/notifications'; import { doSetClientSetting } from 'redux/actions/settings'; import { selectClientSetting } from 'redux/selectors/settings'; import { selectHasOdyseeMembership } from 'redux/selectors/user'; @@ -13,6 +14,8 @@ const select = (state) => ({ const perform = { doSetClientSetting, + doToast, + doOpenModal, doHideModal, }; diff --git a/ui/modal/modalCustomizeHomepage/style.scss b/ui/modal/modalCustomizeHomepage/style.scss new file mode 100644 index 000000000..a79d266c4 --- /dev/null +++ b/ui/modal/modalCustomizeHomepage/style.scss @@ -0,0 +1,10 @@ +.modal-customize-homepage__body { + display: flex; + flex-direction: column; + + button { + align-self: flex-end; + margin-right: var(--spacing-s); + margin-bottom: var(--spacing-s); + } +} diff --git a/ui/modal/modalCustomizeHomepage/view.jsx b/ui/modal/modalCustomizeHomepage/view.jsx index 5bee56b80..d088c727f 100644 --- a/ui/modal/modalCustomizeHomepage/view.jsx +++ b/ui/modal/modalCustomizeHomepage/view.jsx @@ -1,9 +1,11 @@ // @flow import React from 'react'; +import './style.scss'; import Button from 'component/button'; import Card from 'component/common/card'; import HomepageSort from 'component/homepageSort'; import MembershipSplash from 'component/membershipSplash'; +import * as MODALS from 'constants/modal_types'; import * as SETTINGS from 'constants/settings'; import { Modal } from 'modal/modal'; @@ -13,11 +15,13 @@ type Props = { hasMembership: ?boolean, homepageOrder: HomepageOrder, doSetClientSetting: (key: string, value: any, push: boolean) => void, + doToast: ({ message: string, isError?: boolean }) => void, + doOpenModal: (id: string, {}) => void, doHideModal: () => void, }; export default function ModalCustomizeHomepage(props: Props) { - const { hasMembership, homepageOrder, doSetClientSetting, doHideModal } = props; + const { hasMembership, homepageOrder, doSetClientSetting, doToast, doOpenModal, doHideModal } = props; const order = React.useRef(); function handleNewOrder(newOrder: HomepageOrder) { @@ -32,13 +36,12 @@ export default function ModalCustomizeHomepage(props: Props) { if (order.current) { const orderToSave: HomepageOrder = order.current; - // ** Note: the forEach() is probably masking Flow from seeing that null active/hidden is already handled. if (orderToSave.active && orderToSave.hidden) { if (homepageOrder.active) { homepageOrder.active.forEach((x) => { - // $FlowFixMe: ** + // $FlowIgnore: null case handled. if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) { - // $FlowFixMe: ** + // $FlowIgnore: null case handled. orderToSave.active.push(x); } }); @@ -46,9 +49,9 @@ export default function ModalCustomizeHomepage(props: Props) { if (homepageOrder.hidden) { homepageOrder.hidden.forEach((x) => { - // $FlowFixMe: ** + // $FlowIgnore: null case handled. if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) { - // $FlowFixMe: ** + // $FlowIgnore: null case handled. orderToSave.hidden.push(x); } }); @@ -62,8 +65,25 @@ export default function ModalCustomizeHomepage(props: Props) { doHideModal(); } + function handleReset() { + doOpenModal(MODALS.CONFIRM, { + title: __('Reset homepage to defaults?'), + subtitle: __('This action is permanent and cannot be undone'), + onConfirm: (closeModal) => { + doSetClientSetting(SETTINGS.HOMEPAGE_ORDER, { active: null, hidden: null }, true); + doToast({ message: __('Homepage restored to default.') }); + closeModal(); + }, + }); + } + return ( - + {!hasMembership && ( } + body={ +
+ +
+ } actions={ -
+
diff --git a/ui/page/home/helper.jsx b/ui/page/home/helper.jsx new file mode 100644 index 000000000..ba47c2ff3 --- /dev/null +++ b/ui/page/home/helper.jsx @@ -0,0 +1,68 @@ +// @flow +import * as ICONS from 'constants/icons'; +import * as PAGES from 'constants/pages'; + +type HomepageOrder = { active: ?Array, hidden: ?Array }; + +const FYP_SECTION: RowDataItem = { + id: 'FYP', + title: 'Recommended', + icon: ICONS.GLOBE, + link: `/$/${PAGES.FYP}`, +}; + +function pushAllValidCategories(rowData: Array, hasMembership: ?boolean) { + const x: Array = []; + + rowData.forEach((data: RowDataItem) => { + if (!data.hideByDefault) { + x.push(data); + } + + if (data.id === 'FOLLOWING' && hasMembership) { + x.push(FYP_SECTION); + } + }); + + return x; +} + +export function getSortedRowData( + authenticated: boolean, + hasMembership: ?boolean, + homepageOrder: HomepageOrder, + rowData: Array +) { + let sortedRowData: Array = []; + + if (authenticated) { + if (homepageOrder.active) { + // Grab categories that are still valid in the latest homepage: + homepageOrder.active.forEach((key) => { + const dataIndex = rowData.findIndex((data) => data.id === key); + if (dataIndex !== -1) { + sortedRowData.push(rowData[dataIndex]); + rowData.splice(dataIndex, 1); + } else if (key === 'FYP') { + // Special-case injection (not part of category definition): + sortedRowData.push(FYP_SECTION); + } + }); + + // For remaining 'rowData', display it if it's a new category: + rowData.forEach((data: RowDataItem) => { + if (!data.hideByDefault) { + if (!homepageOrder.hidden || !homepageOrder.hidden.includes(data.id)) { + sortedRowData.push(data); + } + } + }); + } else { + sortedRowData = pushAllValidCategories(rowData, hasMembership); + } + } else { + sortedRowData = pushAllValidCategories(rowData, hasMembership); + } + + return sortedRowData; +} diff --git a/ui/page/home/view.jsx b/ui/page/home/view.jsx index 908648be0..a1b9d1a41 100644 --- a/ui/page/home/view.jsx +++ b/ui/page/home/view.jsx @@ -1,9 +1,12 @@ // @flow +import React from 'react'; +import classnames from 'classnames'; + +import { getSortedRowData } from './helper'; +import { ENABLE_NO_SOURCE_CLAIMS } from 'config'; import * as ICONS from 'constants/icons'; import * as MODALS from 'constants/modal_types'; import * as PAGES from 'constants/pages'; -import { ENABLE_NO_SOURCE_CLAIMS } from 'config'; -import React from 'react'; import Page from 'component/page'; import Button from 'component/button'; import ClaimTilesDiscover from 'component/claimTilesDiscover'; @@ -17,17 +20,9 @@ import { GetLinksData } from 'util/buildHomepage'; import { getLivestreamUris } from 'util/livestream'; import ScheduledStreams from 'component/scheduledStreams'; import { splitBySeparator } from 'util/lbryURI'; -import classnames from 'classnames'; import Ads from 'web/component/ads'; import Meme from 'web/component/meme'; -const FYP_SECTION: RowDataItem = { - id: 'FYP', - title: 'Recommended', - icon: ICONS.GLOBE, - link: `/$/${PAGES.FYP}`, -}; - type HomepageOrder = { active: ?Array, hidden: ?Array }; type Props = { @@ -86,34 +81,7 @@ function HomePage(props: Props) { showNsfw ); - let sortedRowData: Array = []; - if (homepageOrder.active && authenticated) { - homepageOrder.active.forEach((key) => { - const dataIndex = rowData.findIndex((data) => data.id === key); - if (dataIndex !== -1) { - sortedRowData.push(rowData[dataIndex]); - rowData.splice(dataIndex, 1); - } else if (key === 'FYP') { - sortedRowData.push(FYP_SECTION); - } - }); - - if (homepageOrder.hidden) { - rowData.forEach((data: RowDataItem) => { - // $FlowIssue: null 'hidden' already avoided outside anonymous function. - if (!homepageOrder.hidden.includes(data.id)) { - sortedRowData.push(data); - } - }); - } - } else { - rowData.forEach((key) => { - sortedRowData.push(key); - if (key.id === 'FOLLOWING' && hasMembership) { - sortedRowData.push(FYP_SECTION); - } - }); - } + const sortedRowData: Array = getSortedRowData(authenticated, hasMembership, homepageOrder, rowData); type SectionHeaderProps = { title: string, diff --git a/ui/util/buildHomepage.js b/ui/util/buildHomepage.js index 38ba6205f..fe6276a9c 100644 --- a/ui/util/buildHomepage.js +++ b/ui/util/buildHomepage.js @@ -2,7 +2,6 @@ import * as PAGES from 'constants/pages'; import * as ICONS from 'constants/icons'; import * as CS from 'constants/claim_search'; -import { HOMEPAGE_EXCLUDED_CATEGORIES } from 'constants/homepage_languages'; import { parseURI } from 'util/lbryURI'; import moment from 'moment'; import { toCapitalCase } from 'util/string'; @@ -24,6 +23,7 @@ export type HomepageCat = { excludedChannelIds?: Array, searchLanguages?: Array, mixIn?: Array, + hideByDefault?: boolean, }; function getLimitPerChannel(size, isChannel) { @@ -94,6 +94,7 @@ export const getHomepageRowForCat = (key: string, cat: HomepageCat) => { title: cat.label, pinnedUrls: cat.pinnedUrls, pinnedClaimIds: cat.pinnedClaimIds, + hideByDefault: cat.hideByDefault, options: { claimType: cat.claimType || ['stream', 'repost'], channelIds: cat.channelIds, @@ -337,18 +338,12 @@ export function GetLinksData( // @endif // ************************************************************************** - // TODO: provide better method for exempting from homepage const entries = Object.entries(all); for (let i = 0; i < entries.length; ++i) { const key = entries[i][0]; const val = entries[i][1]; - // $FlowFixMe https://github.com/facebook/flow/issues/2221 - if (isHomepage && HOMEPAGE_EXCLUDED_CATEGORIES.includes(key)) { - continue; - } - - // $FlowFixMe https://github.com/facebook/flow/issues/2221 + // $FlowIgnore (https://github.com/facebook/flow/issues/2221) rowData.push(getHomepageRowForCat(key, val)); }