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,
pinnedUrls?: Array<string>,
pinnedClaimIds?: Array<string>, // takes precedence over pinnedUrls
hideByDefault?: boolean,
options?: {
channelIds?: Array<string>,
excludedChannelIds?: Array<string>,

View file

@ -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--"
}

View file

@ -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<string> = savedActiveOrder.filter((x) => sectionKeys.includes(x));
let hiddenOrder: Array<string> = 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;
}
// ****************************************************************************

View file

@ -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<SideNavLink> = [

View file

@ -19,5 +19,3 @@ export function getHomepageLanguage(code) {
}
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 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,
};

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
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 (
<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 && (
<Card
title={__('Customize Homepage')}
@ -79,9 +99,14 @@ export default function ModalCustomizeHomepage(props: Props) {
{hasMembership && (
<Card
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={
<div className="section__actions">
<div className="modal-customize-homepage__actions section__actions">
<Button button="primary" label={__('Save')} onClick={handleSave} />
<Button button="link" label={__('Cancel')} onClick={doHideModal} />
</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
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<string>, hidden: ?Array<string> };
type Props = {
@ -86,34 +81,7 @@ function HomePage(props: Props) {
showNsfw
);
let sortedRowData: Array<RowDataItem> = [];
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<RowDataItem> = getSortedRowData(authenticated, hasMembership, homepageOrder, rowData);
type SectionHeaderProps = {
title: string,

View file

@ -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<string>,
searchLanguages?: Array<string>,
mixIn?: Array<string>,
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));
}