add recommended subscriptions
This commit is contained in:
parent
b245028050
commit
4ecd5e1684
48 changed files with 594 additions and 189 deletions
|
@ -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 } ]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
);
|
||||
};
|
||||
|
|
12
src/renderer/component/subscribeSuggested/index.js
Normal file
12
src/renderer/component/subscribeSuggested/index.js
Normal 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);
|
23
src/renderer/component/subscribeSuggested/view.jsx
Normal file
23
src/renderer/component/subscribeSuggested/view.jsx
Normal 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;
|
|
@ -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';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { stopContextMenu } from 'util/contextMenu';
|
||||
import { stopContextMenu } from 'util/context-menu';
|
||||
|
||||
type Props = {
|
||||
source: string,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
63
src/renderer/page/subscriptions/internal/first-run.jsx
Normal file
63
src/renderer/page/subscriptions/internal/first-run.jsx
Normal 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>
|
||||
);
|
||||
};
|
134
src/renderer/page/subscriptions/internal/user-subscriptions.jsx
Normal file
134
src/renderer/page/subscriptions/internal/user-subscriptions.jsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 = {}) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
23
src/renderer/scss/component/_subscriptions.scss
Normal file
23
src/renderer/scss/component/_subscriptions.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
10
src/renderer/util/swap-json.js
Normal file
10
src/renderer/util/swap-json.js
Normal 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
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
BIN
static/img/gerbil-sad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 173 KiB |
Loading…
Add table
Reference in a new issue