add recommended subscriptions

This commit is contained in:
Sean Yesmunt 2018-11-21 16:20:55 -05:00
parent b245028050
commit 4ecd5e1684
48 changed files with 594 additions and 189 deletions

View file

@ -44,6 +44,7 @@
"jsx-a11y/interactive-supports-focus": 0,
"jsx-a11y/click-events-have-key-events": 0,
"consistent-return": 0,
"no-prototype-builtins": 0,
"flowtype/space-after-type-colon": [ 2, "always", { "allowLineBreak": true } ]
}
}

View file

@ -6,7 +6,7 @@ import ReactModal from 'react-modal';
import throttle from 'util/throttle';
import SideBar from 'component/sideBar';
import Header from 'component/header';
import { openContextMenu } from '../../util/contextMenu';
import { openContextMenu } from '../../util/context-menu';
const TWO_POINT_FIVE_MINUTES = 1000 * 60 * 2.5;

View file

@ -1,18 +1,19 @@
// @flow
import * as React from 'react';
import type { Claim } from 'types/claim';
import React, { PureComponent } from 'react';
import { normalizeURI } from 'lbry-redux';
import ToolTip from 'component/common/tooltip';
import FileCard from 'component/fileCard';
import Button from 'component/button';
import * as icons from 'constants/icons';
import type { Claim } from 'types/claim';
import SubscribeButton from 'component/subscribeButton';
type Props = {
category: string,
names: Array<string>,
names: ?Array<string>,
categoryLink: ?string,
fetching: boolean,
channelClaims: Array<Claim>,
channelClaims: ?Array<Claim>,
fetchChannel: string => void,
obscureNsfw: boolean,
};
@ -22,9 +23,8 @@ type State = {
canScrollPrevious: boolean,
};
class CategoryList extends React.PureComponent<Props, State> {
class CategoryList extends PureComponent<Props, State> {
static defaultProps = {
names: [],
categoryLink: '',
};
@ -209,7 +209,6 @@ class CategoryList extends React.PureComponent<Props, State> {
render() {
const { category, categoryLink, names, channelClaims, obscureNsfw } = this.props;
const { canScrollNext, canScrollPrevious } = this.state;
const isCommunityTopBids = category.match(/^community/i);
const showScrollButtons = isCommunityTopBids ? !obscureNsfw : true;
@ -218,7 +217,10 @@ class CategoryList extends React.PureComponent<Props, State> {
<div className="card-row__header">
<div className="card-row__title">
{categoryLink ? (
<Button label={category} navigate="/show" navigateParams={{ uri: categoryLink }} />
<div className="card__actions card__actions--no-margin">
<Button label={category} navigate="/show" navigateParams={{ uri: categoryLink }} />
<SubscribeButton uri={`lbry://${categoryLink}`} showSnackBarOnSubscribe />
</div>
) : (
category
)}
@ -263,19 +265,33 @@ class CategoryList extends React.PureComponent<Props, State> {
}}
>
{names &&
names.length &&
names.map(name => (
<FileCard showSubscribedLogo key={name} uri={normalizeURI(name)} />
))}
{channelClaims &&
channelClaims.length &&
channelClaims.map(claim => (
<FileCard
showSubcribedLogo
key={claim.claim_id}
uri={`lbry://${claim.name}#${claim.claim_id}`}
/>
))}
channelClaims
// Only show the first 10 claims, regardless of the amount we have on a channel page
.slice(0, 10)
.map(claim => (
<FileCard
showSubcribedLogo
key={claim.claim_id}
uri={`lbry://${claim.name}#${claim.claim_id}`}
/>
))}
{/*
If there aren't any uris passed in, create an empty array and render placeholder cards
channelClaims or names are being fetched
*/}
{!channelClaims &&
!names &&
/* eslint-disable react/no-array-index-key */
new Array(10).fill(1).map((x, i) => <FileCard key={i} />)
/* eslint-enable react/no-array-index-key */
}
</div>
)}
</div>

View file

@ -76,7 +76,7 @@ class ChannelTile extends React.PureComponent<Props> {
)}
{subscriptionUri && (
<div className="card__actions">
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
<SubscribeButton uri={subscriptionUri} />
</div>
)}
</div>

View file

@ -1,7 +1,7 @@
// @flow
import React from 'react';
import classnames from 'classnames';
import { formatCredits, formatFullPrice } from 'util/formatCredits';
import { formatCredits, formatFullPrice } from 'util/format-credits';
type Props = {
amount: number,

View file

@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path';
import React from 'react';
import Button from 'component/button';
import parseData from 'util/parseData';
import parseData from 'util/parse-data';
import * as icons from 'constants/icons';
import { remote } from 'electron';
@ -33,7 +33,10 @@ class FileExporter extends React.PureComponent<Props> {
fs.writeFile(filename, data, err => {
if (err) throw err;
// Do something after creation
onFileCreated && onFileCreated(filename);
if (onFileCreated) {
onFileCreated(filename);
}
});
}
@ -55,24 +58,22 @@ class FileExporter extends React.PureComponent<Props> {
],
};
remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
options,
filename => {
// User hit cancel so do nothing:
if (!filename) return;
// Get extension and remove initial dot
const format = path.extname(filename).replace(/\./g, '');
// Parse data to string with the chosen format
const parsed = parseData(data, format, filters);
// Write file
parsed && this.handleFileCreation(filename, parsed);
remote.dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
// User hit cancel so do nothing:
if (!filename) return;
// Get extension and remove initial dot
const format = path.extname(filename).replace(/\./g, '');
// Parse data to string with the chosen format
const parsed = parseData(data, format, filters);
// Write file
if (parsed) {
this.handleFileCreation(filename, parsed);
}
);
});
}
render() {
const { title, label } = this.props;
const { label } = this.props;
return (
<Button
button="primary"

View file

@ -6,7 +6,7 @@ import MarkdownPreview from 'component/common/markdown-preview';
import SimpleMDE from 'react-simplemde-editor';
import 'simplemde/dist/simplemde.min.css'; // eslint-disable-line import/no-extraneous-dependencies
import Toggle from 'react-toggle';
import { openEditorMenu, stopContextMenu } from 'util/contextMenu';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
type Props = {
name: string,

View file

@ -9,7 +9,7 @@ import UriIndicator from 'component/uriIndicator';
import * as icons from 'constants/icons';
import classnames from 'classnames';
import FilePrice from 'component/filePrice';
import { openCopyLinkMenu } from 'util/contextMenu';
import { openCopyLinkMenu } from 'util/context-menu';
import DateTime from 'component/dateTime';
type Props = {

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { selectBalance, selectIsBackDisabled, selectIsForwardDisabled } from 'lbry-redux';
import { formatCredits } from 'util/formatCredits';
import { formatCredits } from 'util/format-credits';
import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
import { doDownloadUpgradeRequested } from 'redux/actions/app';

View file

@ -8,7 +8,7 @@ import {
import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
import { doDownloadUpgrade } from 'redux/actions/app';
import { selectIsUpgradeAvailable, selectNavLinks } from 'redux/selectors/app';
import { formatCredits } from 'util/formatCredits';
import { formatCredits } from 'util/format-credits';
import Page from './view';
const select = state => ({

View file

@ -1,12 +1,18 @@
import { connect } from 'react-redux';
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
import { doOpenModal } from 'redux/actions/app';
import { selectSubscriptions, makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import {
selectSubscriptions,
makeSelectIsSubscribed,
selectFirstRunCompleted,
} from 'redux/selectors/subscriptions';
import { doToast } from 'lbry-redux';
import SubscribeButton from './view';
const select = (state, props) => ({
subscriptions: selectSubscriptions(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
firstRunCompleted: selectFirstRunCompleted(state),
});
export default connect(
@ -15,5 +21,6 @@ export default connect(
doChannelSubscribe,
doChannelUnsubscribe,
doOpenModal,
doToast,
}
)(SubscribeButton);

View file

@ -2,6 +2,7 @@
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import React from 'react';
import { parseURI } from 'lbry-redux';
import Button from 'component/button';
type SubscribtionArgs = {
@ -10,30 +11,36 @@ type SubscribtionArgs = {
};
type Props = {
channelName: ?string,
uri: ?string,
uri: string,
isSubscribed: boolean,
subscriptions: Array<string>,
doChannelSubscribe: ({ channelName: string, uri: string }) => void,
doChannelUnsubscribe: SubscribtionArgs => void,
doOpenModal: ({ id: string }) => void,
firstRunCompleted: boolean,
showSnackBarOnSubscribe: boolean,
doToast: ({ message: string }) => void,
};
export default (props: Props) => {
const {
channelName,
uri,
doChannelSubscribe,
doChannelUnsubscribe,
doOpenModal,
subscriptions,
isSubscribed,
firstRunCompleted,
showSnackBarOnSubscribe,
doToast,
} = props;
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
return channelName && uri ? (
const { claimName } = parseURI(uri);
return (
<Button
iconColor="red"
icon={isSubscribed ? undefined : ICONS.HEART}
@ -42,14 +49,19 @@ export default (props: Props) => {
onClick={e => {
e.stopPropagation();
if (!subscriptions.length) {
if (!subscriptions.length && !firstRunCompleted) {
doOpenModal(MODALS.FIRST_SUBSCRIPTION);
}
subscriptionHandler({
channelName,
channelName: claimName,
uri,
});
if (showSnackBarOnSubscribe) {
doToast({ message: `${__('Successfully subscribed to')} ${claimName}!` });
}
}}
/>
) : null;
);
};

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { selectSuggestedChannels } from 'redux/selectors/subscriptions';
import SuggestedSubscriptions from './view';
const select = state => ({
suggested: selectSuggestedChannels(state),
});
export default connect(
select,
null
)(SuggestedSubscriptions);

View file

@ -0,0 +1,23 @@
// @flow
import React, { PureComponent } from 'react';
import CategoryList from 'component/categoryList';
type Props = {
suggested: Array<{ label: string, uri: string }>,
};
class SuggestedSubscriptions extends PureComponent<Props> {
render() {
const { suggested } = this.props;
return suggested ? (
<div className="card__content subscriptions__suggested">
{suggested.map(({ uri, label }) => (
<CategoryList key={uri} category={label} categoryLink={uri} />
))}
</div>
) : null;
}
}
export default SuggestedSubscriptions;

View file

@ -2,7 +2,7 @@
import * as React from 'react';
import CodeMirror from 'codemirror/lib/codemirror';
import { openSnippetMenu, stopContextMenu } from 'util/contextMenu';
import { openSnippetMenu, stopContextMenu } from 'util/context-menu';
// Addons
import 'codemirror/addon/selection/mark-selection';

View file

@ -1,6 +1,6 @@
// @flow
import React from 'react';
import { stopContextMenu } from 'util/contextMenu';
import { stopContextMenu } from 'util/context-menu';
type Props = {
source: string,

View file

@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import { stopContextMenu } from 'util/contextMenu';
import { stopContextMenu } from 'util/context-menu';
type Props = {
source: string,

View file

@ -3,7 +3,7 @@ import React from 'react';
import classnames from 'classnames';
import { normalizeURI, SEARCH_TYPES, isURIValid } from 'lbry-redux';
import Icon from 'component/common/icon';
import { parseQueryParams } from 'util/query_params';
import { parseQueryParams } from 'util/query-params';
import * as icons from 'constants/icons';
import Autocomplete from './internal/autocomplete';
@ -20,6 +20,7 @@ type Props = {
doBlur: () => void,
resultCount: number,
focused: boolean,
doShowSnackBar: ({}) => void,
};
class WunderBar extends React.PureComponent<Props> {

View file

@ -189,6 +189,11 @@ export const FETCH_SUBSCRIPTIONS_START = 'FETCH_SUBSCRIPTIONS_START';
export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
export const SET_VIEW_MODE = 'SET_VIEW_MODE';
export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START';
export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL';
export const SUBSCRIPTION_FIRST_RUN_COMPLETED = 'SUBSCRIPTION_FIRST_RUN_COMPLETED';
export const VIEW_SUGGESTED_SUBSCRIPTIONS = 'VIEW_SUGGESTED_SUBSCRIPTIONS';
// Publishing
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';

View file

@ -5,3 +5,8 @@ export const VIEW_LATEST_FIRST = 'view_latest_first';
export const DOWNLOADING = 'DOWNLOADING';
export const DOWNLOADED = 'DOWNLOADED';
export const NOTIFY_ONLY = 'NOTIFY_ONLY;';
// Suggested types
export const SUGGESTED_TOP_BID = 'top_bid';
export const SUGGESTED_TOP_SUBSCRIBED = 'top_subscribed';
export const SUGGESTED_FEATURED = 'featured';

View file

@ -1,11 +1,9 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doNavigate } from 'redux/actions/navigation';
import ModalFirstSubscription from './view';
const perform = dispatch => () => ({
closeModal: () => dispatch(doHideModal()),
navigate: path => dispatch(doNavigate(path)),
});
export default connect(

View file

@ -5,37 +5,24 @@ import Button from 'component/button';
type Props = {
closeModal: () => void,
navigate: string => void,
};
const ModalFirstSubscription = (props: Props) => {
const { closeModal, navigate } = props;
const { closeModal } = props;
return (
<Modal type="custom" isOpen contentLabel="Subscriptions 101" title={__('Subscriptions 101')}>
<section className="card__content">
<p>{__('You just subscribed to your first channel. Awesome!')}</p>
<p>{__('A few quick things to know:')}</p>
<p className="card__content">
{__('1) You can use the')}{' '}
<Button
button="link"
label={__('Subscriptions Page')}
onClick={() => {
navigate('/subscriptions');
closeModal();
}}
/>{' '}
{__('to view content across all of your subscribed channels.')}
</p>
<p className="card__content">
{__(
'2) This app will automatically download new free content from channels you are subscribed to.'
'1) This app will automatically download new free content from channels you are subscribed to.'
)}
</p>
<p className="card__content">
{__(
'3) If we have your email address, we may send you notifications and rewards related to new content.'
'2) If we have your email address, we may send you notifications and rewards related to new content.'
)}
</p>
<div className="modal__buttons">

View file

@ -98,7 +98,7 @@ class ChannelPage extends React.PureComponent<Props> {
</h1>
</section>
<div className="card__actions">
<SubscribeButton uri={`lbry://${permanentUrl}`} channelName={name} />
<SubscribeButton uri={`lbry://${permanentUrl}`} />
<Button
button="alt"
icon={icons.GLOBE}

View file

@ -19,7 +19,7 @@ import SubscribeButton from 'component/subscribeButton';
import Page from 'component/page';
import FileDownloadLink from 'component/fileDownloadLink';
import classnames from 'classnames';
import getMediaType from 'util/getMediaType';
import getMediaType from 'util/get-media-type';
import RecommendedContent from 'component/recommendedContent';
import { FormField, FormRow } from 'component/common/form';
import ToolTip from 'component/common/tooltip';
@ -208,7 +208,7 @@ class FilePage extends React.Component<Props> {
}}
/>
) : (
<SubscribeButton uri={channelUri} channelName={channelName} />
<SubscribeButton uri={channelUri} />
)}
{!claimIsMine && (
<Button

View file

@ -7,11 +7,15 @@ import {
selectIsFetchingSubscriptions,
selectUnreadSubscriptions,
selectViewMode,
selectFirstRunCompleted,
selectshowSuggestedSubs,
} from 'redux/selectors/subscriptions';
import {
doUpdateUnreadSubscriptions,
doFetchMySubscriptions,
doSetViewMode,
doFetchRecommendedSubscriptions,
doCompleteFirstRun,
doShowSuggestedSubs,
} from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -26,14 +30,18 @@ const select = state => ({
allSubscriptions: selectSubscriptionClaims(state),
unreadSubscriptions: selectUnreadSubscriptions(state),
viewMode: selectViewMode(state),
firstRunCompleted: selectFirstRunCompleted(state),
showSuggestedSubs: selectshowSuggestedSubs(state),
});
export default connect(
select,
{
doUpdateUnreadSubscriptions,
doFetchMySubscriptions,
doSetClientSetting,
doSetViewMode,
doFetchRecommendedSubscriptions,
doCompleteFirstRun,
doShowSuggestedSubs,
}
)(SubscriptionsPage);

View file

@ -0,0 +1,63 @@
// @flow
import React, { Fragment } from 'react';
import Native from 'native';
import Button from 'component/button';
import SuggestedSubscriptions from 'component/subscribeSuggested';
type Props = {
showSuggested: boolean,
loadingSuggested: boolean,
numberOfSubscriptions: number,
onFinish: () => void,
doShowSuggestedSubs: () => void,
};
export default (props: Props) => {
const {
showSuggested,
loadingSuggested,
numberOfSubscriptions,
doShowSuggestedSubs,
onFinish,
} = props;
return (
<Fragment>
<div className="page__empty--horizontal">
<img
alt="Friendly gerbil"
className="subscriptions__gerbil"
src={Native.imagePath('gerbil-happy.png')}
/>
<div className="card__content">
<h2 className="card__title">
{numberOfSubscriptions > 0 ? __('Woohoo!') : __('No subscriptions... yet.')}
</h2>
<p className="card__subtitle">
{showSuggested
? __('I hear these channels are pretty good.')
: __("I'll tell you where the good channels are if you find me a wheel.")}
</p>
{!showSuggested && (
<div className="card__actions">
<Button button="primary" label={__('Explore')} onClick={doShowSuggestedSubs} />
</div>
)}
{showSuggested &&
numberOfSubscriptions > 0 && (
<div className="card__actions">
<Button
button="primary"
onClick={onFinish}
label={`${__('View your')} ${numberOfSubscriptions} ${
numberOfSubscriptions > 1 ? __('subcriptions') : __('subscription')
}`}
/>
</div>
)}
</div>
</div>
{showSuggested && !loadingSuggested && <SuggestedSubscriptions />}
</Fragment>
);
};

View file

@ -0,0 +1,134 @@
// @flow
import type { ViewMode } from 'types/subscription';
import type { Claim } from 'types/claim';
import { VIEW_ALL, VIEW_LATEST_FIRST } from 'constants/subscriptions';
import React, { Fragment } from 'react';
import Button from 'component/button';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
import FileList from 'component/fileList';
import { FormField } from 'component/common/form';
import FileCard from 'component/fileCard';
import { parseURI } from 'lbry-redux';
import Native from 'native';
import SuggestedSubscriptions from 'component/subscribeSuggested';
type Props = {
viewMode: ViewMode,
doSetViewMode: ViewMode => void,
hasSubscriptions: boolean,
subscriptions: Array<{ uri: string, ...Claim }>,
autoDownload: boolean,
onChangeAutoDownload: (SyntheticInputEvent<*>) => void,
unreadSubscriptions: Array<{ channel: string, uris: Array<string> }>,
};
export default (props: Props) => {
const {
viewMode,
doSetViewMode,
hasSubscriptions,
subscriptions,
autoDownload,
onChangeAutoDownload,
unreadSubscriptions,
} = props;
return (
<Fragment>
<HiddenNsfwClaims
uris={subscriptions.reduce((arr, { name, claim_id: claimId }) => {
if (name && claimId) {
arr.push(`lbry://${name}#${claimId}`);
}
return arr;
}, [])}
/>
{hasSubscriptions && (
<div className="card--space-between">
<div className="card__actions card__actions--no-margin">
<Button
disabled={viewMode === VIEW_ALL}
button="link"
label="All Subscriptions"
onClick={() => doSetViewMode(VIEW_ALL)}
/>
<Button
button="link"
disabled={viewMode === VIEW_LATEST_FIRST}
label={__('Latest Only')}
onClick={() => doSetViewMode(VIEW_LATEST_FIRST)}
/>
</div>
<FormField
type="checkbox"
name="auto_download"
onChange={onChangeAutoDownload}
checked={autoDownload}
prefix={__('Auto download')}
/>
</div>
)}
{!hasSubscriptions && (
<Fragment>
<div className="page__empty--horizontal">
<img
alt="Sad gerbil"
className="subscriptions__gerbil"
src={Native.imagePath('gerbil-sad.png')}
/>
<div className="card__content">
<h2 className="card__title">{__('Oh no! What happened to your subscriptions?')}</h2>
<p className="card__subtitle">{__('These channels look pretty cool.')}</p>
</div>
</div>
<SuggestedSubscriptions />
</Fragment>
)}
{hasSubscriptions && (
<div className="card__content">
{viewMode === VIEW_ALL && (
<Fragment>
<div className="card__title">{__('Your subscriptions')}</div>
<FileList hideFilter sortByHeight fileInfos={subscriptions} />
</Fragment>
)}
{viewMode === VIEW_LATEST_FIRST && (
<Fragment>
{unreadSubscriptions.length ? (
unreadSubscriptions.map(({ channel, uris }) => {
const { claimName } = parseURI(channel);
return (
<section key={channel}>
<div className="card__title">
<Button
button="link"
navigate="/show"
navigateParams={{ uri: channel }}
label={claimName}
/>
</div>
<div className="card__list card__content">
{uris.map(uri => <FileCard isNew key={uri} uri={uri} />)}
</div>
</section>
);
})
) : (
<Fragment>
<div className="page__empty">
<h3 className="card__title">{__('All caught up!')}</h3>
<p className="card__subtitle">{__('You might like these channels.')}</p>
</div>
<SuggestedSubscriptions />
</Fragment>
)}
</Fragment>
)}
</div>
)}
</Fragment>
);
};

View file

@ -1,16 +1,11 @@
// @flow
import type { ViewMode } from 'types/subscription';
import type { Claim } from 'types/claim';
import { VIEW_ALL, VIEW_LATEST_FIRST } from 'constants/subscriptions';
import * as settings from 'constants/settings';
import * as React from 'react';
import * as SETTINGS from 'constants/settings';
import React, { PureComponent } from 'react';
import Page from 'component/page';
import Button from 'component/button';
import FileList from 'component/fileList';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
import { FormField } from 'component/common/form';
import FileCard from 'component/fileCard';
import { parseURI } from 'lbry-redux';
import FirstRun from './internal/first-run';
import UserSubscriptions from './internal/user-subscriptions';
type Props = {
subscribedChannels: Array<string>, // The channels a user is subscribed to
@ -25,9 +20,15 @@ type Props = {
doSetViewMode: ViewMode => void,
doFetchMySubscriptions: () => void,
doSetClientSetting: (string, boolean) => void,
doFetchRecommendedSubscriptions: () => void,
loadingSuggested: boolean,
firstRunCompleted: boolean,
doCompleteFirstRun: () => void,
doShowSuggestedSubs: () => void,
showSuggestedSubs: boolean,
};
export default class extends React.PureComponent<Props> {
export default class extends PureComponent<Props> {
constructor() {
super();
@ -35,56 +36,26 @@ export default class extends React.PureComponent<Props> {
}
componentDidMount() {
const { doFetchMySubscriptions } = this.props;
const {
doFetchMySubscriptions,
doFetchRecommendedSubscriptions,
allSubscriptions,
firstRunCompleted,
doShowSuggestedSubs,
} = this.props;
doFetchMySubscriptions();
doFetchRecommendedSubscriptions();
// For channels that already have subscriptions, show the suggested subs right away
// This can probably be removed at a future date, it is just to make it so the "view your x subscriptions" button shows up right away
// Existing users will still go through the "first run"
if (!firstRunCompleted && allSubscriptions.length) {
doShowSuggestedSubs();
}
}
onAutoDownloadChange(event: SyntheticInputEvent<*>) {
this.props.doSetClientSetting(settings.AUTO_DOWNLOAD, event.target.checked);
}
renderSubscriptions() {
const { viewMode, unreadSubscriptions, allSubscriptions } = this.props;
if (viewMode === VIEW_ALL) {
return (
<React.Fragment>
<div className="card__title">{__('Your subscriptions')}</div>
<FileList hideFilter sortByHeight fileInfos={allSubscriptions} />
</React.Fragment>
);
}
return (
<React.Fragment>
{unreadSubscriptions.length ? (
unreadSubscriptions.map(({ channel, uris }) => {
const { claimName } = parseURI(channel);
return (
<section key={channel}>
<div className="card__title">
<Button
button="link"
navigate="/show"
navigateParams={{ uri: channel }}
label={claimName}
/>
</div>
<div className="card__list card__content">
{uris.map(uri => <FileCard isNew key={uri} uri={uri} />)}
</div>
</section>
);
})
) : (
<div className="page__empty">
<h3 className="card__title">{__('You are all caught up!')}</h3>
<div className="card__actions">
<Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div>
</div>
)}
</React.Fragment>
);
this.props.doSetClientSetting(SETTINGS.AUTO_DOWNLOAD, event.target.checked);
}
render() {
@ -95,58 +66,39 @@ export default class extends React.PureComponent<Props> {
autoDownload,
viewMode,
doSetViewMode,
loadingSuggested,
firstRunCompleted,
doCompleteFirstRun,
doShowSuggestedSubs,
showSuggestedSubs,
unreadSubscriptions,
} = this.props;
const numberOfSubscriptions = subscribedChannels && subscribedChannels.length;
return (
// Only pass in the loading prop if there are no subscriptions
// If there are any, let the page update in the background
// The loading prop removes children and shows a loading spinner
<Page notContained loading={loading && !subscribedChannels}>
<HiddenNsfwClaims
uris={allSubscriptions.reduce((arr, { name, claim_id: claimId }) => {
if (name && claimId) {
arr.push(`lbry://${name}#${claimId}`);
}
return arr;
}, [])}
/>
{!!subscribedChannels.length && (
<div className="card--space-between">
<div className="card__actions card__actions--no-margin">
<Button
disabled={viewMode === VIEW_ALL}
button="link"
label="All Subscriptions"
onClick={() => doSetViewMode(VIEW_ALL)}
/>
<Button
button="link"
disabled={viewMode === VIEW_LATEST_FIRST}
label={__('Latest Only')}
onClick={() => doSetViewMode(VIEW_LATEST_FIRST)}
/>
</div>
<FormField
type="checkbox"
name="auto_download"
onChange={this.onAutoDownloadChange}
checked={autoDownload}
prefix={__('Auto download')}
/>
</div>
)}
{!subscribedChannels.length && (
<div className="page__empty">
<h3 className="card__title">
{__("It looks like you aren't subscribed to any channels yet.")}
</h3>
<div className="card__actions">
<Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div>
</div>
)}
{!!subscribedChannels.length && (
<div className="card__content">{this.renderSubscriptions()}</div>
{firstRunCompleted ? (
<UserSubscriptions
viewMode={viewMode}
doSetViewMode={doSetViewMode}
hasSubscriptions={numberOfSubscriptions > 0}
subscriptions={allSubscriptions}
autoDownload={autoDownload}
onChangeAutoDownload={this.onAutoDownloadChange}
unreadSubscriptions={unreadSubscriptions}
loadingSuggested={loadingSuggested}
/>
) : (
<FirstRun
showSuggested={showSuggestedSubs}
doShowSuggestedSubs={doShowSuggestedSubs}
loadingSuggested={loadingSuggested}
numberOfSubscriptions={numberOfSubscriptions}
onFinish={doCompleteFirstRun}
/>
)}
</Page>
);

View file

@ -24,11 +24,11 @@ import {
makeSelectChannelForClaimUri,
parseURI,
creditsToString,
doError
doError,
} from 'lbry-redux';
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
import setBadge from 'util/setBadge';
import setProgressBar from 'util/setProgressBar';
import setBadge from 'util/set-badge';
import setProgressBar from 'util/set-progress-bar';
import analytics from 'analytics';
const DOWNLOAD_POLL_INTERVAL = 250;

View file

@ -10,7 +10,7 @@ import {
} from 'lbry-redux';
import { doHideModal } from 'redux/actions/app';
import { doHistoryBack } from 'redux/actions/navigation';
import setProgressBar from 'util/setProgressBar';
import setProgressBar from 'util/set-progress-bar';
export function doOpenFileInFolder(path) {
return () => {

View file

@ -1,5 +1,5 @@
import { ACTIONS, selectHistoryIndex, selectHistoryStack } from 'lbry-redux';
import { toQueryString } from 'util/query_params';
import { toQueryString } from 'util/query-params';
import analytics from 'analytics';
export function doNavigate(path, params = {}, options = {}) {

View file

@ -394,3 +394,33 @@ export const doCheckSubscriptionsInit = () => (dispatch: ReduxDispatch) => {
data: { checkSubscriptionsTimer },
});
};
export const doFetchRecommendedSubscriptions = () => (dispatch: ReduxDispatch) => {
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
});
return Lbryio.call('subscription', 'suggest')
.then(suggested =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS,
data: suggested,
})
)
.catch(error =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL,
error,
})
);
};
export const doCompleteFirstRun = () => (dispatch: ReduxDispatch) =>
dispatch({
type: ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED,
});
export const doShowSuggestedSubs = () => (dispatch: ReduxDispatch) =>
dispatch({
type: ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS,
});

View file

@ -12,13 +12,18 @@ import type {
DoRemoveSubscriptionUnreads,
FetchedSubscriptionsSucess,
SetViewMode,
GetSuggestedSubscriptionsSuccess,
} from 'types/subscription';
const defaultState: SubscriptionState = {
subscriptions: [],
unread: {},
suggested: {},
loading: false,
viewMode: VIEW_ALL,
loadingSuggested: false,
firstRunCompleted: false,
showSuggestedSubs: false,
};
export default handleActions(
@ -52,7 +57,7 @@ export default handleActions(
}
return {
...state,
...unread,
unread: { ...unread },
subscriptions: newSubscriptions,
};
},
@ -128,6 +133,30 @@ export default handleActions(
...state,
viewMode: action.data,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: true,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS]: (
state: SubscriptionState,
action: GetSuggestedSubscriptionsSuccess
): SubscriptionState => ({
...state,
suggested: action.data,
loadingSuggested: false,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: false,
}),
[ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED]: (state: SubscriptionState): SubscriptionState => ({
...state,
firstRunCompleted: true,
}),
[ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({
...state,
showSuggestedSubs: true,
}),
},
defaultState
);

View file

@ -1,3 +1,4 @@
import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions';
import { createSelector } from 'reselect';
import {
selectAllClaimsByChannel,
@ -7,6 +8,7 @@ import {
selectClaimsByUri,
parseURI,
} from 'lbry-redux';
import { swapKeyAndValue } from 'util/swap-json';
// Returns the entire subscriptions state
const selectState = state => state.subscriptions || {};
@ -20,6 +22,72 @@ export const selectIsFetchingSubscriptions = createSelector(selectState, state =
// The current view mode on the subscriptions page
export const selectViewMode = createSelector(selectState, state => state.viewMode);
// Suggested subscriptions from internal apis
export const selectSuggested = createSelector(selectState, state => state.suggested);
export const selectLoadingSuggested = createSelector(selectState, state => state.loadingSuggested);
export const selectSuggestedChannels = createSelector(
selectSubscriptions,
selectSuggested,
(userSubscriptions, suggested) => {
if (!suggested) {
return null;
}
// Swap the key/value because we will use the uri for everything, this just makes it easier
// suggested is returned from the api with the form:
// {
// featured: { "Channel label": uri, ... },
// top_subscribed: { "@channel": uri, ... }
// top_bid: { "@channel": uri, ... }
// }
// To properly compare the suggested subscriptions from our current subscribed channels
// We only care about the uri, not the label
// We also only care about top_subscribed and featured
// top_bid could just be porn or a channel with no content
const topSubscribedSuggestions = swapKeyAndValue(suggested[SUGGESTED_TOP_SUBSCRIBED]);
const featuredSuggestions = swapKeyAndValue(suggested[SUGGESTED_FEATURED]);
// Make sure there are no duplicates
// If a uri isn't already in the suggested object, add it
const suggestedChannels = { ...topSubscribedSuggestions };
Object.keys(featuredSuggestions).forEach(uri => {
if (!suggestedChannels[uri]) {
const channelLabel = featuredSuggestions[uri];
suggestedChannels[uri] = channelLabel;
}
});
userSubscriptions.forEach(({ uri }) => {
// Note to passer bys:
// Maybe we should just remove the `lbry://` prefix from subscription uris
// Most places don't store them like that
const subscribedUri = uri.slice('lbry://'.length);
if (suggestedChannels[subscribedUri]) {
delete suggestedChannels[subscribedUri];
}
});
return Object.keys(suggestedChannels)
.map(uri => ({
uri,
label: suggestedChannels[uri],
}))
.slice(0, 5);
}
);
export const selectFirstRunCompleted = createSelector(
selectState,
state => state.firstRunCompleted
);
export const selectshowSuggestedSubs = createSelector(
selectState,
state => state.showSuggestedSubs
);
// Fetching any claims that are a part of a users subscriptions
export const selectSubscriptionsBeingFetched = createSelector(
selectSubscriptions,

View file

@ -192,9 +192,19 @@ p:not(:first-of-type) {
display: flex;
flex-direction: column;
margin-top: 200px;
margin-bottom: 100px;
text-align: center;
}
// Empty pages that display columns of content
.page__empty--horizontal {
max-width: 60vw;
margin: auto;
display: flex;
flex-direction: row;
text-align: left;
}
.columns {
display: flex;
justify-content: space-between;

View file

@ -0,0 +1,23 @@
// The gerbil is tied to subscriptions currently, but this style should move to it's own file once
// the gerbil is added in more places with different layouts
.subscriptions__gerbil {
height: 250px;
width: 210px;
}
.subscriptions__suggested {
animation: expand 0.2s;
width: 100%;
margin-top: $spacing-vertical;
}
@-webkit-keyframes expand {
0% {
margin-top: 200px;
opacity: 0;
}
100% {
margin-top: $spacing-vertical;
opacity: 1;
}
}

View file

@ -101,7 +101,6 @@ const compressor = createCompressor();
// We were caching so much data the app was locking up
// We can't add this back until we can perform this in a non-blocking way
// const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions', 'unread', 'viewMode']);
const contentFilter = createFilter('content', ['positions', 'history']);
const fileInfoFilter = createFilter('fileInfo', [
'fileListPublishedSort',
@ -116,14 +115,7 @@ const persistOptions = {
whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app'],
// Order is important. Needs to be compressed last or other transforms can't
// read the data
transforms: [
subscriptionsFilter,
walletFilter,
contentFilter,
fileInfoFilter,
appFilter,
compressor,
],
transforms: [walletFilter, contentFilter, fileInfoFilter, appFilter, compressor],
debounce: 10000,
storage: localForage,
};

View file

@ -7,6 +7,9 @@ import {
NOTIFY_ONLY,
VIEW_ALL,
VIEW_LATEST_FIRST,
SUGGESTED_TOP_BID,
SUGGESTED_TOP_SUBSCRIBED,
SUGGESTED_FEATURED,
} from 'constants/subscriptions';
export type Subscription = {
@ -31,11 +34,21 @@ export type UnreadSubscriptions = {
export type ViewMode = VIEW_LATEST_FIRST | VIEW_ALL;
export type SuggestedType = SUGGESTED_TOP_BID | SUGGESTED_TOP_SUBSCRIBED | SUGGESTED_FEATURED;
export type SuggestedSubscriptions = {
[SuggestedType]: string,
};
export type SubscriptionState = {
subscriptions: Array<Subscription>,
unread: UnreadSubscriptions,
loading: boolean,
viewMode: ViewMode,
suggested: SuggestedSubscriptions,
loadingSuggested: boolean,
firstRunCompleted: boolean,
showSuggestedSubs: boolean,
};
//
@ -94,6 +107,11 @@ export type SetViewMode = {
data: ViewMode,
};
export type GetSuggestedSubscriptionsSuccess = {
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
data: SuggestedSubscriptions,
};
export type Action =
| DoChannelSubscribe
| DoChannelUnsubscribe

View file

@ -0,0 +1,10 @@
export function swapKeyAndValue(dict) {
const ret = {};
// eslint-disable-next-line no-restricted-syntax
for (const key in dict) {
if (dict.hasOwnProperty(key)) {
ret[dict[key]] = key;
}
}
return ret;
}

BIN
static/img/gerbil-happy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
static/img/gerbil-sad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB