persisting subscriptions

This commit is contained in:
Sean Yesmunt 2017-12-08 15:14:35 -05:00
parent f1f3d311e9
commit a7fe02ea98
27 changed files with 180186 additions and 45 deletions

View file

@ -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='^redux\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/redux\1'
module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/types\1' module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/types\1'
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\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] [strict]

View file

@ -9,6 +9,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### Added
* ShapeShift integration * ShapeShift integration
* In-app subscriptions
### Changed ### Changed
* The first run process for new users has changed substantially. New users can now easily receive one credit. * The first run process for new users has changed substantially. New users can now easily receive one credit.

View file

@ -41,6 +41,13 @@ export const Header = props => {
title={__("Discover Content")} title={__("Discover Content")}
/> />
</div> </div>
<div className="header__item">
<Link
onClick={() => navigate("/subscriptions")}
button="alt button--flat"
icon="icon-at"
/>
</div>
<div className="header__item header__item--wunderbar"> <div className="header__item header__item--wunderbar">
<WunderBar /> <WunderBar />
</div> </div>

View file

@ -17,6 +17,7 @@ import SearchPage from "page/search";
import AuthPage from "page/auth"; import AuthPage from "page/auth";
import InvitePage from "page/invite"; import InvitePage from "page/invite";
import BackupPage from "page/backup"; import BackupPage from "page/backup";
import SubscriptionsPage from "page/subscriptions";
const route = (page, routesMap) => { const route = (page, routesMap) => {
const component = routesMap[page]; const component = routesMap[page];
@ -46,6 +47,7 @@ const Router = props => {
settings: <SettingsPage params={params} />, settings: <SettingsPage params={params} />,
show: <ShowPage {...params} />, show: <ShowPage {...params} />,
wallet: <WalletPage params={params} />, wallet: <WalletPage params={params} />,
subscriptions: <SubscriptionsPage params={params} />,
}); });
}; };

View file

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import Link from "component/link"; import Link from "component/link";
import classnames from "classnames";
const SubHeader = props => { const SubHeader = props => {
const { subLinks, currentPage, navigate, modifier } = props; const { subLinks, currentPage, navigate, fullWidth, smallMargin } = props;
const links = []; const links = [];
@ -22,7 +23,10 @@ const SubHeader = props => {
return ( return (
<nav <nav
className={"sub-header" + (modifier ? " sub-header--" + modifier : "")} className={classnames("sub-header", {
"sub-header--full-width": fullWidth,
"sub-header--small-margin": smallMargin,
})}
> >
{links} {links}
</nav> </nav>

View file

@ -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_SUCCESS = "GET_ACTIVE_SHIFT_SUCCESS";
export const GET_ACTIVE_SHIFT_FAIL = "GET_ACTIVE_SHIFT_FAIL"; export const GET_ACTIVE_SHIFT_FAIL = "GET_ACTIVE_SHIFT_FAIL";
export const CLEAR_SHAPE_SHIFT = "CLEAR_SHAPE_SHIFT"; 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

File diff suppressed because it is too large Load diff

0
src/renderer/dist/css/mixin/link.css vendored Normal file
View 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
View file

@ -0,0 +1,3 @@
declare module 'reselect' {
declare module.exports: any;
}

View file

@ -15,6 +15,11 @@ import {
} from "redux/selectors/navigation"; } from "redux/selectors/navigation";
import { doNavigate } from "redux/actions/navigation"; import { doNavigate } from "redux/actions/navigation";
import { makeSelectTotalPagesForChannel } from "redux/selectors/content"; import { makeSelectTotalPagesForChannel } from "redux/selectors/content";
import { selectSubscriptions } from "redux/selectors/subscriptions";
import {
channelSubscribe,
channelUnsubscribe,
} from "redux/actions/subscriptions";
import ChannelPage from "./view"; import ChannelPage from "./view";
const select = (state, props) => ({ const select = (state, props) => ({
@ -24,12 +29,16 @@ const select = (state, props) => ({
page: makeSelectCurrentParam("page")(state), page: makeSelectCurrentParam("page")(state),
params: selectCurrentParams(state), params: selectCurrentParams(state),
totalPages: makeSelectTotalPagesForChannel(props.uri)(state), totalPages: makeSelectTotalPagesForChannel(props.uri)(state),
subscriptions: selectSubscriptions(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)), fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
fetchClaimCount: uri => dispatch(doFetchClaimCountByChannel(uri)), fetchClaimCount: uri => dispatch(doFetchClaimCountByChannel(uri)),
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),
channelSubscribe: subscription => dispatch(channelSubscribe(subscription)),
channelUnsubscribe: subscription =>
dispatch(channelUnsubscribe(subscription)),
}); });
export default connect(select, perform)(ChannelPage); export default connect(select, perform)(ChannelPage);

View file

@ -3,6 +3,7 @@ import lbryuri from "lbryuri";
import { BusyMessage } from "component/common"; import { BusyMessage } from "component/common";
import FileTile from "component/fileTile"; import FileTile from "component/fileTile";
import ReactPaginate from "react-paginate"; import ReactPaginate from "react-paginate";
import Link from "component/link";
class ChannelPage extends React.PureComponent { class ChannelPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
@ -38,8 +39,28 @@ class ChannelPage extends React.PureComponent {
uri, uri,
page, page,
totalPages, totalPages,
channelSubscribe,
channelUnsubscribe,
subscriptions,
} = this.props; } = 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; let contentList;
if (fetching) { if (fetching) {
contentList = <BusyMessage message={__("Fetching content")} />; contentList = <BusyMessage message={__("Fetching content")} />;
@ -68,6 +89,18 @@ class ChannelPage extends React.PureComponent {
<div className="card__title-identity"> <div className="card__title-identity">
<h1>{uri}</h1> <h1>{uri}</h1>
</div> </div>
<div className="card__actions">
<Link
button="primary"
label={subscriptionLabel}
onClick={() =>
subscriptionHandler({
channelName: name,
uri: subscriptionUri,
})
}
/>
</div>
</div> </div>
<div className="card__content"> <div className="card__content">
<p className="empty"> <p className="empty">

View file

@ -4,16 +4,43 @@ import lbryuri from "lbryuri";
import FileCard from "component/fileCard"; import FileCard from "component/fileCard";
import { Icon, BusyMessage } from "component/common.js"; import { Icon, BusyMessage } from "component/common.js";
import ToolTip from "component/tooltip.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() { componentWillMount() {
this.setState({ this.setState({
numItems: this.props.names.length, 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() { handleScrollPrevious() {
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems); const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
if (cardRow.scrollLeft > 0) { if (cardRow.scrollLeft > 0) {
@ -148,12 +175,21 @@ class FeaturedCategory extends React.PureComponent {
} }
render() { render() {
const { category, names } = this.props; const { category, names, categoryLink } = this.props;
return ( return (
<div className="card-row card-row--small"> <div className="card-row card-row--small">
<h3 className="card-row__header"> <h3 className="card-row__header">
{category} {categoryLink ? (
<Link
className="no-underline"
label={category}
href={categoryLink}
/>
) : (
category
)}
{category && {category &&
category.match(/^community/i) && ( category.match(/^community/i) && (
<ToolTip <ToolTip
@ -214,24 +250,28 @@ class DiscoverPage extends React.PureComponent {
failedToLoad = !fetchingFeaturedUris && !hasContent; failedToLoad = !fetchingFeaturedUris && !hasContent;
return ( return (
<main className={hasContent && fetchingFeaturedUris ? "reloading" : null}> <main
className={classnames("main main--no-margin", {
reloading: hasContent && fetchingFeaturedUris,
})}
>
<SubHeader fullWidth smallMargin />
{!hasContent && {!hasContent &&
fetchingFeaturedUris && ( fetchingFeaturedUris && (
<BusyMessage message={__("Fetching content")} /> <BusyMessage message={__("Fetching content")} />
)} )}
{hasContent && {hasContent &&
Object.keys(featuredUris).map( Object.keys(featuredUris).map(category => {
category => return featuredUris[category].length ? (
featuredUris[category].length ? ( <FeaturedCategory
<FeaturedCategory key={category}
key={category} category={category}
category={category} names={featuredUris[category]}
names={featuredUris[category]} />
/> ) : (
) : ( ""
"" );
) })}
)}
{failedToLoad && ( {failedToLoad && (
<div className="empty">{__("Failed to load landing content.")}</div> <div className="empty">{__("Failed to load landing content.")}</div>
)} )}

View file

@ -2,8 +2,13 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doNavigate } from "redux/actions/navigation"; import { doNavigate } from "redux/actions/navigation";
import { doFetchFileInfo } from "redux/actions/file_info"; import { doFetchFileInfo } from "redux/actions/file_info";
import {
channelSubscribe,
channelUnsubscribe,
} from "redux/actions/subscriptions";
import { makeSelectFileInfoForUri } from "redux/selectors/file_info"; import { makeSelectFileInfoForUri } from "redux/selectors/file_info";
import { selectRewardContentClaimIds } from "redux/selectors/content"; import { selectRewardContentClaimIds } from "redux/selectors/content";
import { selectSubscriptions } from "redux/selectors/subscriptions";
import { doFetchCostInfoForUri } from "redux/actions/cost_info"; import { doFetchCostInfoForUri } from "redux/actions/cost_info";
import { import {
makeSelectClaimForUri, makeSelectClaimForUri,
@ -24,12 +29,16 @@ const select = (state, props) => ({
tab: makeSelectCurrentParam("tab")(state), tab: makeSelectCurrentParam("tab")(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
subscriptions: selectSubscriptions(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
channelSubscribe: subscription => dispatch(channelSubscribe(subscription)),
channelUnsubscribe: subscription =>
dispatch(channelUnsubscribe(subscription)),
}); });
export default connect(select, perform)(FilePage); export default connect(select, perform)(FilePage);

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import lbry from "lbry.js"; import lbry from "lbry";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri";
import Video from "component/video"; import Video from "component/video";
import { Thumbnail } from "component/common"; import { Thumbnail } from "component/common";
import FilePrice from "component/filePrice"; import FilePrice from "component/filePrice";
@ -10,6 +10,7 @@ import Icon from "component/icon";
import WalletSendTip from "component/walletSendTip"; import WalletSendTip from "component/walletSendTip";
import DateTime from "component/dateTime"; import DateTime from "component/dateTime";
import * as icons from "constants/icons"; import * as icons from "constants/icons";
import Link from "component/link";
class FilePage extends React.PureComponent { class FilePage extends React.PureComponent {
componentDidMount() { componentDidMount() {
@ -42,6 +43,9 @@ class FilePage extends React.PureComponent {
tab, tab,
uri, uri,
rewardedContentClaimIds, rewardedContentClaimIds,
channelSubscribe,
channelUnsubscribe,
subscriptions,
} = this.props; } = this.props;
const showTipBox = tab == "tip"; 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 title = metadata.title;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const mediaType = lbry.getMediaType(contentType); const mediaType = lbry.getMediaType(contentType);
@ -62,6 +66,35 @@ class FilePage extends React.PureComponent {
Object.values(player.mime).indexOf(contentType) !== -1 || Object.values(player.mime).indexOf(contentType) !== -1 ||
mediaType === "audio"; 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 ( return (
<section className={"card " + (obscureNsfw ? "card--obscured " : "")}> <section className={"card " + (obscureNsfw ? "card--obscured " : "")}>
<div className="show-page-media"> <div className="show-page-media">
@ -98,6 +131,20 @@ class FilePage extends React.PureComponent {
</span> </span>
</div> </div>
</div> </div>
{canSubscribe && (
<div className="card__actions">
<Link
button="primary"
onClick={() =>
subscriptionHandler({
channelName,
uri: subscriptionUri,
})
}
label={subscriptionLabel}
/>
</div>
)}
<FileDetails uri={uri} /> <FileDetails uri={uri} />
</div> </div>
)} )}

View 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);

View 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>
);
}
}

View 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 })
}

View 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
);

View file

@ -61,6 +61,12 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => {
settings: __("Settings"), settings: __("Settings"),
help: __("Help"), help: __("Help"),
}; };
case "discover":
case "subscriptions":
return {
discover: __("Home"),
subscriptions: __("Subscriptions"),
};
default: default:
return null; return null;
} }
@ -111,6 +117,8 @@ export const selectPageTitle = createSelector(
return params.query return params.query
? __("Search results for %s", params.query) ? __("Search results for %s", params.query)
: __("Search"); : __("Search");
case "subscriptions":
return __("Your Subscriptions");
case "discover": case "discover":
case false: case false:
case null: case null:

View file

@ -77,6 +77,8 @@ export const selectWunderBarIcon = createSelector(
case "discover": case "discover":
case "search": case "search":
return "icon-search"; return "icon-search";
case "subscriptions":
return "icon-th-list";
default: default:
return "icon-file"; return "icon-file";
} }

View 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;
}
);

View file

@ -62,6 +62,11 @@ body
{ {
width: $width-page-constrained; width: $width-page-constrained;
} }
main.main--no-margin {
margin-left: 0;
margin-right: 0;
}
} }
.reloading { .reloading {

View file

@ -233,6 +233,10 @@ $font-size-subtext-multiple: 0.82;
} }
} }
.card-row__placeholder {
padding-bottom: $spacing-vertical;
}
$padding-top-card-hover-hack: 20px; $padding-top-card-hover-hack: 20px;
$padding-right-card-hover-hack: 30px; $padding-right-card-hover-hack: 30px;

View file

@ -50,8 +50,17 @@ nav.sub-header
color: var(--tab-active-color); color: var(--tab-active-color);
} }
} }
&.sub-header--full-width {
max-width: 100%;
}
&.sub-header--small-margin {
margin-bottom: $spacing-vertical;
}
} }
@keyframes activeTab { @keyframes activeTab {
from {width: 0;} from {width: 0;}
to {width: 100%;} to {width: 100%;}

View file

@ -12,16 +12,15 @@ import settingsReducer from "redux/reducers/settings";
import userReducer from "redux/reducers/user"; import userReducer from "redux/reducers/user";
import walletReducer from "redux/reducers/wallet"; import walletReducer from "redux/reducers/wallet";
import shapeShiftReducer from "redux/reducers/shape_shift"; import shapeShiftReducer from "redux/reducers/shape_shift";
import subscriptionsReducer from "redux/reducers/subscriptions";
import { persistStore, autoRehydrate } from "redux-persist"; import { persistStore, autoRehydrate } from "redux-persist";
import createCompressor from "redux-persist-transform-compress"; import createCompressor from "redux-persist-transform-compress";
import createFilter from "redux-persist-transform-filter"; import createFilter from "redux-persist-transform-filter";
//import { REHYDRATE } from "redux-persist/constants"; import localForage from "localforage";
//import createActionBuffer from "redux-action-buffer"; import { createStore, applyMiddleware, compose, combineReducers } from "redux";
import thunk from "redux-thunk";
const localForage = require("localforage"); const env = process.env.NODE_ENV || "production"
const redux = require("redux");
const thunk = require("redux-thunk").default;
const env = process.env.NODE_ENV || "production";
function isFunction(object) { function isFunction(object) {
return typeof object === "function"; return typeof object === "function";
@ -55,7 +54,7 @@ function enableBatching(reducer) {
}; };
} }
const reducers = redux.combineReducers({ const reducers = combineReducers({
app: appReducer, app: appReducer,
navigation: navigationReducer, navigation: navigationReducer,
availability: availabilityReducer, availability: availabilityReducer,
@ -69,6 +68,7 @@ const reducers = redux.combineReducers({
wallet: walletReducer, wallet: walletReducer,
user: userReducer, user: userReducer,
shapeShift: shapeShiftReducer, shapeShift: shapeShiftReducer,
subscriptions: subscriptionsReducer,
}); });
const bulkThunk = createBulkThunkMiddleware(); const bulkThunk = createBulkThunkMiddleware();
@ -81,26 +81,36 @@ if (env === "development") {
middleware.push(logger); middleware.push(logger);
} }
// middleware.push(createActionBuffer(REHYDRATE)); // was causing issues with authentication reducers not firing const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || redux.compose; const store = createStore(
const createStoreWithMiddleware = composeEnhancers( enableBatching(reducers),
autoRehydrate(), {}, // initial state
redux.applyMiddleware(...middleware) composeEnhancers(
)(redux.createStore); autoRehydrate({
log: env === "development",
}),
applyMiddleware(...middleware)
)
);
const reduxStore = createStoreWithMiddleware(enableBatching(reducers));
const compressor = createCompressor(); const compressor = createCompressor();
const saveClaimsFilter = createFilter("claims", ["byId", "claimsByUri"]); const saveClaimsFilter = createFilter("claims", ["byId", "claimsByUri"]);
const subscriptionsFilter = createFilter("subscriptions", ["subscriptions"]);
const persistOptions = { const persistOptions = {
whitelist: ["claims"], whitelist: ["claims", "subscriptions"],
// 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: [saveClaimsFilter, compressor], transforms: [saveClaimsFilter, subscriptionsFilter, compressor],
debounce: 10000, debounce: 10000,
storage: localForage, 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;

View file

@ -1,11 +1,17 @@
// util for creating reducers // util for creating reducers
// based off of redux-actions // based off of redux-actions
// https://redux-actions.js.org/docs/api/handleAction.html#handleactions // https://redux-actions.js.org/docs/api/handleAction.html#handleactions
export const handleActions = (actionMap, defaultState) => { export const handleActions = (actionMap, defaultState) => {
return (state = defaultState, action) => { return (state = defaultState, action) => {
const handler = actionMap[action.type]; 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;
}; };
}; };