persisting subscriptions
This commit is contained in:
parent
f1f3d311e9
commit
a7fe02ea98
27 changed files with 180186 additions and 45 deletions
|
@ -16,5 +16,7 @@ module.name_mapper='^util\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/util\1'
|
|||
module.name_mapper='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/redux\1'
|
||||
module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/types\1'
|
||||
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\1'
|
||||
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1'
|
||||
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1'
|
||||
|
||||
[strict]
|
||||
|
|
|
@ -9,6 +9,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
## [Unreleased]
|
||||
### Added
|
||||
* ShapeShift integration
|
||||
* In-app subscriptions
|
||||
|
||||
### Changed
|
||||
* The first run process for new users has changed substantially. New users can now easily receive one credit.
|
||||
|
|
|
@ -41,6 +41,13 @@ export const Header = props => {
|
|||
title={__("Discover Content")}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={() => navigate("/subscriptions")}
|
||||
button="alt button--flat"
|
||||
icon="icon-at"
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item header__item--wunderbar">
|
||||
<WunderBar />
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ import SearchPage from "page/search";
|
|||
import AuthPage from "page/auth";
|
||||
import InvitePage from "page/invite";
|
||||
import BackupPage from "page/backup";
|
||||
import SubscriptionsPage from "page/subscriptions";
|
||||
|
||||
const route = (page, routesMap) => {
|
||||
const component = routesMap[page];
|
||||
|
@ -46,6 +47,7 @@ const Router = props => {
|
|||
settings: <SettingsPage params={params} />,
|
||||
show: <ShowPage {...params} />,
|
||||
wallet: <WalletPage params={params} />,
|
||||
subscriptions: <SubscriptionsPage params={params} />,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import Link from "component/link";
|
||||
import classnames from "classnames";
|
||||
|
||||
const SubHeader = props => {
|
||||
const { subLinks, currentPage, navigate, modifier } = props;
|
||||
const { subLinks, currentPage, navigate, fullWidth, smallMargin } = props;
|
||||
|
||||
const links = [];
|
||||
|
||||
|
@ -22,7 +23,10 @@ const SubHeader = props => {
|
|||
|
||||
return (
|
||||
<nav
|
||||
className={"sub-header" + (modifier ? " sub-header--" + modifier : "")}
|
||||
className={classnames("sub-header", {
|
||||
"sub-header--full-width": fullWidth,
|
||||
"sub-header--small-margin": smallMargin,
|
||||
})}
|
||||
>
|
||||
{links}
|
||||
</nav>
|
||||
|
|
|
@ -159,3 +159,8 @@ export const GET_ACTIVE_SHIFT_START = "GET_ACTIVE_SHIFT_START";
|
|||
export const GET_ACTIVE_SHIFT_SUCCESS = "GET_ACTIVE_SHIFT_SUCCESS";
|
||||
export const GET_ACTIVE_SHIFT_FAIL = "GET_ACTIVE_SHIFT_FAIL";
|
||||
export const CLEAR_SHAPE_SHIFT = "CLEAR_SHAPE_SHIFT";
|
||||
|
||||
// Subscriptions
|
||||
export const CHANNEL_SUBSCRIBE = "CHANNEL_SUBSCRIBE";
|
||||
export const CHANNEL_UNSUBSCRIBE = "CHANNEL_UNSUBSCRIBE";
|
||||
export const HAS_FETCHED_SUBSCRIPTIONS = "HAS_FETCHED_SUBSCRIPTIONS";
|
||||
|
|
3318
src/renderer/dist/css/all.css
vendored
Normal file
3318
src/renderer/dist/css/all.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
0
src/renderer/dist/css/mixin/link.css
vendored
Normal file
0
src/renderer/dist/css/mixin/link.css
vendored
Normal file
176325
src/renderer/dist/js/bundle.js
vendored
Normal file
176325
src/renderer/dist/js/bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
src/renderer/flow-typed/reselect.js
vendored
Normal file
3
src/renderer/flow-typed/reselect.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
declare module 'reselect' {
|
||||
declare module.exports: any;
|
||||
}
|
|
@ -15,6 +15,11 @@ import {
|
|||
} from "redux/selectors/navigation";
|
||||
import { doNavigate } from "redux/actions/navigation";
|
||||
import { makeSelectTotalPagesForChannel } from "redux/selectors/content";
|
||||
import { selectSubscriptions } from "redux/selectors/subscriptions";
|
||||
import {
|
||||
channelSubscribe,
|
||||
channelUnsubscribe,
|
||||
} from "redux/actions/subscriptions";
|
||||
import ChannelPage from "./view";
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -24,12 +29,16 @@ const select = (state, props) => ({
|
|||
page: makeSelectCurrentParam("page")(state),
|
||||
params: selectCurrentParams(state),
|
||||
totalPages: makeSelectTotalPagesForChannel(props.uri)(state),
|
||||
subscriptions: selectSubscriptions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
|
||||
fetchClaimCount: uri => dispatch(doFetchClaimCountByChannel(uri)),
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
channelSubscribe: subscription => dispatch(channelSubscribe(subscription)),
|
||||
channelUnsubscribe: subscription =>
|
||||
dispatch(channelUnsubscribe(subscription)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ChannelPage);
|
||||
|
|
|
@ -3,6 +3,7 @@ import lbryuri from "lbryuri";
|
|||
import { BusyMessage } from "component/common";
|
||||
import FileTile from "component/fileTile";
|
||||
import ReactPaginate from "react-paginate";
|
||||
import Link from "component/link";
|
||||
|
||||
class ChannelPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
|
@ -38,8 +39,28 @@ class ChannelPage extends React.PureComponent {
|
|||
uri,
|
||||
page,
|
||||
totalPages,
|
||||
channelSubscribe,
|
||||
channelUnsubscribe,
|
||||
subscriptions,
|
||||
} = this.props;
|
||||
|
||||
const { name, claim_id: claimId } = claim;
|
||||
|
||||
const isSubscribed =
|
||||
subscriptions
|
||||
.map(subscription => subscription.channelName)
|
||||
.indexOf(name) !== -1;
|
||||
|
||||
const subscriptionHandler = isSubscribed
|
||||
? channelUnsubscribe
|
||||
: channelSubscribe;
|
||||
|
||||
const subscriptionLabel = isSubscribed ? "Unsubscribe" : "Subscribe";
|
||||
const subscriptionUri = lbryuri.build(
|
||||
{ channelName: name, claimId },
|
||||
false
|
||||
);
|
||||
|
||||
let contentList;
|
||||
if (fetching) {
|
||||
contentList = <BusyMessage message={__("Fetching content")} />;
|
||||
|
@ -68,6 +89,18 @@ class ChannelPage extends React.PureComponent {
|
|||
<div className="card__title-identity">
|
||||
<h1>{uri}</h1>
|
||||
</div>
|
||||
<div className="card__actions">
|
||||
<Link
|
||||
button="primary"
|
||||
label={subscriptionLabel}
|
||||
onClick={() =>
|
||||
subscriptionHandler({
|
||||
channelName: name,
|
||||
uri: subscriptionUri,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<p className="empty">
|
||||
|
|
|
@ -4,16 +4,43 @@ import lbryuri from "lbryuri";
|
|||
import FileCard from "component/fileCard";
|
||||
import { Icon, BusyMessage } from "component/common.js";
|
||||
import ToolTip from "component/tooltip.js";
|
||||
import SubHeader from "component/subHeader";
|
||||
import classnames from "classnames";
|
||||
import Link from "component/link";
|
||||
|
||||
// This should be in a separate file
|
||||
export class FeaturedCategory extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
numItems: undefined,
|
||||
canScrollPrevious: false,
|
||||
canScrollNext: false,
|
||||
};
|
||||
}
|
||||
|
||||
class FeaturedCategory extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
numItems: this.props.names.length,
|
||||
canScrollPrevious: false,
|
||||
canScrollNext: true,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
|
||||
const cards = cardRow.getElementsByTagName("section");
|
||||
|
||||
// check if the last card is visible
|
||||
const lastCard = cards[cards.length - 1];
|
||||
const isCompletelyVisible = this.isCardVisible(lastCard, cardRow, false);
|
||||
|
||||
if (!isCompletelyVisible) {
|
||||
this.setState({
|
||||
canScrollNext: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollPrevious() {
|
||||
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
|
||||
if (cardRow.scrollLeft > 0) {
|
||||
|
@ -148,12 +175,21 @@ class FeaturedCategory extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { category, names } = this.props;
|
||||
const { category, names, categoryLink } = this.props;
|
||||
|
||||
return (
|
||||
<div className="card-row card-row--small">
|
||||
<h3 className="card-row__header">
|
||||
{category}
|
||||
{categoryLink ? (
|
||||
<Link
|
||||
className="no-underline"
|
||||
label={category}
|
||||
href={categoryLink}
|
||||
/>
|
||||
) : (
|
||||
category
|
||||
)}
|
||||
|
||||
{category &&
|
||||
category.match(/^community/i) && (
|
||||
<ToolTip
|
||||
|
@ -214,24 +250,28 @@ class DiscoverPage extends React.PureComponent {
|
|||
failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<main className={hasContent && fetchingFeaturedUris ? "reloading" : null}>
|
||||
<main
|
||||
className={classnames("main main--no-margin", {
|
||||
reloading: hasContent && fetchingFeaturedUris,
|
||||
})}
|
||||
>
|
||||
<SubHeader fullWidth smallMargin />
|
||||
{!hasContent &&
|
||||
fetchingFeaturedUris && (
|
||||
<BusyMessage message={__("Fetching content")} />
|
||||
)}
|
||||
{hasContent &&
|
||||
Object.keys(featuredUris).map(
|
||||
category =>
|
||||
featuredUris[category].length ? (
|
||||
<FeaturedCategory
|
||||
key={category}
|
||||
category={category}
|
||||
names={featuredUris[category]}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
)}
|
||||
Object.keys(featuredUris).map(category => {
|
||||
return featuredUris[category].length ? (
|
||||
<FeaturedCategory
|
||||
key={category}
|
||||
category={category}
|
||||
names={featuredUris[category]}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
})}
|
||||
{failedToLoad && (
|
||||
<div className="empty">{__("Failed to load landing content.")}</div>
|
||||
)}
|
||||
|
|
|
@ -2,8 +2,13 @@ import React from "react";
|
|||
import { connect } from "react-redux";
|
||||
import { doNavigate } from "redux/actions/navigation";
|
||||
import { doFetchFileInfo } from "redux/actions/file_info";
|
||||
import {
|
||||
channelSubscribe,
|
||||
channelUnsubscribe,
|
||||
} from "redux/actions/subscriptions";
|
||||
import { makeSelectFileInfoForUri } from "redux/selectors/file_info";
|
||||
import { selectRewardContentClaimIds } from "redux/selectors/content";
|
||||
import { selectSubscriptions } from "redux/selectors/subscriptions";
|
||||
import { doFetchCostInfoForUri } from "redux/actions/cost_info";
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
|
@ -24,12 +29,16 @@ const select = (state, props) => ({
|
|||
tab: makeSelectCurrentParam("tab")(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
subscriptions: selectSubscriptions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
channelSubscribe: subscription => dispatch(channelSubscribe(subscription)),
|
||||
channelUnsubscribe: subscription =>
|
||||
dispatch(channelUnsubscribe(subscription)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePage);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import lbry from "lbry.js";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import lbry from "lbry";
|
||||
import lbryuri from "lbryuri";
|
||||
import Video from "component/video";
|
||||
import { Thumbnail } from "component/common";
|
||||
import FilePrice from "component/filePrice";
|
||||
|
@ -10,6 +10,7 @@ import Icon from "component/icon";
|
|||
import WalletSendTip from "component/walletSendTip";
|
||||
import DateTime from "component/dateTime";
|
||||
import * as icons from "constants/icons";
|
||||
import Link from "component/link";
|
||||
|
||||
class FilePage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
|
@ -42,6 +43,9 @@ class FilePage extends React.PureComponent {
|
|||
tab,
|
||||
uri,
|
||||
rewardedContentClaimIds,
|
||||
channelSubscribe,
|
||||
channelUnsubscribe,
|
||||
subscriptions,
|
||||
} = this.props;
|
||||
|
||||
const showTipBox = tab == "tip";
|
||||
|
@ -52,7 +56,7 @@ class FilePage extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const { height } = claim;
|
||||
const { height, channel_name: channelName, value } = claim;
|
||||
const title = metadata.title;
|
||||
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const mediaType = lbry.getMediaType(contentType);
|
||||
|
@ -62,6 +66,35 @@ class FilePage extends React.PureComponent {
|
|||
Object.values(player.mime).indexOf(contentType) !== -1 ||
|
||||
mediaType === "audio";
|
||||
|
||||
const channelClaimId =
|
||||
value &&
|
||||
value.publisherSignature &&
|
||||
value.publisherSignature.certificateId;
|
||||
|
||||
const canSubscribe = !!channelName && !!channelClaimId;
|
||||
|
||||
let isSubscribed;
|
||||
let subscriptionHandler;
|
||||
let subscriptionLabel;
|
||||
let subscriptionUri;
|
||||
|
||||
if (canSubscribe) {
|
||||
isSubscribed =
|
||||
subscriptions
|
||||
.map(subscription => subscription.channelName)
|
||||
.indexOf(channelName) !== -1;
|
||||
|
||||
subscriptionHandler = isSubscribed
|
||||
? channelUnsubscribe
|
||||
: channelSubscribe;
|
||||
|
||||
subscriptionLabel = isSubscribed ? "Unsubscribe" : "Subscribe";
|
||||
subscriptionUri = lbryuri.build(
|
||||
{ channelName, claimId: channelClaimId },
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={"card " + (obscureNsfw ? "card--obscured " : "")}>
|
||||
<div className="show-page-media">
|
||||
|
@ -98,6 +131,20 @@ class FilePage extends React.PureComponent {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{canSubscribe && (
|
||||
<div className="card__actions">
|
||||
<Link
|
||||
button="primary"
|
||||
onClick={() =>
|
||||
subscriptionHandler({
|
||||
channelName,
|
||||
uri: subscriptionUri,
|
||||
})
|
||||
}
|
||||
label={subscriptionLabel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FileDetails uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
|
|
21
src/renderer/page/subscriptions/index.js
Normal file
21
src/renderer/page/subscriptions/index.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
selectSubscriptionsFromClaims,
|
||||
selectSubscriptions,
|
||||
selectHasFetchedSubscriptions
|
||||
} from "redux/selectors/subscriptions";
|
||||
import { doFetchClaimsByChannel } from "redux/actions/content";
|
||||
import { setHasFetchedSubscriptions } from "redux/actions/subscriptions";
|
||||
import SubscriptionsPage from "./view";
|
||||
|
||||
const select = state => ({
|
||||
hasFetchedSubscriptions: state.subscriptions.hasFetchedSubscriptions,
|
||||
savedSubscriptions: selectSubscriptions(state),
|
||||
subscriptions: selectSubscriptionsFromClaims(state),
|
||||
})
|
||||
|
||||
export default connect(select, {
|
||||
doFetchClaimsByChannel,
|
||||
setHasFetchedSubscriptions
|
||||
})(SubscriptionsPage);
|
114
src/renderer/page/subscriptions/view.jsx
Normal file
114
src/renderer/page/subscriptions/view.jsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import SubHeader from "component/subHeader";
|
||||
import { BusyMessage } from "component/common.js";
|
||||
import { FeaturedCategory } from "page/discover/view";
|
||||
import type { Subscription } from "redux/reducers/subscriptions";
|
||||
|
||||
type SavedSubscriptions = Array<Subscription>;
|
||||
|
||||
type Props = {
|
||||
doFetchClaimsByChannel: (string, number) => any,
|
||||
savedSubscriptions: SavedSubscriptions,
|
||||
// TODO build out claim types
|
||||
subscriptions: Array<any>,
|
||||
setHasFetchedSubscriptions: () => void,
|
||||
hasFetchedSubscriptions: boolean,
|
||||
};
|
||||
|
||||
export default class extends React.PureComponent<Props> {
|
||||
// setHasFetchedSubscriptions is a terrible hack
|
||||
// it allows the subscriptions to load correctly when refresing on the subscriptions page
|
||||
// currently the page is rendered before the state is rehyrdated
|
||||
// that causes this component to be rendered with zero savedSubscriptions
|
||||
// we need to wait until persist/REHYDRATE has fired before rendering the page
|
||||
componentDidMount() {
|
||||
const { savedSubscriptions, setHasFetchedSubscriptions } = this.props;
|
||||
if (savedSubscriptions.length) {
|
||||
this.fetchSubscriptions(savedSubscriptions);
|
||||
setHasFetchedSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props: Props) {
|
||||
const {
|
||||
savedSubscriptions,
|
||||
hasFetchedSubscriptions,
|
||||
setHasFetchedSubscriptions,
|
||||
} = props;
|
||||
|
||||
if (!hasFetchedSubscriptions && savedSubscriptions.length) {
|
||||
this.fetchSubscriptions(savedSubscriptions);
|
||||
setHasFetchedSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
fetchSubscriptions(savedSubscriptions: SavedSubscriptions) {
|
||||
const { doFetchClaimsByChannel } = this.props;
|
||||
if (savedSubscriptions.length) {
|
||||
// can this use batchActions?
|
||||
savedSubscriptions.forEach(sub => {
|
||||
doFetchClaimsByChannel(sub.uri, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { subscriptions, savedSubscriptions } = this.props;
|
||||
|
||||
let someClaimsNotLoaded;
|
||||
for (var i = 0; i < subscriptions.length; i++) {
|
||||
const subscription = subscriptions[i];
|
||||
if (!subscription.claims.length) {
|
||||
someClaimsNotLoaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchingSubscriptions =
|
||||
!!savedSubscriptions.length &&
|
||||
(subscriptions.length !== savedSubscriptions.length ||
|
||||
someClaimsNotLoaded);
|
||||
|
||||
return (
|
||||
<main className="main main--no-margin">
|
||||
<SubHeader fullWidth smallMargin />
|
||||
{!savedSubscriptions.length && (
|
||||
<span>{__("You haven't subscribed to any channels yet")}</span>
|
||||
)}
|
||||
{fetchingSubscriptions && (
|
||||
<div className="card-row__placeholder">
|
||||
<BusyMessage message={__("Fetching subscriptions")} />
|
||||
</div>
|
||||
)}
|
||||
{!!savedSubscriptions.length && (
|
||||
<div>
|
||||
{!!subscriptions.length &&
|
||||
subscriptions.map(subscription => {
|
||||
if (!subscription.claims.length) {
|
||||
// will need to update when you can subscribe to empty channels
|
||||
// for now this prevents issues with FeaturedCategory being rendered
|
||||
// before the names (claim uris) are populated
|
||||
return "";
|
||||
}
|
||||
|
||||
// creating uris for each subscription file
|
||||
const names = subscription.claims.slice().map(claim => {
|
||||
return `${claim.name}#${claim.claim_id}`;
|
||||
});
|
||||
|
||||
return (
|
||||
<FeaturedCategory
|
||||
key={subscription.channelName}
|
||||
categoryLink={`lbry://${subscription.uri}`}
|
||||
category={subscription.channelName}
|
||||
names={names}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
26
src/renderer/redux/actions/subscriptions.js
Normal file
26
src/renderer/redux/actions/subscriptions.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
import * as actions from "constants/action_types";
|
||||
import type { Subscription, Action, Dispatch } from "redux/reducers/subscriptions";
|
||||
import lbry from "lbry";
|
||||
|
||||
export const channelSubscribe = (subscription: Subscription) => (
|
||||
dispatch: Dispatch
|
||||
) => {
|
||||
return dispatch({
|
||||
type: actions.CHANNEL_SUBSCRIBE,
|
||||
data: subscription,
|
||||
});
|
||||
};
|
||||
|
||||
export const channelUnsubscribe = (subscription: Subscription) => (
|
||||
dispatch: Dispatch
|
||||
) => {
|
||||
return dispatch({
|
||||
type: actions.CHANNEL_UNSUBSCRIBE,
|
||||
data: subscription,
|
||||
});
|
||||
};
|
||||
|
||||
export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => {
|
||||
return dispatch({ type: actions.HAS_FETCHED_SUBSCRIPTIONS })
|
||||
}
|
80
src/renderer/redux/reducers/subscriptions.js
Normal file
80
src/renderer/redux/reducers/subscriptions.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
// @flow
|
||||
import * as actions from "constants/action_types";
|
||||
import { handleActions } from "util/redux-utils";
|
||||
|
||||
// Subscription redux types
|
||||
export type SubscriptionState = {
|
||||
subscriptions: Array<Subscription>,
|
||||
hasFetchedSubscriptions: boolean
|
||||
};
|
||||
|
||||
export type Subscription = {
|
||||
channelName: string,
|
||||
uri: string,
|
||||
};
|
||||
|
||||
// Subscription action types
|
||||
type ChannelSubscribe = {
|
||||
type: actions.CHANNEL_SUBSCRIBE,
|
||||
data: Subscription,
|
||||
};
|
||||
|
||||
type ChannelUnsubscribe = {
|
||||
type: actions.CHANNEL_UNSUBSCRIBE,
|
||||
data: Subscription,
|
||||
};
|
||||
|
||||
type HasFetchedSubscriptions = {
|
||||
type: actions.HAS_FETCHED_SUBSCRIPTIONS
|
||||
}
|
||||
|
||||
export type Action = ChannelSubscribe | ChannelUnsubscribe | HasFetchedSubscriptions;
|
||||
export type Dispatch = (action: Action) => any;
|
||||
|
||||
const defaultState = {
|
||||
subscriptions: [],
|
||||
hasFetchedSubscriptions: false
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[actions.CHANNEL_SUBSCRIBE]: (
|
||||
state: SubscriptionState,
|
||||
action: ChannelSubscribe
|
||||
): SubscriptionState => {
|
||||
const newSubscription: Subscription = action.data;
|
||||
let newSubscriptions: Array<Subscription> = state.subscriptions.slice();
|
||||
newSubscriptions.unshift(newSubscription);
|
||||
|
||||
return {
|
||||
...state,
|
||||
subscriptions: newSubscriptions,
|
||||
};
|
||||
},
|
||||
[actions.CHANNEL_UNSUBSCRIBE]: (
|
||||
state: SubscriptionState,
|
||||
action: ChannelUnsubscribe
|
||||
): SubscriptionState => {
|
||||
const subscriptionToRemove: Subscription = action.data;
|
||||
|
||||
const newSubscriptions = state.subscriptions
|
||||
.slice()
|
||||
.filter(subscription => {
|
||||
return subscription.channelName !== subscriptionToRemove.channelName;
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
subscriptions: newSubscriptions,
|
||||
};
|
||||
},
|
||||
[actions.HAS_FETCHED_SUBSCRIPTIONS]: (
|
||||
state: SubscriptionState,
|
||||
action: HasFetchedSubscriptions
|
||||
): SubscriptionState => ({
|
||||
...state,
|
||||
hasFetchedSubscriptions: true
|
||||
})
|
||||
},
|
||||
defaultState
|
||||
);
|
|
@ -61,6 +61,12 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => {
|
|||
settings: __("Settings"),
|
||||
help: __("Help"),
|
||||
};
|
||||
case "discover":
|
||||
case "subscriptions":
|
||||
return {
|
||||
discover: __("Home"),
|
||||
subscriptions: __("Subscriptions"),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -111,6 +117,8 @@ export const selectPageTitle = createSelector(
|
|||
return params.query
|
||||
? __("Search results for %s", params.query)
|
||||
: __("Search");
|
||||
case "subscriptions":
|
||||
return __("Your Subscriptions");
|
||||
case "discover":
|
||||
case false:
|
||||
case null:
|
||||
|
|
|
@ -77,6 +77,8 @@ export const selectWunderBarIcon = createSelector(
|
|||
case "discover":
|
||||
case "search":
|
||||
return "icon-search";
|
||||
case "subscriptions":
|
||||
return "icon-th-list";
|
||||
default:
|
||||
return "icon-file";
|
||||
}
|
||||
|
|
51
src/renderer/redux/selectors/subscriptions.js
Normal file
51
src/renderer/redux/selectors/subscriptions.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import * as settings from "constants/settings";
|
||||
import { createSelector } from "reselect";
|
||||
import { selectAllClaimsByChannel, selectClaimsById } from "./claims";
|
||||
|
||||
// get the entire subscriptions state
|
||||
const _selectState = state => state.subscriptions || {};
|
||||
|
||||
// list of saved channel names and uris
|
||||
export const selectSubscriptions = createSelector(
|
||||
_selectState,
|
||||
state => state.subscriptions
|
||||
);
|
||||
|
||||
export const selectSubscriptionsFromClaims = createSelector(
|
||||
selectAllClaimsByChannel,
|
||||
selectClaimsById,
|
||||
selectSubscriptions,
|
||||
(channelIds, allClaims, savedSubscriptions) => {
|
||||
// no claims loaded yet
|
||||
if (!Object.keys(channelIds).length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let fetchedSubscriptions = [];
|
||||
|
||||
savedSubscriptions.forEach(subscription => {
|
||||
let channelClaims = [];
|
||||
|
||||
// if subscribed channel has content
|
||||
if (channelIds[subscription.uri]) {
|
||||
// This will need to be more robust, we will want to be able to load more than the first page
|
||||
const pageOneChannelIds = channelIds[subscription.uri]["1"];
|
||||
|
||||
// we have the channel ids and the corresponding claims
|
||||
// loop over the list of ids and grab the claim
|
||||
pageOneChannelIds.forEach(id => {
|
||||
const grabbedClaim = allClaims[id];
|
||||
channelClaims.push(grabbedClaim);
|
||||
});
|
||||
}
|
||||
|
||||
fetchedSubscriptions.push({
|
||||
claims: channelClaims,
|
||||
channelName: subscription.channelName,
|
||||
uri: subscription.uri,
|
||||
});
|
||||
});
|
||||
|
||||
return fetchedSubscriptions;
|
||||
}
|
||||
);
|
|
@ -62,6 +62,11 @@ body
|
|||
{
|
||||
width: $width-page-constrained;
|
||||
}
|
||||
|
||||
main.main--no-margin {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reloading {
|
||||
|
|
|
@ -233,6 +233,10 @@ $font-size-subtext-multiple: 0.82;
|
|||
}
|
||||
}
|
||||
|
||||
.card-row__placeholder {
|
||||
padding-bottom: $spacing-vertical;
|
||||
}
|
||||
|
||||
$padding-top-card-hover-hack: 20px;
|
||||
$padding-right-card-hover-hack: 30px;
|
||||
|
||||
|
|
|
@ -50,8 +50,17 @@ nav.sub-header
|
|||
color: var(--tab-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.sub-header--full-width {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.sub-header--small-margin {
|
||||
margin-bottom: $spacing-vertical;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes activeTab {
|
||||
from {width: 0;}
|
||||
to {width: 100%;}
|
||||
|
|
|
@ -12,16 +12,15 @@ import settingsReducer from "redux/reducers/settings";
|
|||
import userReducer from "redux/reducers/user";
|
||||
import walletReducer from "redux/reducers/wallet";
|
||||
import shapeShiftReducer from "redux/reducers/shape_shift";
|
||||
import subscriptionsReducer from "redux/reducers/subscriptions";
|
||||
import { persistStore, autoRehydrate } from "redux-persist";
|
||||
import createCompressor from "redux-persist-transform-compress";
|
||||
import createFilter from "redux-persist-transform-filter";
|
||||
//import { REHYDRATE } from "redux-persist/constants";
|
||||
//import createActionBuffer from "redux-action-buffer";
|
||||
import localForage from "localforage";
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from "redux";
|
||||
import thunk from "redux-thunk";
|
||||
|
||||
const localForage = require("localforage");
|
||||
const redux = require("redux");
|
||||
const thunk = require("redux-thunk").default;
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
const env = process.env.NODE_ENV || "production"
|
||||
|
||||
function isFunction(object) {
|
||||
return typeof object === "function";
|
||||
|
@ -55,7 +54,7 @@ function enableBatching(reducer) {
|
|||
};
|
||||
}
|
||||
|
||||
const reducers = redux.combineReducers({
|
||||
const reducers = combineReducers({
|
||||
app: appReducer,
|
||||
navigation: navigationReducer,
|
||||
availability: availabilityReducer,
|
||||
|
@ -69,6 +68,7 @@ const reducers = redux.combineReducers({
|
|||
wallet: walletReducer,
|
||||
user: userReducer,
|
||||
shapeShift: shapeShiftReducer,
|
||||
subscriptions: subscriptionsReducer,
|
||||
});
|
||||
|
||||
const bulkThunk = createBulkThunkMiddleware();
|
||||
|
@ -81,26 +81,36 @@ if (env === "development") {
|
|||
middleware.push(logger);
|
||||
}
|
||||
|
||||
// middleware.push(createActionBuffer(REHYDRATE)); // was causing issues with authentication reducers not firing
|
||||
const composeEnhancers =
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || redux.compose;
|
||||
const createStoreWithMiddleware = composeEnhancers(
|
||||
autoRehydrate(),
|
||||
redux.applyMiddleware(...middleware)
|
||||
)(redux.createStore);
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const store = createStore(
|
||||
enableBatching(reducers),
|
||||
{}, // initial state
|
||||
composeEnhancers(
|
||||
autoRehydrate({
|
||||
log: env === "development",
|
||||
}),
|
||||
applyMiddleware(...middleware)
|
||||
)
|
||||
);
|
||||
|
||||
const reduxStore = createStoreWithMiddleware(enableBatching(reducers));
|
||||
const compressor = createCompressor();
|
||||
const saveClaimsFilter = createFilter("claims", ["byId", "claimsByUri"]);
|
||||
const subscriptionsFilter = createFilter("subscriptions", ["subscriptions"]);
|
||||
|
||||
const persistOptions = {
|
||||
whitelist: ["claims"],
|
||||
whitelist: ["claims", "subscriptions"],
|
||||
// Order is important. Needs to be compressed last or other transforms can't
|
||||
// read the data
|
||||
transforms: [saveClaimsFilter, compressor],
|
||||
transforms: [saveClaimsFilter, subscriptionsFilter, compressor],
|
||||
debounce: 10000,
|
||||
storage: localForage,
|
||||
};
|
||||
window.cacheStore = persistStore(reduxStore, persistOptions);
|
||||
|
||||
export default reduxStore;
|
||||
window.cacheStore = persistStore(store, persistOptions, err => {
|
||||
if (err) {
|
||||
console.error("Unable to load saved settings");
|
||||
}
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
// util for creating reducers
|
||||
// based off of redux-actions
|
||||
// https://redux-actions.js.org/docs/api/handleAction.html#handleactions
|
||||
|
||||
export const handleActions = (actionMap, defaultState) => {
|
||||
return (state = defaultState, action) => {
|
||||
const handler = actionMap[action.type];
|
||||
const newState = handler ? handler(state, action) : {};
|
||||
return Object.assign({}, state, newState);
|
||||
|
||||
if (handler) {
|
||||
const newState = handler(state, action);
|
||||
return Object.assign({}, state, newState);
|
||||
}
|
||||
|
||||
// just return the original state if no handler
|
||||
// returning a copy here breaks redux-persist
|
||||
return state;
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue