mirror of
https://github.com/LBRYFoundation/lbry-desktop.git
synced 2025-08-30 17:01:25 +00:00
Merge pull request #2109 from lbryio/subscriptions
Show recommended subscriptions
This commit is contained in:
commit
1dd6a9e42f
49 changed files with 596 additions and 189 deletions
|
@ -44,6 +44,7 @@
|
||||||
"jsx-a11y/interactive-supports-focus": 0,
|
"jsx-a11y/interactive-supports-focus": 0,
|
||||||
"jsx-a11y/click-events-have-key-events": 0,
|
"jsx-a11y/click-events-have-key-events": 0,
|
||||||
"consistent-return": 0,
|
"consistent-return": 0,
|
||||||
|
"no-prototype-builtins": 0,
|
||||||
"flowtype/space-after-type-colon": [ 2, "always", { "allowLineBreak": true } ]
|
"flowtype/space-after-type-colon": [ 2, "always", { "allowLineBreak": true } ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import ReactModal from 'react-modal';
|
||||||
import throttle from 'util/throttle';
|
import throttle from 'util/throttle';
|
||||||
import SideBar from 'component/sideBar';
|
import SideBar from 'component/sideBar';
|
||||||
import Header from 'component/header';
|
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;
|
const TWO_POINT_FIVE_MINUTES = 1000 * 60 * 2.5;
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import type { Claim } from 'types/claim';
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
import { normalizeURI } from 'lbry-redux';
|
import { normalizeURI } from 'lbry-redux';
|
||||||
import ToolTip from 'component/common/tooltip';
|
import ToolTip from 'component/common/tooltip';
|
||||||
import FileCard from 'component/fileCard';
|
import FileCard from 'component/fileCard';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import type { Claim } from 'types/claim';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
category: string,
|
category: string,
|
||||||
names: Array<string>,
|
names: ?Array<string>,
|
||||||
categoryLink: ?string,
|
categoryLink: ?string,
|
||||||
fetching: boolean,
|
fetching: boolean,
|
||||||
channelClaims: Array<Claim>,
|
channelClaims: ?Array<Claim>,
|
||||||
fetchChannel: string => void,
|
fetchChannel: string => void,
|
||||||
obscureNsfw: boolean,
|
obscureNsfw: boolean,
|
||||||
};
|
};
|
||||||
|
@ -22,9 +23,8 @@ type State = {
|
||||||
canScrollPrevious: boolean,
|
canScrollPrevious: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class CategoryList extends React.PureComponent<Props, State> {
|
class CategoryList extends PureComponent<Props, State> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
names: [],
|
|
||||||
categoryLink: '',
|
categoryLink: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -209,7 +209,6 @@ class CategoryList extends React.PureComponent<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
const { category, categoryLink, names, channelClaims, obscureNsfw } = this.props;
|
const { category, categoryLink, names, channelClaims, obscureNsfw } = this.props;
|
||||||
const { canScrollNext, canScrollPrevious } = this.state;
|
const { canScrollNext, canScrollPrevious } = this.state;
|
||||||
|
|
||||||
const isCommunityTopBids = category.match(/^community/i);
|
const isCommunityTopBids = category.match(/^community/i);
|
||||||
const showScrollButtons = isCommunityTopBids ? !obscureNsfw : true;
|
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__header">
|
||||||
<div className="card-row__title">
|
<div className="card-row__title">
|
||||||
{categoryLink ? (
|
{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
|
category
|
||||||
)}
|
)}
|
||||||
|
@ -263,19 +265,33 @@ class CategoryList extends React.PureComponent<Props, State> {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{names &&
|
{names &&
|
||||||
|
names.length &&
|
||||||
names.map(name => (
|
names.map(name => (
|
||||||
<FileCard showSubscribedLogo key={name} uri={normalizeURI(name)} />
|
<FileCard showSubscribedLogo key={name} uri={normalizeURI(name)} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{channelClaims &&
|
{channelClaims &&
|
||||||
channelClaims.length &&
|
channelClaims.length &&
|
||||||
channelClaims.map(claim => (
|
channelClaims
|
||||||
<FileCard
|
// Only show the first 10 claims, regardless of the amount we have on a channel page
|
||||||
showSubcribedLogo
|
.slice(0, 10)
|
||||||
key={claim.claim_id}
|
.map(claim => (
|
||||||
uri={`lbry://${claim.name}#${claim.claim_id}`}
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -76,7 +76,7 @@ class ChannelTile extends React.PureComponent<Props> {
|
||||||
)}
|
)}
|
||||||
{subscriptionUri && (
|
{subscriptionUri && (
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
|
<SubscribeButton uri={subscriptionUri} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { formatCredits, formatFullPrice } from 'util/formatCredits';
|
import { formatCredits, formatFullPrice } from 'util/format-credits';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
amount: number,
|
amount: number,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import parseData from 'util/parseData';
|
import parseData from 'util/parse-data';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import { remote } from 'electron';
|
import { remote } from 'electron';
|
||||||
|
|
||||||
|
@ -33,7 +33,10 @@ class FileExporter extends React.PureComponent<Props> {
|
||||||
fs.writeFile(filename, data, err => {
|
fs.writeFile(filename, data, err => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
// Do something after creation
|
// 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.dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||||
remote.getCurrentWindow(),
|
// User hit cancel so do nothing:
|
||||||
options,
|
if (!filename) return;
|
||||||
filename => {
|
// Get extension and remove initial dot
|
||||||
// User hit cancel so do nothing:
|
const format = path.extname(filename).replace(/\./g, '');
|
||||||
if (!filename) return;
|
// Parse data to string with the chosen format
|
||||||
// Get extension and remove initial dot
|
const parsed = parseData(data, format, filters);
|
||||||
const format = path.extname(filename).replace(/\./g, '');
|
// Write file
|
||||||
// Parse data to string with the chosen format
|
if (parsed) {
|
||||||
const parsed = parseData(data, format, filters);
|
this.handleFileCreation(filename, parsed);
|
||||||
// Write file
|
|
||||||
parsed && this.handleFileCreation(filename, parsed);
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { title, label } = this.props;
|
const { label } = this.props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
import 'simplemde/dist/simplemde.min.css'; // eslint-disable-line import/no-extraneous-dependencies
|
import 'simplemde/dist/simplemde.min.css'; // eslint-disable-line import/no-extraneous-dependencies
|
||||||
import Toggle from 'react-toggle';
|
import Toggle from 'react-toggle';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/contextMenu';
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import UriIndicator from 'component/uriIndicator';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import FilePrice from 'component/filePrice';
|
import FilePrice from 'component/filePrice';
|
||||||
import { openCopyLinkMenu } from 'util/contextMenu';
|
import { openCopyLinkMenu } from 'util/context-menu';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectBalance, selectIsBackDisabled, selectIsForwardDisabled } from 'lbry-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 { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
|
||||||
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
|
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
|
||||||
import { doDownloadUpgradeRequested } from 'redux/actions/app';
|
import { doDownloadUpgradeRequested } from 'redux/actions/app';
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
|
import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
|
||||||
import { doDownloadUpgrade } from 'redux/actions/app';
|
import { doDownloadUpgrade } from 'redux/actions/app';
|
||||||
import { selectIsUpgradeAvailable, selectNavLinks } from 'redux/selectors/app';
|
import { selectIsUpgradeAvailable, selectNavLinks } from 'redux/selectors/app';
|
||||||
import { formatCredits } from 'util/formatCredits';
|
import { formatCredits } from 'util/format-credits';
|
||||||
import Page from './view';
|
import Page from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
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';
|
import SubscribeButton from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
subscriptions: selectSubscriptions(state),
|
subscriptions: selectSubscriptions(state),
|
||||||
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
||||||
|
firstRunCompleted: selectFirstRunCompleted(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -15,5 +21,6 @@ export default connect(
|
||||||
doChannelSubscribe,
|
doChannelSubscribe,
|
||||||
doChannelUnsubscribe,
|
doChannelUnsubscribe,
|
||||||
doOpenModal,
|
doOpenModal,
|
||||||
|
doToast,
|
||||||
}
|
}
|
||||||
)(SubscribeButton);
|
)(SubscribeButton);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { parseURI } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
|
||||||
type SubscribtionArgs = {
|
type SubscribtionArgs = {
|
||||||
|
@ -10,30 +11,36 @@ type SubscribtionArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelName: ?string,
|
uri: string,
|
||||||
uri: ?string,
|
|
||||||
isSubscribed: boolean,
|
isSubscribed: boolean,
|
||||||
subscriptions: Array<string>,
|
subscriptions: Array<string>,
|
||||||
doChannelSubscribe: ({ channelName: string, uri: string }) => void,
|
doChannelSubscribe: ({ channelName: string, uri: string }) => void,
|
||||||
doChannelUnsubscribe: SubscribtionArgs => void,
|
doChannelUnsubscribe: SubscribtionArgs => void,
|
||||||
doOpenModal: ({ id: string }) => void,
|
doOpenModal: ({ id: string }) => void,
|
||||||
|
firstRunCompleted: boolean,
|
||||||
|
showSnackBarOnSubscribe: boolean,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (props: Props) => {
|
export default (props: Props) => {
|
||||||
const {
|
const {
|
||||||
channelName,
|
|
||||||
uri,
|
uri,
|
||||||
doChannelSubscribe,
|
doChannelSubscribe,
|
||||||
doChannelUnsubscribe,
|
doChannelUnsubscribe,
|
||||||
doOpenModal,
|
doOpenModal,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
|
firstRunCompleted,
|
||||||
|
showSnackBarOnSubscribe,
|
||||||
|
doToast,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||||
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
||||||
|
|
||||||
return channelName && uri ? (
|
const { claimName } = parseURI(uri);
|
||||||
|
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
iconColor="red"
|
iconColor="red"
|
||||||
icon={isSubscribed ? undefined : ICONS.HEART}
|
icon={isSubscribed ? undefined : ICONS.HEART}
|
||||||
|
@ -42,14 +49,19 @@ export default (props: Props) => {
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!subscriptions.length) {
|
if (!subscriptions.length && !firstRunCompleted) {
|
||||||
doOpenModal(MODALS.FIRST_SUBSCRIPTION);
|
doOpenModal(MODALS.FIRST_SUBSCRIPTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptionHandler({
|
subscriptionHandler({
|
||||||
channelName,
|
channelName: claimName,
|
||||||
uri,
|
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 * as React from 'react';
|
||||||
import CodeMirror from 'codemirror/lib/codemirror';
|
import CodeMirror from 'codemirror/lib/codemirror';
|
||||||
import { openSnippetMenu, stopContextMenu } from 'util/contextMenu';
|
import { openSnippetMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
|
||||||
// Addons
|
// Addons
|
||||||
import 'codemirror/addon/selection/mark-selection';
|
import 'codemirror/addon/selection/mark-selection';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { stopContextMenu } from 'util/contextMenu';
|
import { stopContextMenu } from 'util/context-menu';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
source: string,
|
source: string,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { stopContextMenu } from 'util/contextMenu';
|
import { stopContextMenu } from 'util/context-menu';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
source: string,
|
source: string,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { normalizeURI, SEARCH_TYPES, isURIValid } from 'lbry-redux';
|
import { normalizeURI, SEARCH_TYPES, isURIValid } from 'lbry-redux';
|
||||||
import Icon from 'component/common/icon';
|
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 * as icons from 'constants/icons';
|
||||||
import Autocomplete from './internal/autocomplete';
|
import Autocomplete from './internal/autocomplete';
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ type Props = {
|
||||||
doBlur: () => void,
|
doBlur: () => void,
|
||||||
resultCount: number,
|
resultCount: number,
|
||||||
focused: boolean,
|
focused: boolean,
|
||||||
|
doShowSnackBar: ({}) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class WunderBar extends React.PureComponent<Props> {
|
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_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
|
||||||
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
|
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
|
||||||
export const SET_VIEW_MODE = 'SET_VIEW_MODE';
|
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
|
// Publishing
|
||||||
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
||||||
|
|
|
@ -5,3 +5,8 @@ export const VIEW_LATEST_FIRST = 'view_latest_first';
|
||||||
export const DOWNLOADING = 'DOWNLOADING';
|
export const DOWNLOADING = 'DOWNLOADING';
|
||||||
export const DOWNLOADED = 'DOWNLOADED';
|
export const DOWNLOADED = 'DOWNLOADED';
|
||||||
export const NOTIFY_ONLY = 'NOTIFY_ONLY;';
|
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 { connect } from 'react-redux';
|
||||||
import { doHideModal } from 'redux/actions/app';
|
import { doHideModal } from 'redux/actions/app';
|
||||||
import { doNavigate } from 'redux/actions/navigation';
|
|
||||||
import ModalFirstSubscription from './view';
|
import ModalFirstSubscription from './view';
|
||||||
|
|
||||||
const perform = dispatch => () => ({
|
const perform = dispatch => () => ({
|
||||||
closeModal: () => dispatch(doHideModal()),
|
closeModal: () => dispatch(doHideModal()),
|
||||||
navigate: path => dispatch(doNavigate(path)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -5,37 +5,24 @@ import Button from 'component/button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
closeModal: () => void,
|
closeModal: () => void,
|
||||||
navigate: string => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalFirstSubscription = (props: Props) => {
|
const ModalFirstSubscription = (props: Props) => {
|
||||||
const { closeModal, navigate } = props;
|
const { closeModal } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal type="custom" isOpen contentLabel="Subscriptions 101" title={__('Subscriptions 101')}>
|
<Modal type="custom" isOpen contentLabel="Subscriptions 101" title={__('Subscriptions 101')}>
|
||||||
<section className="card__content">
|
<section className="card__content">
|
||||||
<p>{__('You just subscribed to your first channel. Awesome!')}</p>
|
<p>{__('You just subscribed to your first channel. Awesome!')}</p>
|
||||||
<p>{__('A few quick things to know:')}</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">
|
<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>
|
||||||
<p className="card__content">
|
<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>
|
</p>
|
||||||
<div className="modal__buttons">
|
<div className="modal__buttons">
|
||||||
|
|
|
@ -98,7 +98,7 @@ class ChannelPage extends React.PureComponent<Props> {
|
||||||
</h1>
|
</h1>
|
||||||
</section>
|
</section>
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
<SubscribeButton uri={`lbry://${permanentUrl}`} channelName={name} />
|
<SubscribeButton uri={`lbry://${permanentUrl}`} />
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
icon={icons.GLOBE}
|
icon={icons.GLOBE}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import SubscribeButton from 'component/subscribeButton';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import getMediaType from 'util/getMediaType';
|
import getMediaType from 'util/get-media-type';
|
||||||
import RecommendedContent from 'component/recommendedContent';
|
import RecommendedContent from 'component/recommendedContent';
|
||||||
import { FormField, FormRow } from 'component/common/form';
|
import { FormField, FormRow } from 'component/common/form';
|
||||||
import ToolTip from 'component/common/tooltip';
|
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 && (
|
{!claimIsMine && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -7,11 +7,15 @@ import {
|
||||||
selectIsFetchingSubscriptions,
|
selectIsFetchingSubscriptions,
|
||||||
selectUnreadSubscriptions,
|
selectUnreadSubscriptions,
|
||||||
selectViewMode,
|
selectViewMode,
|
||||||
|
selectFirstRunCompleted,
|
||||||
|
selectshowSuggestedSubs,
|
||||||
} from 'redux/selectors/subscriptions';
|
} from 'redux/selectors/subscriptions';
|
||||||
import {
|
import {
|
||||||
doUpdateUnreadSubscriptions,
|
|
||||||
doFetchMySubscriptions,
|
doFetchMySubscriptions,
|
||||||
doSetViewMode,
|
doSetViewMode,
|
||||||
|
doFetchRecommendedSubscriptions,
|
||||||
|
doCompleteFirstRun,
|
||||||
|
doShowSuggestedSubs,
|
||||||
} from 'redux/actions/subscriptions';
|
} from 'redux/actions/subscriptions';
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
@ -26,14 +30,18 @@ const select = state => ({
|
||||||
allSubscriptions: selectSubscriptionClaims(state),
|
allSubscriptions: selectSubscriptionClaims(state),
|
||||||
unreadSubscriptions: selectUnreadSubscriptions(state),
|
unreadSubscriptions: selectUnreadSubscriptions(state),
|
||||||
viewMode: selectViewMode(state),
|
viewMode: selectViewMode(state),
|
||||||
|
firstRunCompleted: selectFirstRunCompleted(state),
|
||||||
|
showSuggestedSubs: selectshowSuggestedSubs(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
{
|
{
|
||||||
doUpdateUnreadSubscriptions,
|
|
||||||
doFetchMySubscriptions,
|
doFetchMySubscriptions,
|
||||||
doSetClientSetting,
|
doSetClientSetting,
|
||||||
doSetViewMode,
|
doSetViewMode,
|
||||||
|
doFetchRecommendedSubscriptions,
|
||||||
|
doCompleteFirstRun,
|
||||||
|
doShowSuggestedSubs,
|
||||||
}
|
}
|
||||||
)(SubscriptionsPage);
|
)(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
|
// @flow
|
||||||
import type { ViewMode } from 'types/subscription';
|
import type { ViewMode } from 'types/subscription';
|
||||||
import type { Claim } from 'types/claim';
|
import type { Claim } from 'types/claim';
|
||||||
import { VIEW_ALL, VIEW_LATEST_FIRST } from 'constants/subscriptions';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import * as settings from 'constants/settings';
|
import React, { PureComponent } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import FirstRun from './internal/first-run';
|
||||||
import FileList from 'component/fileList';
|
import UserSubscriptions from './internal/user-subscriptions';
|
||||||
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
|
|
||||||
import { FormField } from 'component/common/form';
|
|
||||||
import FileCard from 'component/fileCard';
|
|
||||||
import { parseURI } from 'lbry-redux';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscribedChannels: Array<string>, // The channels a user is subscribed to
|
subscribedChannels: Array<string>, // The channels a user is subscribed to
|
||||||
|
@ -25,9 +20,15 @@ type Props = {
|
||||||
doSetViewMode: ViewMode => void,
|
doSetViewMode: ViewMode => void,
|
||||||
doFetchMySubscriptions: () => void,
|
doFetchMySubscriptions: () => void,
|
||||||
doSetClientSetting: (string, boolean) => 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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -35,56 +36,26 @@ export default class extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { doFetchMySubscriptions } = this.props;
|
const {
|
||||||
|
doFetchMySubscriptions,
|
||||||
|
doFetchRecommendedSubscriptions,
|
||||||
|
allSubscriptions,
|
||||||
|
firstRunCompleted,
|
||||||
|
doShowSuggestedSubs,
|
||||||
|
} = this.props;
|
||||||
doFetchMySubscriptions();
|
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<*>) {
|
onAutoDownloadChange(event: SyntheticInputEvent<*>) {
|
||||||
this.props.doSetClientSetting(settings.AUTO_DOWNLOAD, event.target.checked);
|
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -95,58 +66,39 @@ export default class extends React.PureComponent<Props> {
|
||||||
autoDownload,
|
autoDownload,
|
||||||
viewMode,
|
viewMode,
|
||||||
doSetViewMode,
|
doSetViewMode,
|
||||||
|
loadingSuggested,
|
||||||
|
firstRunCompleted,
|
||||||
|
doCompleteFirstRun,
|
||||||
|
doShowSuggestedSubs,
|
||||||
|
showSuggestedSubs,
|
||||||
|
unreadSubscriptions,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const numberOfSubscriptions = subscribedChannels && subscribedChannels.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Only pass in the loading prop if there are no subscriptions
|
// Only pass in the loading prop if there are no subscriptions
|
||||||
// If there are any, let the page update in the background
|
// If there are any, let the page update in the background
|
||||||
// The loading prop removes children and shows a loading spinner
|
// The loading prop removes children and shows a loading spinner
|
||||||
<Page notContained loading={loading && !subscribedChannels}>
|
<Page notContained loading={loading && !subscribedChannels}>
|
||||||
<HiddenNsfwClaims
|
{firstRunCompleted ? (
|
||||||
uris={allSubscriptions.reduce((arr, { name, claim_id: claimId }) => {
|
<UserSubscriptions
|
||||||
if (name && claimId) {
|
viewMode={viewMode}
|
||||||
arr.push(`lbry://${name}#${claimId}`);
|
doSetViewMode={doSetViewMode}
|
||||||
}
|
hasSubscriptions={numberOfSubscriptions > 0}
|
||||||
return arr;
|
subscriptions={allSubscriptions}
|
||||||
}, [])}
|
autoDownload={autoDownload}
|
||||||
/>
|
onChangeAutoDownload={this.onAutoDownloadChange}
|
||||||
{!!subscribedChannels.length && (
|
unreadSubscriptions={unreadSubscriptions}
|
||||||
<div className="card--space-between">
|
loadingSuggested={loadingSuggested}
|
||||||
<div className="card__actions card__actions--no-margin">
|
/>
|
||||||
<Button
|
) : (
|
||||||
disabled={viewMode === VIEW_ALL}
|
<FirstRun
|
||||||
button="link"
|
showSuggested={showSuggestedSubs}
|
||||||
label="All Subscriptions"
|
doShowSuggestedSubs={doShowSuggestedSubs}
|
||||||
onClick={() => doSetViewMode(VIEW_ALL)}
|
loadingSuggested={loadingSuggested}
|
||||||
/>
|
numberOfSubscriptions={numberOfSubscriptions}
|
||||||
<Button
|
onFinish={doCompleteFirstRun}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,11 +24,11 @@ import {
|
||||||
makeSelectChannelForClaimUri,
|
makeSelectChannelForClaimUri,
|
||||||
parseURI,
|
parseURI,
|
||||||
creditsToString,
|
creditsToString,
|
||||||
doError
|
doError,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
|
||||||
import setBadge from 'util/setBadge';
|
import setBadge from 'util/set-badge';
|
||||||
import setProgressBar from 'util/setProgressBar';
|
import setProgressBar from 'util/set-progress-bar';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
|
|
||||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doHideModal } from 'redux/actions/app';
|
import { doHideModal } from 'redux/actions/app';
|
||||||
import { doHistoryBack } from 'redux/actions/navigation';
|
import { doHistoryBack } from 'redux/actions/navigation';
|
||||||
import setProgressBar from 'util/setProgressBar';
|
import setProgressBar from 'util/set-progress-bar';
|
||||||
|
|
||||||
export function doOpenFileInFolder(path) {
|
export function doOpenFileInFolder(path) {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ACTIONS, selectHistoryIndex, selectHistoryStack } from 'lbry-redux';
|
import { ACTIONS, selectHistoryIndex, selectHistoryStack } from 'lbry-redux';
|
||||||
import { toQueryString } from 'util/query_params';
|
import { toQueryString } from 'util/query-params';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
|
|
||||||
export function doNavigate(path, params = {}, options = {}) {
|
export function doNavigate(path, params = {}, options = {}) {
|
||||||
|
|
|
@ -394,3 +394,33 @@ export const doCheckSubscriptionsInit = () => (dispatch: ReduxDispatch) => {
|
||||||
data: { checkSubscriptionsTimer },
|
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,
|
DoRemoveSubscriptionUnreads,
|
||||||
FetchedSubscriptionsSucess,
|
FetchedSubscriptionsSucess,
|
||||||
SetViewMode,
|
SetViewMode,
|
||||||
|
GetSuggestedSubscriptionsSuccess,
|
||||||
} from 'types/subscription';
|
} from 'types/subscription';
|
||||||
|
|
||||||
const defaultState: SubscriptionState = {
|
const defaultState: SubscriptionState = {
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
unread: {},
|
unread: {},
|
||||||
|
suggested: {},
|
||||||
loading: false,
|
loading: false,
|
||||||
viewMode: VIEW_ALL,
|
viewMode: VIEW_ALL,
|
||||||
|
loadingSuggested: false,
|
||||||
|
firstRunCompleted: false,
|
||||||
|
showSuggestedSubs: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
|
@ -127,6 +132,30 @@ export default handleActions(
|
||||||
...state,
|
...state,
|
||||||
viewMode: action.data,
|
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
|
defaultState
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import {
|
import {
|
||||||
selectAllClaimsByChannel,
|
selectAllClaimsByChannel,
|
||||||
|
@ -7,6 +8,7 @@ import {
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
parseURI,
|
parseURI,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
|
import { swapKeyAndValue } from 'util/swap-json';
|
||||||
|
|
||||||
// Returns the entire subscriptions state
|
// Returns the entire subscriptions state
|
||||||
const selectState = state => state.subscriptions || {};
|
const selectState = state => state.subscriptions || {};
|
||||||
|
@ -20,6 +22,72 @@ export const selectIsFetchingSubscriptions = createSelector(selectState, state =
|
||||||
// The current view mode on the subscriptions page
|
// The current view mode on the subscriptions page
|
||||||
export const selectViewMode = createSelector(selectState, state => state.viewMode);
|
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
|
// Fetching any claims that are a part of a users subscriptions
|
||||||
export const selectSubscriptionsBeingFetched = createSelector(
|
export const selectSubscriptionsBeingFetched = createSelector(
|
||||||
selectSubscriptions,
|
selectSubscriptions,
|
||||||
|
|
|
@ -192,9 +192,20 @@ p:not(:first-of-type) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 200px;
|
margin-top: 200px;
|
||||||
|
margin-bottom: 100px;
|
||||||
text-align: center;
|
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;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.columns {
|
.columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
@ -6,4 +6,5 @@
|
||||||
'component/markdown-editor', 'component/scrollbar', 'component/spinner', 'component/nav',
|
'component/markdown-editor', 'component/scrollbar', 'component/spinner', 'component/nav',
|
||||||
'component/file-list', 'component/file-render', 'component/search', 'component/toggle',
|
'component/file-list', 'component/file-render', 'component/search', 'component/toggle',
|
||||||
'component/dat-gui', 'component/item-list', 'component/time', 'component/icon',
|
'component/dat-gui', 'component/item-list', 'component/time', 'component/icon',
|
||||||
'component/placeholder', 'component/badge', 'component/expandable', 'themes/dark';
|
'component/placeholder', 'component/badge', 'component/expandable', 'component/subscriptions',
|
||||||
|
'themes/dark';
|
||||||
|
|
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 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
|
// We can't add this back until we can perform this in a non-blocking way
|
||||||
// const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
|
// const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
|
||||||
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions', 'unread', 'viewMode']);
|
|
||||||
const contentFilter = createFilter('content', ['positions', 'history']);
|
const contentFilter = createFilter('content', ['positions', 'history']);
|
||||||
const fileInfoFilter = createFilter('fileInfo', [
|
const fileInfoFilter = createFilter('fileInfo', [
|
||||||
'fileListPublishedSort',
|
'fileListPublishedSort',
|
||||||
|
@ -116,14 +115,7 @@ const persistOptions = {
|
||||||
whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app'],
|
whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app'],
|
||||||
// Order is important. Needs to be compressed last or other transforms can't
|
// Order is important. Needs to be compressed last or other transforms can't
|
||||||
// read the data
|
// read the data
|
||||||
transforms: [
|
transforms: [walletFilter, contentFilter, fileInfoFilter, appFilter, compressor],
|
||||||
subscriptionsFilter,
|
|
||||||
walletFilter,
|
|
||||||
contentFilter,
|
|
||||||
fileInfoFilter,
|
|
||||||
appFilter,
|
|
||||||
compressor,
|
|
||||||
],
|
|
||||||
debounce: 10000,
|
debounce: 10000,
|
||||||
storage: localForage,
|
storage: localForage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,9 @@ import {
|
||||||
NOTIFY_ONLY,
|
NOTIFY_ONLY,
|
||||||
VIEW_ALL,
|
VIEW_ALL,
|
||||||
VIEW_LATEST_FIRST,
|
VIEW_LATEST_FIRST,
|
||||||
|
SUGGESTED_TOP_BID,
|
||||||
|
SUGGESTED_TOP_SUBSCRIBED,
|
||||||
|
SUGGESTED_FEATURED,
|
||||||
} from 'constants/subscriptions';
|
} from 'constants/subscriptions';
|
||||||
|
|
||||||
export type Subscription = {
|
export type Subscription = {
|
||||||
|
@ -31,11 +34,21 @@ export type UnreadSubscriptions = {
|
||||||
|
|
||||||
export type ViewMode = VIEW_LATEST_FIRST | VIEW_ALL;
|
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 = {
|
export type SubscriptionState = {
|
||||||
subscriptions: Array<Subscription>,
|
subscriptions: Array<Subscription>,
|
||||||
unread: UnreadSubscriptions,
|
unread: UnreadSubscriptions,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
viewMode: ViewMode,
|
viewMode: ViewMode,
|
||||||
|
suggested: SuggestedSubscriptions,
|
||||||
|
loadingSuggested: boolean,
|
||||||
|
firstRunCompleted: boolean,
|
||||||
|
showSuggestedSubs: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -94,6 +107,11 @@ export type SetViewMode = {
|
||||||
data: ViewMode,
|
data: ViewMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetSuggestedSubscriptionsSuccess = {
|
||||||
|
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
|
||||||
|
data: SuggestedSubscriptions,
|
||||||
|
};
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| DoChannelSubscribe
|
| DoChannelSubscribe
|
||||||
| DoChannelUnsubscribe
|
| 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