[merge after 34 release] Lbry redux publish WIP #2607

Merged
jessopb merged 5 commits from lbryReduxPublish into master 2019-07-29 20:51:10 +02:00
18 changed files with 61 additions and 503 deletions

View file

@ -124,7 +124,7 @@
"jsmediatags": "^3.8.1",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#ea56de4548480eb1a10b752b3cecbad7de2f8914",
"lbry-redux": "lbryio/lbry-redux#8910693fe1fc4166fdc748d128344012c3d61874",
"lbryinc": "lbryio/lbryinc#a93596c51c8fb0a226cb84df04c26a6bb60a45fb",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",

View file

@ -1,19 +1,18 @@
import { connect } from 'react-redux';
import { doResolveUri, selectBalance } from 'lbry-redux';
import {
doResolveUri,
selectBalance,
selectPublishFormValues,
selectIsStillEditing,
selectMyClaimForUri,
selectIsResolvingPublishUris,
selectTakeOverAmount,
} from 'redux/selectors/publish';
import {
doResetThumbnailStatus,
doClearPublish,
doUpdatePublishForm,
doPublish,
doPrepareEdit,
} from 'redux/actions/publish';
} from 'lbry-redux';
import { doPublishDesktop } from 'redux/actions/publish';
import { selectUnclaimedRewardValue } from 'lbryinc';
import PublishPage from './view';
@ -35,7 +34,7 @@ const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
clearPublish: () => dispatch(doClearPublish()),
resolveUri: uri => dispatch(doResolveUri(uri)),
publish: params => dispatch(doPublish(params)),
publish: () => dispatch(doPublishDesktop()),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
});

View file

@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux';
import { selectIsStillEditing, makeSelectPublishFormValue } from 'redux/selectors/publish';
import { doUpdatePublishForm } from 'redux/actions/publish';
import { selectBalance, selectIsStillEditing, makeSelectPublishFormValue, doUpdatePublishForm } from 'lbry-redux';
import PublishPage from './view';
const select = state => ({

View file

@ -1,19 +1,17 @@
import { connect } from 'react-redux';
import { doResolveUri } from 'lbry-redux';
import {
doResolveUri,
selectPublishFormValues,
selectIsStillEditing,
selectMyClaimForUri,
selectIsResolvingPublishUris,
selectTakeOverAmount,
} from 'redux/selectors/publish';
import {
doResetThumbnailStatus,
doClearPublish,
doUpdatePublishForm,
doPublish,
doPrepareEdit,
} from 'redux/actions/publish';
} from 'lbry-redux';
import { doPublishDesktop } from 'redux/actions/publish';
import { selectUnclaimedRewardValue } from 'lbryinc';
import PublishPage from './view';
@ -34,7 +32,7 @@ const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
neb-b commented 2019-07-09 17:51:45 +02:00 (Migrated from github.com)
Review

We don't need to pass in the callbacks here, lets do it in redux (in the desktop codebase).

We don't need to pass in the callbacks here, lets do it in redux (in the desktop codebase).
clearPublish: () => dispatch(doClearPublish()),
resolveUri: uri => dispatch(doResolveUri(uri)),
publish: params => dispatch(doPublish(params)),
publish: () => dispatch(doPublishDesktop()),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
});

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectPublishFormValue, selectIsStillEditing } from 'redux/selectors/publish';
import { makeSelectPublishFormValue, selectIsStillEditing } from 'lbry-redux';
import PublishPage from './view';
const select = state => ({

View file

@ -5,9 +5,11 @@ import {
selectMyClaimForUri,
selectIsResolvingPublishUris,
selectTakeOverAmount,
} from 'redux/selectors/publish';
import { doUpdatePublishForm, doPrepareEdit } from 'redux/actions/publish';
import { selectBalance } from 'lbry-redux';
doUpdatePublishForm,
doPrepareEdit,
selectBalance,
} from 'lbry-redux';
import PublishPage from './view';
const select = state => ({
@ -15,7 +17,6 @@ const select = state => ({
channel: makeSelectPublishFormValue('channel')(state),
bid: makeSelectPublishFormValue('bid')(state),
uri: makeSelectPublishFormValue('uri')(state),
bid: makeSelectPublishFormValue('bid')(state),
isStillEditing: selectIsStillEditing(state),
isResolvingUri: selectIsResolvingPublishUris(state),
amountNeededForTakeover: selectTakeOverAmount(state),

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux';
import { doUpdatePublishForm } from 'redux/actions/publish';
import { makeSelectPublishFormValue } from 'redux/selectors/publish';
import { makeSelectPublishFormValue, doUpdatePublishForm } from 'lbry-redux';
import PublishPage from './view';
const select = state => ({

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectPublishFormValue } from 'redux/selectors/publish';
import { doUpdatePublishForm } from 'redux/actions/publish';
import { doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux';
import PublishPage from './view';
const select = state => ({

View file

@ -1,20 +1,19 @@
import { connect } from 'react-redux';
import { doResolveUri } from 'lbry-redux';
import {
doResolveUri,
selectPublishFormValues,
selectIsStillEditing,
selectMyClaimForUri,
selectIsResolvingPublishUris,
selectTakeOverAmount,
} from 'redux/selectors/publish';
import {
doResetThumbnailStatus,
doClearPublish,
doUpdatePublishForm,
doPublish,
doPrepareEdit,
} from 'redux/actions/publish';
} from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app';
import { doPublishDesktop } from 'redux/actions/publish';
import { selectUnclaimedRewardValue } from 'lbryinc';
import PublishPage from './view';
@ -35,7 +34,7 @@ const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
clearPublish: () => dispatch(doClearPublish()),
resolveUri: uri => dispatch(doResolveUri(uri)),
publish: params => dispatch(doPublish(params)),
publish: () => dispatch(doPublishDesktop()),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),

View file

@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doUploadThumbnail } from 'redux/actions/publish';
import { doToast } from 'lbry-redux';
import { doToast, doUploadThumbnail } from 'lbry-redux';
import ModalAutoGenerateThumbnail from './view';
const perform = dispatch => ({

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doUploadThumbnail, doUpdatePublishForm } from 'redux/actions/publish';
import { doUploadThumbnail, doUpdatePublishForm } from 'lbry-redux';
import ModalConfirmThumbnailUpload from './view';
const perform = dispatch => ({

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import ModalPublishSuccess from './view';
import { doClearPublish } from 'redux/actions/publish';
import { doClearPublish } from 'lbry-redux';
import { push } from 'connected-react-router';
const perform = dispatch => ({

View file

@ -16,11 +16,11 @@ import {
makeSelectTitleForUri,
makeSelectThumbnailForUri,
makeSelectClaimIsNsfw,
doPrepareEdit,
} from 'lbry-redux';
import { doFetchViewCount, makeSelectViewCountForUri, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowNsfw, makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doPrepareEdit } from 'redux/actions/publish';
import { doOpenModal } from 'redux/actions/app';
import FilePage from './view';

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { selectIsFetchingClaimListMine, selectMyClaimUrisWithoutChannels } from 'lbry-redux';
import { doCheckPendingPublishes } from 'redux/actions/publish';
import { doCheckPendingPublishesApp } from 'redux/actions/publish';
import FileListPublished from './view';
const select = state => ({
@ -9,7 +9,7 @@ const select = state => ({
});
const perform = dispatch => ({
checkPendingPublishes: () => dispatch(doCheckPendingPublishes()),
checkPendingPublishes: () => dispatch(doCheckPendingPublishesApp()),
});
export default connect(

View file

@ -8,6 +8,7 @@ import {
notificationsReducer,
tagsReducer,
commentReducer,
publishReducer,
} from 'lbry-redux';
import {
userReducer,
@ -23,7 +24,7 @@ import availabilityReducer from 'redux/reducers/availability';
import contentReducer from 'redux/reducers/content';
import settingsReducer from 'redux/reducers/settings';
import subscriptionsReducer from 'redux/reducers/subscriptions';
import publishReducer from 'redux/reducers/publish';
export default history =>
combineReducers({

View file

@ -1,319 +1,25 @@
// @flow
import { CC_LICENSES, COPYRIGHT, OTHER, NONE, PUBLIC_DOMAIN } from 'constants/licenses';
import * as MODALS from 'constants/modal_types';
import {
ACTIONS,
Lbry,
selectMyChannelClaims,
THUMBNAIL_STATUSES,
batchActions,
creditsToString,
selectPendingById,
selectMyClaimsWithoutChannels,
doError,
isClaimNsfw,
} from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app';
import { batchActions, doError, selectMyClaims, doPublish, doCheckPendingPublishes } from 'lbry-redux';
import * as ACTIONS from 'constants/action_types';
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
import { selectMyClaimForUri, selectPublishFormValues } from 'redux/selectors/publish';
import { push } from 'connected-react-router';
import analytics from 'analytics';
import { formatLbryUriForWeb } from 'util/uri';
// @if TARGET='app'
import fs from 'fs';
import path from 'path';
// @endif
import { doOpenModal } from './app';
export const doResetThumbnailStatus = () => (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
thumbnailPath: '',
},
});
return fetch('https://spee.ch/api/config/site/publishing')
.then(res => res.json())
.then(status => {
if (status.disabled) {
throw Error();
}
return dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
thumbnail: '',
},
});
})
.catch(() =>
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN,
thumbnail: '',
},
})
);
};
export const doClearPublish = () => (dispatch: Dispatch) => {
dispatch({ type: ACTIONS.CLEAR_PUBLISH });
return dispatch(doResetThumbnailStatus());
};
export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: { ...publishFormValue },
});
export const doUploadThumbnail = (filePath: string, thumbnailBuffer: Uint8Array) => (dispatch: Dispatch) => {
let thumbnail, fileExt, fileName, fileType;
if (filePath) {
thumbnail = fs.readFileSync(filePath);
fileExt = path.extname(filePath);
fileName = path.basename(filePath);
fileType = `image/${fileExt.slice(1)}`;
} else if (thumbnailBuffer) {
thumbnail = thumbnailBuffer;
fileExt = '.png';
fileName = 'thumbnail.png';
fileType = 'image/png';
} else {
return null;
}
const makeid = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
return text;
};
const uploadError = (error = '') =>
dispatch(
batchActions(
{
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
uploadThumbnailStatus: THUMBNAIL_STATUSES.READY,
thumbnail: '',
nsfw: false,
},
},
doError(error)
)
);
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS },
});
const data = new FormData();
const name = makeid();
const file = new File([thumbnail], fileName, { type: fileType });
data.append('name', name);
data.append('file', file);
return fetch('https://spee.ch/api/claim/publish', {
method: 'POST',
body: data,
})
.then(response => response.json())
.then(json =>
json.success
? dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE,
thumbnail: `${json.data.url}${fileExt}`,
},
})
: uploadError(json.message)
)
.catch(err => uploadError(err.message));
};
export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem) => (dispatch: Dispatch) => {
const { name, amount, value } = claim;
const channelName = (claim && claim.signing_channel && claim.signing_channel.normalized_name) || null;
const {
author,
description,
// use same values as default state
// fee will be undefined for free content
fee = {
amount: '0',
currency: 'LBC',
},
languages,
license,
license_url: licenseUrl,
thumbnail,
title,
tags,
} = value;
const publishData: UpdatePublishFormData = {
name,
bid: Number(amount),
contentIsFree: fee.amount === '0',
author,
description,
fee,
languages,
thumbnail: thumbnail ? thumbnail.url : null,
title,
uri,
uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined,
licenseUrl,
nsfw: isClaimNsfw(claim),
tags: tags ? tags.map(tag => ({ name: tag })) : [],
};
if (channelName) {
publishData['channel'] = channelName;
}
// Make sure custom liscence's are mapped properly
// If the license isn't one of the standard licenses, map the custom license and description/url
if (!CC_LICENSES.some(({ value }) => value === license)) {
if (!license || license === NONE || license === PUBLIC_DOMAIN) {
publishData.licenseType = license;
} else if (license && !licenseUrl && license !== NONE) {
publishData.licenseType = COPYRIGHT;
} else {
publishData.licenseType = OTHER;
}
publishData.otherLicenseDescription = license;
} else {
publishData.licenseType = license;
}
if (fileInfo && fileInfo.download_path) {
try {
fs.accessSync(fileInfo.download_path, fs.constants.R_OK);
publishData.filePath = fileInfo.download_path;
} catch (e) {
console.error(e.name, e.message); // eslint-disable-line no-console
}
}
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
};
export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => {
dispatch({ type: ACTIONS.PUBLISH_START });
const state = getState();
const publishData = selectPublishFormValues(state);
const myClaimForUri = selectMyClaimForUri(state);
const myChannels = selectMyChannelClaims(state);
const myClaims = selectMyClaimsWithoutChannels(state);
const {
name,
bid,
filePath,
description,
language,
licenseUrl,
licenseType,
otherLicenseDescription,
thumbnail,
channel,
title,
contentIsFree,
fee,
uri,
tags,
locations,
} = publishData;
let publishingLicense;
switch (licenseType) {
case COPYRIGHT:
case OTHER:
publishingLicense = otherLicenseDescription;
break;
default:
publishingLicense = licenseType;
}
// get the claim id from the channel name, we will use that instead
const namedChannelClaim = myChannels.find(myChannel => myChannel.name === channel);
const channelId = namedChannelClaim ? namedChannelClaim.claim_id : '';
const publishPayload: {
name: ?string,
channel_id?: string,
bid: number,
file_path?: string,
tags: Array<string>,
locations?: Array<Location>,
license_url?: string,
license?: string,
thumbnail_url?: string,
release_time?: number,
fee_currency?: string,
fee_amount?: string,
} = {
name,
title,
description,
locations,
bid: creditsToString(bid),
languages: [language],
tags: tags && tags.map(tag => tag.name),
thumbnail_url: thumbnail,
};
if (publishingLicense) {
publishPayload.license = publishingLicense;
}
if (licenseUrl) {
publishPayload.license_url = licenseUrl;
}
// Set release time to curret date. On edits, keep original release/transaction time as release_time
if (myClaimForUri && myClaimForUri.value.release_time) {
publishPayload.release_time = Number(myClaimForUri.value.release_time);
} else if (myClaimForUri && myClaimForUri.timestamp) {
publishPayload.release_time = Number(myClaimForUri.timestamp);
} else {
publishPayload.release_time = Number(Math.round(Date.now() / 1000));
}
if (channelId) {
publishPayload.channel_id = channelId;
}
if (!contentIsFree && fee && (fee.currency && Number(fee.amount) > 0)) {
publishPayload.fee_currency = fee.currency;
publishPayload.fee_amount = creditsToString(fee.amount);
}
// Only pass file on new uploads, not metadata only edits.
// The sdk will figure it out
if (filePath) publishPayload.file_path = filePath;
const success = successResponse => {
export const doPublishDesktop = () => (dispatch: Dispatch, getState: () => {}) => {
const publishSuccess = successResponse => {
const state = getState();
analytics.apiLogPublish();
const myClaims = selectMyClaims(state);
const pendingClaim = successResponse.outputs[0];
const uri = pendingClaim.permanent_url;
const actions = [];
actions.push({
type: ACTIONS.PUBLISH_SUCCESS,
});
// We have to fake a temp claim until the new pending one is returned by claim_list_mine
// We can't rely on claim_list_mine because there might be some delay before the new claims are returned
// Doing this allows us to show the pending claim immediately, it will get overwritten by the real one
@ -323,74 +29,38 @@ export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => {
const myNewClaims = isEdit
? myClaims.map(claim => (isMatch(claim) ? pendingClaim : claim))
: myClaims.concat(pendingClaim);
actions.push(doOpenModal(MODALS.PUBLISH, { uri, isEdit }));
actions.push({
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
data: {
claims: myNewClaims,
},
});
dispatch(batchActions(...actions));
};
const failure = error => {
dispatch({ type: ACTIONS.PUBLISH_FAIL });
dispatch(doError(error.message));
const publishFail = error => {
const actions = [];
actions.push({ type: ACTIONS.PUBLISH_FAIL });
actions.push(doError(error.message));
dispatch(batchActions(...actions));
};
return Lbry.publish(publishPayload).then(success, failure);
return dispatch(doPublish(publishSuccess, publishFail));
};
// Calls claim_list_mine until any pending publishes are confirmed
export const doCheckPendingPublishes = () => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const pendingById = selectPendingById(state);
if (!Object.keys(pendingById).length) {
return;
}
let publishCheckInterval;
const checkFileList = () => {
Lbry.claim_list().then(claims => {
if (claims) {
claims.forEach(claim => {
// If it's confirmed, check if it was pending previously
if (claim.confirmations > 0 && pendingById[claim.claim_id]) {
delete pendingById[claim.claim_id];
// If it's confirmed, check if we should notify the user
if (selectosNotificationsEnabled(getState())) {
const notif = new window.Notification('LBRY Publish Complete', {
body: `${claim.value.title} has been published to lbry://${claim.name}. Click here to view it`,
silent: false,
});
notif.onclick = () => {
dispatch(push(formatLbryUriForWeb(claim.permanent_url)));
};
}
}
});
}
dispatch({
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
data: {
claims,
},
export const doCheckPendingPublishesApp = () => (dispatch: Dispatch, getState: GetState) => {
const onConfirmed = claim => {
if (selectosNotificationsEnabled(getState())) {
const notif = new window.Notification('LBRY Publish Complete', {
body: `${claim.value.title} has been published to lbry://${claim.name}. Click here to view it`,
silent: false,
});
if (!Object.keys(pendingById).length) {
clearInterval(publishCheckInterval);
}
});
notif.onclick = () => {
dispatch(push(formatLbryUriForWeb(claim.permanent_url)));
};
}
};
publishCheckInterval = setInterval(() => {
checkFileList();
}, 30000);
return dispatch(doCheckPendingPublishes(onConfirmed));
};

View file

@ -1,107 +1 @@
// @flow
import { handleActions } from 'util/redux-utils';
import { buildURI } from 'lbry-redux';
import * as ACTIONS from 'constants/action_types';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import { CHANNEL_ANONYMOUS } from 'constants/claim';
type PublishState = {
editingURI: ?string,
filePath: ?string,
contentIsFree: boolean,
fee: {
amount: number,
currency: string,
},
title: string,
thumbnail_url: string,
thumbnailPath: string,
uploadThumbnailStatus: string,
description: string,
language: string,
channel: string,
channelId: ?string,
name: string,
nameError: ?string,
bid: number,
bidError: ?string,
otherLicenseDescription: string,
licenseUrl: string,
tags: Array<string>,
};
const defaultState: PublishState = {
editingURI: undefined,
filePath: undefined,
contentIsFree: true,
fee: {
amount: 1,
currency: 'LBC',
},
title: '',
thumbnail_url: '',
thumbnailPath: '',
uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN,
description: '',
language: 'en',
nsfw: false,
channel: CHANNEL_ANONYMOUS,
channelId: '',
name: '',
nameError: undefined,
bid: 0.1,
bidError: undefined,
licenseType: 'None',
otherLicenseDescription: 'All rights reserved',
licenseUrl: '',
tags: [],
publishing: false,
publishSuccess: false,
publishError: undefined,
};
export default handleActions(
{
[ACTIONS.UPDATE_PUBLISH_FORM]: (state, action): PublishState => {
const { data } = action;
return {
...state,
...data,
};
},
[ACTIONS.CLEAR_PUBLISH]: (): PublishState => ({
...defaultState,
}),
[ACTIONS.PUBLISH_START]: (state: PublishState): PublishState => ({
...state,
publishing: true,
}),
[ACTIONS.PUBLISH_FAIL]: (state: PublishState): PublishState => ({
...state,
publishing: false,
}),
[ACTIONS.PUBLISH_SUCCESS]: (state: PublishState): PublishState => ({
...state,
publishing: false,
}),
[ACTIONS.DO_PREPARE_EDIT]: (state: PublishState, action) => {
const { ...publishData } = action.data;
const { channel, name, uri } = publishData;
// The short uri is what is presented to the user
// The editingUri is the full uri with claim id
const shortUri = buildURI({
channelName: channel,
contentName: name,
});
return {
...defaultState,
...publishData,
editingURI: uri,
uri: shortUri,
};
},
},
defaultState
);
// deleted, moved to lbry-redux

View file

@ -6653,9 +6653,9 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
yargs "^13.2.2"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#ea56de4548480eb1a10b752b3cecbad7de2f8914:
lbry-redux@lbryio/lbry-redux#8910693fe1fc4166fdc748d128344012c3d61874:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/ea56de4548480eb1a10b752b3cecbad7de2f8914"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/8910693fe1fc4166fdc748d128344012c3d61874"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"