Homepage: add "reset" + 'hideByDefault' categories

Requires a related commit from the homepages repo.

## Changes
- Add ability to reset the sort order.
- Make 'News' and 'Wild West' a "hideByDefault" category. It can be made visible in the homepage from the customization dialog.
This commit is contained in:
infinite-persistence 2022-05-25 21:28:18 +08:00 committed by Thomas Zarebczan
parent 786d9d0253
commit cc0ed44bf6
11 changed files with 146 additions and 78 deletions

View file

@ -20,6 +20,7 @@ declare type RowDataItem = {
extra?: any, extra?: any,
pinnedUrls?: Array<string>, pinnedUrls?: Array<string>,
pinnedClaimIds?: Array<string>, // takes precedence over pinnedUrls pinnedClaimIds?: Array<string>, // takes precedence over pinnedUrls
hideByDefault?: boolean,
options?: { options?: {
channelIds?: Array<string>, channelIds?: Array<string>,
excludedChannelIds?: Array<string>, excludedChannelIds?: Array<string>,

View file

@ -2275,5 +2275,7 @@
"Watch History": "Watch History", "Watch History": "Watch History",
"Currently, your watch history is only saved locally.": "Currently, your watch history is only saved locally.", "Currently, your watch history is only saved locally.": "Currently, your watch history is only saved locally.",
"Clear History": "Clear History", "Clear History": "Clear History",
"Reset homepage to defaults?": "Reset homepage to defaults?",
"Homepage restored to default.": "Homepage restored to default.",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -2,7 +2,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import { HOMEPAGE_EXCLUDED_CATEGORIES } from 'constants/homepage_languages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import 'scss/component/homepage-sort.scss'; import 'scss/component/homepage-sort.scss';
@ -49,27 +48,26 @@ function getInitialList(listId, savedOrder, homepageSections) {
const savedHiddenOrder = savedOrder.hidden || []; const savedHiddenOrder = savedOrder.hidden || [];
const sectionKeys = Object.keys(homepageSections); const sectionKeys = Object.keys(homepageSections);
if (listId === 'ACTIVE') { // From the saved entries, trim those that no longer exists in the latest (or different) Homepage.
// Start with saved order, excluding obsolete items (i.e. category removed or not available in non-English) let activeOrder: Array<string> = savedActiveOrder.filter((x) => sectionKeys.includes(x));
const finalOrder = savedActiveOrder.filter((x) => sectionKeys.includes(x)); let hiddenOrder: Array<string> = savedHiddenOrder.filter((x) => sectionKeys.includes(x));
// Add new categories not seen previously. // Add any new categories found into 'active' ...
sectionKeys.forEach((x) => { sectionKeys.forEach((key: string) => {
if (!finalOrder.includes(x)) { if (!activeOrder.includes(key) && !hiddenOrder.includes(key)) {
finalOrder.push(x); 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. // Final check to exclude items that were previously moved to Hidden.
return finalOrder activeOrder = activeOrder.filter((x) => !hiddenOrder.includes(x));
.filter((x) => !savedHiddenOrder.includes(x))
.filter((x) => !HOMEPAGE_EXCLUDED_CATEGORIES.includes(x)); return listId === 'ACTIVE' ? activeOrder : hiddenOrder;
} else {
console.assert(listId === 'HIDDEN', `Unhandled listId: ${listId}`);
return savedHiddenOrder
.filter((x) => sectionKeys.includes(x))
.filter((x) => !HOMEPAGE_EXCLUDED_CATEGORIES.includes(x));
}
} }
// **************************************************************************** // ****************************************************************************

View file

@ -175,7 +175,7 @@ function SideNavigation(props: Props) {
const isLargeScreen = useIsLargeScreen(); const isLargeScreen = useIsLargeScreen();
const EXTRA_SIDEBAR_LINKS = GetLinksData(homepageData, isLargeScreen).map( const EXTRA_SIDEBAR_LINKS = GetLinksData(homepageData, isLargeScreen).map(
({ pinnedUrls, pinnedClaimIds, ...theRest }) => theRest ({ pinnedUrls, pinnedClaimIds, hideByDefault, ...theRest }) => theRest
); );
const MOBILE_LINKS: Array<SideNavLink> = [ const MOBILE_LINKS: Array<SideNavLink> = [

View file

@ -19,5 +19,3 @@ export function getHomepageLanguage(code) {
} }
export default HOMEPAGE_LANGUAGES; export default HOMEPAGE_LANGUAGES;
export const HOMEPAGE_EXCLUDED_CATEGORIES = Object.freeze(['NEWS', 'WILD_WEST']);

View file

@ -1,7 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ModalCustomizeHomepage from './view'; import ModalCustomizeHomepage from './view';
import * as SETTINGS from 'constants/settings'; 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 { doSetClientSetting } from 'redux/actions/settings';
import { selectClientSetting } from 'redux/selectors/settings'; import { selectClientSetting } from 'redux/selectors/settings';
import { selectHasOdyseeMembership } from 'redux/selectors/user'; import { selectHasOdyseeMembership } from 'redux/selectors/user';
@ -13,6 +14,8 @@ const select = (state) => ({
const perform = { const perform = {
doSetClientSetting, doSetClientSetting,
doToast,
doOpenModal,
doHideModal, doHideModal,
}; };

View file

@ -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);
}
}

View file

@ -1,9 +1,11 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import './style.scss';
import Button from 'component/button'; import Button from 'component/button';
import Card from 'component/common/card'; import Card from 'component/common/card';
import HomepageSort from 'component/homepageSort'; import HomepageSort from 'component/homepageSort';
import MembershipSplash from 'component/membershipSplash'; import MembershipSplash from 'component/membershipSplash';
import * as MODALS from 'constants/modal_types';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
@ -13,11 +15,13 @@ type Props = {
hasMembership: ?boolean, hasMembership: ?boolean,
homepageOrder: HomepageOrder, homepageOrder: HomepageOrder,
doSetClientSetting: (key: string, value: any, push: boolean) => void, doSetClientSetting: (key: string, value: any, push: boolean) => void,
doToast: ({ message: string, isError?: boolean }) => void,
doOpenModal: (id: string, {}) => void,
doHideModal: () => void, doHideModal: () => void,
}; };
export default function ModalCustomizeHomepage(props: Props) { export default function ModalCustomizeHomepage(props: Props) {
const { hasMembership, homepageOrder, doSetClientSetting, doHideModal } = props; const { hasMembership, homepageOrder, doSetClientSetting, doToast, doOpenModal, doHideModal } = props;
const order = React.useRef(); const order = React.useRef();
function handleNewOrder(newOrder: HomepageOrder) { function handleNewOrder(newOrder: HomepageOrder) {
@ -32,13 +36,12 @@ export default function ModalCustomizeHomepage(props: Props) {
if (order.current) { if (order.current) {
const orderToSave: HomepageOrder = 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 (orderToSave.active && orderToSave.hidden) {
if (homepageOrder.active) { if (homepageOrder.active) {
homepageOrder.active.forEach((x) => { homepageOrder.active.forEach((x) => {
// $FlowFixMe: ** // $FlowIgnore: null case handled.
if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) { if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) {
// $FlowFixMe: ** // $FlowIgnore: null case handled.
orderToSave.active.push(x); orderToSave.active.push(x);
} }
}); });
@ -46,9 +49,9 @@ export default function ModalCustomizeHomepage(props: Props) {
if (homepageOrder.hidden) { if (homepageOrder.hidden) {
homepageOrder.hidden.forEach((x) => { homepageOrder.hidden.forEach((x) => {
// $FlowFixMe: ** // $FlowIgnore: null case handled.
if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) { if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) {
// $FlowFixMe: ** // $FlowIgnore: null case handled.
orderToSave.hidden.push(x); orderToSave.hidden.push(x);
} }
}); });
@ -62,8 +65,25 @@ export default function ModalCustomizeHomepage(props: Props) {
doHideModal(); 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 ( return (
<Modal isOpen type={hasMembership ? 'custom' : 'card'} onAborted={hasMembership ? undefined : doHideModal}> <Modal
className="modal-customize-homepage"
isOpen
type={hasMembership ? 'custom' : 'card'}
onAborted={hasMembership ? undefined : doHideModal}
>
{!hasMembership && ( {!hasMembership && (
<Card <Card
title={__('Customize Homepage')} title={__('Customize Homepage')}
@ -79,9 +99,14 @@ export default function ModalCustomizeHomepage(props: Props) {
{hasMembership && ( {hasMembership && (
<Card <Card
title={__('Customize Homepage')} title={__('Customize Homepage')}
body={<HomepageSort onUpdate={handleNewOrder} />} body={
<div className="modal-customize-homepage__body">
<HomepageSort onUpdate={handleNewOrder} />
<Button button="link" label={__('Reset')} onClick={handleReset} />
</div>
}
actions={ actions={
<div className="section__actions"> <div className="modal-customize-homepage__actions section__actions">
<Button button="primary" label={__('Save')} onClick={handleSave} /> <Button button="primary" label={__('Save')} onClick={handleSave} />
<Button button="link" label={__('Cancel')} onClick={doHideModal} /> <Button button="link" label={__('Cancel')} onClick={doHideModal} />
</div> </div>

68
ui/page/home/helper.jsx Normal file
View file

@ -0,0 +1,68 @@
// @flow
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
type HomepageOrder = { active: ?Array<string>, hidden: ?Array<string> };
const FYP_SECTION: RowDataItem = {
id: 'FYP',
title: 'Recommended',
icon: ICONS.GLOBE,
link: `/$/${PAGES.FYP}`,
};
function pushAllValidCategories(rowData: Array<RowDataItem>, hasMembership: ?boolean) {
const x: Array<RowDataItem> = [];
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<RowDataItem>
) {
let sortedRowData: Array<RowDataItem> = [];
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;
}

View file

@ -1,9 +1,12 @@
// @flow // @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 ICONS from 'constants/icons';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
import React from 'react';
import Page from 'component/page'; import Page from 'component/page';
import Button from 'component/button'; import Button from 'component/button';
import ClaimTilesDiscover from 'component/claimTilesDiscover'; import ClaimTilesDiscover from 'component/claimTilesDiscover';
@ -17,17 +20,9 @@ import { GetLinksData } from 'util/buildHomepage';
import { getLivestreamUris } from 'util/livestream'; import { getLivestreamUris } from 'util/livestream';
import ScheduledStreams from 'component/scheduledStreams'; import ScheduledStreams from 'component/scheduledStreams';
import { splitBySeparator } from 'util/lbryURI'; import { splitBySeparator } from 'util/lbryURI';
import classnames from 'classnames';
import Ads from 'web/component/ads'; import Ads from 'web/component/ads';
import Meme from 'web/component/meme'; import Meme from 'web/component/meme';
const FYP_SECTION: RowDataItem = {
id: 'FYP',
title: 'Recommended',
icon: ICONS.GLOBE,
link: `/$/${PAGES.FYP}`,
};
type HomepageOrder = { active: ?Array<string>, hidden: ?Array<string> }; type HomepageOrder = { active: ?Array<string>, hidden: ?Array<string> };
type Props = { type Props = {
@ -86,34 +81,7 @@ function HomePage(props: Props) {
showNsfw showNsfw
); );
let sortedRowData: Array<RowDataItem> = []; const sortedRowData: Array<RowDataItem> = getSortedRowData(authenticated, hasMembership, homepageOrder, rowData);
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);
}
});
}
type SectionHeaderProps = { type SectionHeaderProps = {
title: string, title: string,

View file

@ -2,7 +2,6 @@
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as CS from 'constants/claim_search'; import * as CS from 'constants/claim_search';
import { HOMEPAGE_EXCLUDED_CATEGORIES } from 'constants/homepage_languages';
import { parseURI } from 'util/lbryURI'; import { parseURI } from 'util/lbryURI';
import moment from 'moment'; import moment from 'moment';
import { toCapitalCase } from 'util/string'; import { toCapitalCase } from 'util/string';
@ -24,6 +23,7 @@ export type HomepageCat = {
excludedChannelIds?: Array<string>, excludedChannelIds?: Array<string>,
searchLanguages?: Array<string>, searchLanguages?: Array<string>,
mixIn?: Array<string>, mixIn?: Array<string>,
hideByDefault?: boolean,
}; };
function getLimitPerChannel(size, isChannel) { function getLimitPerChannel(size, isChannel) {
@ -94,6 +94,7 @@ export const getHomepageRowForCat = (key: string, cat: HomepageCat) => {
title: cat.label, title: cat.label,
pinnedUrls: cat.pinnedUrls, pinnedUrls: cat.pinnedUrls,
pinnedClaimIds: cat.pinnedClaimIds, pinnedClaimIds: cat.pinnedClaimIds,
hideByDefault: cat.hideByDefault,
options: { options: {
claimType: cat.claimType || ['stream', 'repost'], claimType: cat.claimType || ['stream', 'repost'],
channelIds: cat.channelIds, channelIds: cat.channelIds,
@ -337,18 +338,12 @@ export function GetLinksData(
// @endif // @endif
// ************************************************************************** // **************************************************************************
// TODO: provide better method for exempting from homepage
const entries = Object.entries(all); const entries = Object.entries(all);
for (let i = 0; i < entries.length; ++i) { for (let i = 0; i < entries.length; ++i) {
const key = entries[i][0]; const key = entries[i][0];
const val = entries[i][1]; const val = entries[i][1];
// $FlowFixMe https://github.com/facebook/flow/issues/2221 // $FlowIgnore (https://github.com/facebook/flow/issues/2221)
if (isHomepage && HOMEPAGE_EXCLUDED_CATEGORIES.includes(key)) {
continue;
}
// $FlowFixMe https://github.com/facebook/flow/issues/2221
rowData.push(getHomepageRowForCat(key, val)); rowData.push(getHomepageRowForCat(key, val));
} }