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:
parent
786d9d0253
commit
cc0ed44bf6
11 changed files with 146 additions and 78 deletions
1
flow-typed/homepage.js
vendored
1
flow-typed/homepage.js
vendored
|
@ -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>,
|
||||
|
|
|
@ -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--"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
|
|
|
@ -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> = [
|
||||
|
|
|
@ -19,5 +19,3 @@ export function getHomepageLanguage(code) {
|
|||
}
|
||||
|
||||
export default HOMEPAGE_LANGUAGES;
|
||||
|
||||
export const HOMEPAGE_EXCLUDED_CATEGORIES = Object.freeze(['NEWS', 'WILD_WEST']);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
10
ui/modal/modalCustomizeHomepage/style.scss
Normal file
10
ui/modal/modalCustomizeHomepage/style.scss
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
68
ui/page/home/helper.jsx
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue