diff --git a/.flowconfig b/.flowconfig index 5b32cc4ce..899a096e6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -16,5 +16,7 @@ module.name_mapper='^util\(.*\)$' -> '/src/renderer/util\1' module.name_mapper='^redux\(.*\)$' -> '/src/renderer/redux\1' module.name_mapper='^types\(.*\)$' -> '/src/renderer/types\1' module.name_mapper='^component\(.*\)$' -> '/src/renderer/component\1' +module.name_mapper='^page\(.*\)$' -> '/src/renderer/page\1' +module.name_mapper='^lbry\(.*\)$' -> '/src/renderer/lbry\1' [strict] diff --git a/.gitignore b/.gitignore index 2a5b30f19..e20de7b15 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /src/main/dist /src/main/locales /src/main/node_modules +/src/renderer/dist /build/venv /build/daemon.ver /lbry-app-venv diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a98bc7a..d02405ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/renderer/component/header/view.jsx b/src/renderer/component/header/view.jsx index 7bbf9899e..1e83abbe7 100644 --- a/src/renderer/component/header/view.jsx +++ b/src/renderer/component/header/view.jsx @@ -41,6 +41,13 @@ export const Header = props => { title={__("Discover Content")} /> +
+ navigate("/subscriptions")} + button="alt button--flat" + icon="icon-at" + /> +
diff --git a/src/renderer/component/router/view.jsx b/src/renderer/component/router/view.jsx index 1c594783e..4a8a82c70 100644 --- a/src/renderer/component/router/view.jsx +++ b/src/renderer/component/router/view.jsx @@ -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: , show: , wallet: , + subscriptions: , }); }; diff --git a/src/renderer/component/subHeader/view.jsx b/src/renderer/component/subHeader/view.jsx index fae008270..5a3b09d5f 100644 --- a/src/renderer/component/subHeader/view.jsx +++ b/src/renderer/component/subHeader/view.jsx @@ -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 ( diff --git a/src/renderer/component/subscribeButton/index.js b/src/renderer/component/subscribeButton/index.js new file mode 100644 index 000000000..d79e53052 --- /dev/null +++ b/src/renderer/component/subscribeButton/index.js @@ -0,0 +1,16 @@ +import { connect } from "react-redux"; +import { + doChannelSubscribe, + doChannelUnsubscribe, +} from "redux/actions/subscriptions"; +import { selectSubscriptions } from "redux/selectors/subscriptions";; +import SubscribeButton from "./view"; + +const select = (state, props) => ({ + subscriptions: selectSubscriptions(state), +}); + +export default connect(select, { + doChannelSubscribe, + doChannelUnsubscribe +})(SubscribeButton); diff --git a/src/renderer/component/subscribeButton/view.jsx b/src/renderer/component/subscribeButton/view.jsx new file mode 100644 index 000000000..cd93cac91 --- /dev/null +++ b/src/renderer/component/subscribeButton/view.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import Link from "component/link"; + +export default ({ + channelName, + uri, + subscriptions, + doChannelSubscribe, + doChannelUnsubscribe + }) => { + + const isSubscribed = + subscriptions + .map(subscription => subscription.channelName) + .indexOf(channelName) !== -1; + + const subscriptionHandler = isSubscribed + ? doChannelUnsubscribe + : doChannelSubscribe; + + const subscriptionLabel = isSubscribed ? __("Unsubscribe") : __("Subscribe"); + + return channelName && uri ? ( +
+ subscriptionHandler({ + channelName, + uri, + })} + /> +
+ ) : ""; +} diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index 3670912fd..f9e8eb836 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -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"; diff --git a/src/renderer/flow-typed/reselect.js b/src/renderer/flow-typed/reselect.js new file mode 100644 index 000000000..9ee4a6a7f --- /dev/null +++ b/src/renderer/flow-typed/reselect.js @@ -0,0 +1,3 @@ +declare module 'reselect' { + declare module.exports: any; +} diff --git a/src/renderer/page/channel/view.jsx b/src/renderer/page/channel/view.jsx index 805ebc2d4..94b726bcd 100644 --- a/src/renderer/page/channel/view.jsx +++ b/src/renderer/page/channel/view.jsx @@ -3,6 +3,8 @@ import lbryuri from "lbryuri"; import { BusyMessage } from "component/common"; import FileTile from "component/fileTile"; import ReactPaginate from "react-paginate"; +import Link from "component/link"; +import SubscribeButton from "component/subscribeButton"; class ChannelPage extends React.PureComponent { componentDidMount() { @@ -38,8 +40,17 @@ class ChannelPage extends React.PureComponent { uri, page, totalPages, + doChannelSubscribe, + doChannelUnsubscribe, + subscriptions, } = this.props; + const { name, claim_id: claimId } = claim; + const subscriptionUri = lbryuri.build( + { channelName: name, claimId }, + false + ); + let contentList; if (fetching) { contentList = ; @@ -68,6 +79,7 @@ class ChannelPage extends React.PureComponent {

{uri}

+

diff --git a/src/renderer/page/discover/view.jsx b/src/renderer/page/discover/view.jsx index f8ea424ab..8cb0da45e 100644 --- a/src/renderer/page/discover/view.jsx +++ b/src/renderer/page/discover/view.jsx @@ -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 (

- {category} + {categoryLink ? ( + + ) : ( + category + )} + {category && category.match(/^community/i) && ( +
+ {!hasContent && fetchingFeaturedUris && ( )} {hasContent && - Object.keys(featuredUris).map( - category => - featuredUris[category].length ? ( - - ) : ( - "" - ) - )} + Object.keys(featuredUris).map(category => { + return featuredUris[category].length ? ( + + ) : ( + "" + ); + })} {failedToLoad && (
{__("Failed to load landing content.")}
)} diff --git a/src/renderer/page/file/index.js b/src/renderer/page/file/index.js index 2e5373c25..ba1fc3398 100644 --- a/src/renderer/page/file/index.js +++ b/src/renderer/page/file/index.js @@ -1,4 +1,3 @@ -import React from "react"; import { connect } from "react-redux"; import { doNavigate } from "redux/actions/navigation"; import { doFetchFileInfo } from "redux/actions/file_info"; @@ -24,6 +23,7 @@ const select = (state, props) => ({ tab: makeSelectCurrentParam("tab")(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), rewardedContentClaimIds: selectRewardContentClaimIds(state, props), + }); const perform = dispatch => ({ diff --git a/src/renderer/page/file/view.jsx b/src/renderer/page/file/view.jsx index abe538fd8..6b91cd138 100644 --- a/src/renderer/page/file/view.jsx +++ b/src/renderer/page/file/view.jsx @@ -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,8 @@ 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"; +import SubscribeButton from "component/subscribeButton"; class FilePage extends React.PureComponent { componentDidMount() { @@ -52,7 +54,6 @@ class FilePage extends React.PureComponent { ); } - const { height } = claim; const title = metadata.title; const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id); const mediaType = lbry.getMediaType(contentType); @@ -61,6 +62,19 @@ class FilePage extends React.PureComponent { const isPlayable = Object.values(player.mime).indexOf(contentType) !== -1 || mediaType === "audio"; + const { height, channel_name: channelName, value } = claim; + const channelClaimId = + value && + value.publisherSignature && + value.publisherSignature.certificateId; + + let subscriptionUri; + if (channelName && channelClaimId) { + subscriptionUri = lbryuri.build( + { channelName, claimId: channelClaimId }, + false + ); + } return (
@@ -98,6 +112,7 @@ class FilePage extends React.PureComponent {

+ )} diff --git a/src/renderer/page/subscriptions/index.js b/src/renderer/page/subscriptions/index.js new file mode 100644 index 000000000..dae4925cb --- /dev/null +++ b/src/renderer/page/subscriptions/index.js @@ -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); diff --git a/src/renderer/page/subscriptions/view.jsx b/src/renderer/page/subscriptions/view.jsx new file mode 100644 index 000000000..94f17cd4f --- /dev/null +++ b/src/renderer/page/subscriptions/view.jsx @@ -0,0 +1,102 @@ +// @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; + +type Props = { + doFetchClaimsByChannel: (string, number) => any, + savedSubscriptions: SavedSubscriptions, + // TODO build out claim types + subscriptions: Array, + setHasFetchedSubscriptions: () => void, + hasFetchedSubscriptions: boolean, +}; + +export default class extends React.PureComponent { + // 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; + + const someClaimsNotLoaded = Boolean(subscriptions.find(subscription => !subscription.claims.length)) + + const fetchingSubscriptions = + !!savedSubscriptions.length && + (subscriptions.length !== savedSubscriptions.length || + someClaimsNotLoaded); + + return ( +
+ + {!savedSubscriptions.length && ( + {__("You haven't subscribed to any channels yet")} + )} + {fetchingSubscriptions && ( +
+ +
+ )} + {!!savedSubscriptions.length && ( +
+ {!!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 ""; + } + + return ( + + ); + })} +
+ )} +
+ ); + } +} diff --git a/src/renderer/redux/actions/subscriptions.js b/src/renderer/redux/actions/subscriptions.js new file mode 100644 index 000000000..6ec76f2bd --- /dev/null +++ b/src/renderer/redux/actions/subscriptions.js @@ -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 doChannelSubscribe = (subscription: Subscription) => ( + dispatch: Dispatch +) => { + return dispatch({ + type: actions.CHANNEL_SUBSCRIBE, + data: subscription, + }); +}; + +export const doChannelUnsubscribe = (subscription: Subscription) => ( + dispatch: Dispatch +) => { + return dispatch({ + type: actions.CHANNEL_UNSUBSCRIBE, + data: subscription, + }); +}; + +export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => { + return dispatch({ type: actions.HAS_FETCHED_SUBSCRIPTIONS }) +} diff --git a/src/renderer/redux/reducers/subscriptions.js b/src/renderer/redux/reducers/subscriptions.js new file mode 100644 index 000000000..23e9ef50e --- /dev/null +++ b/src/renderer/redux/reducers/subscriptions.js @@ -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, + hasFetchedSubscriptions: boolean +}; + +export type Subscription = { + channelName: string, + uri: string, +}; + +// Subscription action types +type doChannelSubscribe = { + type: actions.CHANNEL_SUBSCRIBE, + data: Subscription, +}; + +type doChannelUnsubscribe = { + type: actions.CHANNEL_UNSUBSCRIBE, + data: Subscription, +}; + +type HasFetchedSubscriptions = { + type: actions.HAS_FETCHED_SUBSCRIPTIONS +} + +export type Action = doChannelSubscribe | doChannelUnsubscribe | HasFetchedSubscriptions; +export type Dispatch = (action: Action) => any; + +const defaultState = { + subscriptions: [], + hasFetchedSubscriptions: false +}; + +export default handleActions( + { + [actions.CHANNEL_SUBSCRIBE]: ( + state: SubscriptionState, + action: doChannelSubscribe + ): SubscriptionState => { + const newSubscription: Subscription = action.data; + let newSubscriptions: Array = state.subscriptions.slice(); + newSubscriptions.unshift(newSubscription); + + return { + ...state, + subscriptions: newSubscriptions, + }; + }, + [actions.CHANNEL_UNSUBSCRIBE]: ( + state: SubscriptionState, + action: doChannelUnsubscribe + ): 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 +); diff --git a/src/renderer/redux/selectors/navigation.js b/src/renderer/redux/selectors/navigation.js index 7f78f7b13..2b1b384c9 100644 --- a/src/renderer/redux/selectors/navigation.js +++ b/src/renderer/redux/selectors/navigation.js @@ -61,6 +61,12 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => { settings: __("Settings"), help: __("Help"), }; + case "discover": + case "subscriptions": + return { + discover: __("Discover"), + 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: diff --git a/src/renderer/redux/selectors/search.js b/src/renderer/redux/selectors/search.js index 259971a9e..ad522ec19 100644 --- a/src/renderer/redux/selectors/search.js +++ b/src/renderer/redux/selectors/search.js @@ -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"; } diff --git a/src/renderer/redux/selectors/subscriptions.js b/src/renderer/redux/selectors/subscriptions.js new file mode 100644 index 000000000..5694c8f47 --- /dev/null +++ b/src/renderer/redux/selectors/subscriptions.js @@ -0,0 +1,56 @@ +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); + }); + } + + // all we really need is a uri for each claim + channelClaims = channelClaims.map(claim => { + return `${claim.name}#${claim.claim_id}`; + }) + + fetchedSubscriptions.push({ + claims: channelClaims, + channelName: subscription.channelName, + uri: subscription.uri, + }); + }); + + return fetchedSubscriptions; + } +); diff --git a/src/renderer/scss/_gui.scss b/src/renderer/scss/_gui.scss index 4cfc20bdf..fa188a37f 100644 --- a/src/renderer/scss/_gui.scss +++ b/src/renderer/scss/_gui.scss @@ -62,6 +62,11 @@ body { width: $width-page-constrained; } + + main.main--no-margin { + margin-left: 0; + margin-right: 0; + } } .reloading { diff --git a/src/renderer/scss/component/_card.scss b/src/renderer/scss/component/_card.scss index 53d8c6d1e..5d0b5e76a 100644 --- a/src/renderer/scss/component/_card.scss +++ b/src/renderer/scss/component/_card.scss @@ -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; diff --git a/src/renderer/scss/component/_tabs.scss b/src/renderer/scss/component/_tabs.scss index d2860aab0..c990c8adf 100644 --- a/src/renderer/scss/component/_tabs.scss +++ b/src/renderer/scss/component/_tabs.scss @@ -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%;} diff --git a/src/renderer/store.js b/src/renderer/store.js index 586411687..9e6176b0e 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -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; diff --git a/src/renderer/util/redux-utils.js b/src/renderer/util/redux-utils.js index 17aa4fdaf..04aa0c3cc 100644 --- a/src/renderer/util/redux-utils.js +++ b/src/renderer/util/redux-utils.js @@ -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; }; };