Compare commits

..

No commits in common. "master" and "fix-canonical-url" have entirely different histories.

41 changed files with 8100 additions and 5164 deletions

View file

@ -22,13 +22,9 @@
"__": true
},
"rules": {
"consistent-return": 0,
"import/extensions": 0,
"import/no-commonjs": "warn",
"import/no-amd": "warn",
"import/prefer-default-export": "ignore",
"flowtype/generic-spacing": 0,
"func-names": ["warn", "as-needed"],
"no-plusplus": 0
"func-names": ["warn", "as-needed"]
}
}

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2017-2020 LBRY Inc
Copyright (c) 2017-2018 LBRY Inc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,

View file

@ -9,17 +9,14 @@ Add `lbryinc` as a dependency to your `package.json` file.
If you intend to make changes to the module and test immediately, you can use `npm link` to add the package to your `node_modules` folder. This will create a symlink to the folder where `lbry-redux` was cloned to.
```
cd lbryinc
yarn link
cd /<path>/<to>/<project> (ex: cd ~/lbry-desktop)
yarn link lbryinc
sudo npm link
cd /<path>/<to>/<project>/node_modules
npm link lbryinc
````
### Build
Run `$ yarn build`. If the symlink does not work, just build the file and move the `bundle.js` file in to the `node_modules/` folder.
### Automatic rebuild
To have the code automatically rebuild upon changes you can run `$ yarn dev` which will use `rollup` to watch the files and build upon detection of updated source code.
## License
[MIT © LBRY](LICENSE)

2560
dist/bundle.es.js vendored

File diff suppressed because it is too large Load diff

7127
dist/bundle.js vendored

File diff suppressed because it is too large Load diff

View file

@ -53,12 +53,11 @@
"flow-bin": "^0.69.0",
"flow-typed": "^2.4.0",
"husky": "^0.14.3",
"lbry-redux": "lbryio/lbry-redux",
"lint-staged": "^7.0.4",
"prettier": "^1.4.2",
"rollup": "^1.8.0",
"rollup-plugin-alias": "^2.0.0",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-copy": "^3.1.0",
"rollup-plugin-flow": "^1.1.1",
"rollup-plugin-includepaths": "^0.2.3",
"webpack": "^4.5.0",

View file

@ -1,38 +1,27 @@
import babel from 'rollup-plugin-babel';
import flow from 'rollup-plugin-flow';
import includePaths from 'rollup-plugin-includepaths';
import copy from 'rollup-plugin-copy';
import alias from 'rollup-plugin-alias';
let includePathOptions = {
include: {},
paths: ['src'],
external: [],
extensions: ['.js'],
extensions: ['.js']
};
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.es.js',
format: 'cjs',
format: 'cjs'
},
plugins: [
alias({
entries: [
{
find: 'flow-typed',
replacement: './flow-typed',
},
],
}),
flow({ all: true }),
includePaths(includePathOptions),
babel({
babelrc: false,
presets: [],
}),
copy({ targets: [{ src: './flow-typed', dest: 'dist' }] }),
],
external: ['lbry-redux'],
};
external: ['lbry-redux']
}

View file

@ -1,3 +1,8 @@
// Auth Token
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED';
export const GENERATE_AUTH_TOKEN_SUCCESS = 'GENERATE_AUTH_TOKEN_SUCCESS';
// Claims
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED';
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED';
@ -66,18 +71,14 @@ export const FILTERED_CONTENT_SUBSCRIBE = 'FILTERED_CONTENT_SUBSCRIBE';
export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED';
export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED';
// Stats
// File Stats
export const FETCH_VIEW_COUNT_STARTED = 'FETCH_VIEW_COUNT_STARTED';
export const FETCH_VIEW_COUNT_FAILED = 'FETCH_VIEW_COUNT_FAILED';
export const FETCH_VIEW_COUNT_COMPLETED = 'FETCH_VIEW_COUNT_COMPLETED';
export const FETCH_SUB_COUNT_STARTED = 'FETCH_SUB_COUNT_STARTED';
export const FETCH_SUB_COUNT_FAILED = 'FETCH_SUB_COUNT_FAILED';
export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED';
// Cross-device Sync
export const GET_SYNC_STARTED = 'GET_SYNC_STARTED';
export const GET_SYNC_COMPLETED = 'GET_SYNC_COMPLETED';
export const GET_SYNC_FAILED = 'GET_SYNC_FAILED';
export const SET_SYNC_STARTED = 'SET_SYNC_STARTED';
export const SET_SYNC_FAILED = 'SET_SYNC_FAILED';
export const SET_SYNC_COMPLETED = 'SET_SYNC_COMPLETED';
@ -85,13 +86,3 @@ export const SET_DEFAULT_ACCOUNT = 'SET_DEFAULT_ACCOUNT';
export const SYNC_APPLY_STARTED = 'SYNC_APPLY_STARTED';
export const SYNC_APPLY_COMPLETED = 'SYNC_APPLY_COMPLETED';
export const SYNC_APPLY_FAILED = 'SYNC_APPLY_FAILED';
export const SYNC_APPLY_BAD_PASSWORD = 'SYNC_APPLY_BAD_PASSWORD';
export const SYNC_RESET = 'SYNC_RESET';
// Lbry.tv
export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS';
// User
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED';
export const GENERATE_AUTH_TOKEN_SUCCESS = 'GENERATE_AUTH_TOKEN_SUCCESS';

View file

@ -1,4 +0,0 @@
export const ALREADY_CLAIMED =
'once the invite reward has been claimed the referrer cannot be changed';
export const REFERRER_NOT_FOUND =
'A lbry.tv account could not be found for the referrer you provided.';

View file

@ -0,0 +1,12 @@
export const VIEW_ALL = 'view_all';
export const VIEW_LATEST_FIRST = 'view_latest_first';
// Types for unreads
export const DOWNLOADING = 'DOWNLOADING';
export const DOWNLOADED = 'DOWNLOADED';
export const NOTIFY_ONLY = 'NOTIFY_ONLY;';
// Suggested types
export const SUGGESTED_TOP_BID = 'top_bid';
export const SUGGESTED_TOP_SUBSCRIBED = 'top_subscribed';
export const SUGGESTED_FEATURED = 'featured';

View file

@ -1,11 +0,0 @@
export const YOUTUBE_SYNC_NOT_TRANSFERRED = 'not_transferred';
export const YOUTUBE_SYNC_PENDING = 'pending';
export const YOUTUBE_SYNC_PENDING_EMAIL = 'pendingemail';
export const YOUTUBE_SYNC_PENDING_TRANSFER = 'pending_transfer';
export const YOUTUBE_SYNC_COMPLETED_TRANSFER = 'completed_transfer';
export const YOUTUBE_SYNC_QUEUED = 'queued';
export const YOUTUBE_SYNC_SYNCING = 'syncing';
export const YOUTUBE_SYNC_SYNCED = 'synced';
export const YOUTUBE_SYNC_FAILED = 'failed';
export const YOUTUBE_SYNC_PENDINGUPGRADE = 'pendingupgrade';
export const YOUTUBE_SYNC_ABANDONDED = 'abandoned';

View file

@ -1,79 +1,176 @@
import * as LBRYINC_ACTIONS from 'constants/action_types';
import * as YOUTUBE_STATUSES from 'constants/youtube';
import * as ERRORS from 'constants/errors';
import Lbryio from 'lbryio';
export { Lbryio };
import rewards from 'rewards';
import subscriptionsReducer from 'redux/reducers/subscriptions';
// constants
export { LBRYINC_ACTIONS, YOUTUBE_STATUSES, ERRORS };
export { LBRYINC_ACTIONS };
// utils
export { doTransifexUpload } from 'util/transifex-upload';
// Lbryio and rewards
export { Lbryio, rewards };
// actions
export { doGenerateAuthToken } from 'redux/actions/auth';
export {
doRewardList,
doClaimRewardType,
doClaimEligiblePurchaseRewards,
doClaimRewardClearError,
doFetchRewardedContent,
} from 'redux/actions/rewards';
export {
doChannelSubscribe,
doChannelUnsubscribe,
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
doCheckSubscription,
doCheckSubscriptions,
doCheckSubscriptionsInit,
doCompleteFirstRun,
doFetchMySubscriptions,
doFetchRecommendedSubscriptions,
doRemoveUnreadSubscription,
doRemoveUnreadSubscriptions,
doSetViewMode,
doShowSuggestedSubs,
doUpdateUnreadSubscriptions,
setSubscriptionLatest,
} from 'redux/actions/subscriptions';
export {
doFetchInviteStatus,
doInstallNew,
doAuthenticate,
doUserFetch,
doUserEmailNew,
doUserCheckEmailVerified,
doUserEmailToVerify,
doUserEmailVerifyFailure,
doUserEmailVerify,
doUserPhoneNew,
doUserPhoneReset,
doUserPhoneVerifyFailure,
doUserPhoneVerify,
doFetchAccessToken,
doUserResendVerificationEmail,
doUserIdentityVerify,
doUserInviteNew,
} from 'redux/actions/user';
export { doFetchCostInfoForUri } from 'redux/actions/cost_info';
export { doBlackListedOutpointsSubscribe } from 'redux/actions/blacklist';
export { doFilteredOutpointsSubscribe } from 'redux/actions/filtered';
export { doFetchFeaturedUris, doFetchTrendingUris } from 'redux/actions/homepage';
export { doFetchViewCount, doFetchSubCount } from 'redux/actions/stats';
export { doFetchViewCount } from 'redux/actions/stats';
export {
doCheckSync,
doGetSync,
doSetSync,
doSetDefaultAccount,
doSyncApply,
doResetSync,
doSyncEncryptAndDecrypt,
} from 'redux/actions/sync';
export { doUpdateUploadProgress } from 'redux/actions/web';
// reducers
export { authReducer } from 'redux/reducers/auth';
export { rewardsReducer } from 'redux/reducers/rewards';
export { subscriptionsReducer };
export { userReducer } from 'redux/reducers/user';
export { costInfoReducer } from 'redux/reducers/cost_info';
export { blacklistReducer } from 'redux/reducers/blacklist';
export { filteredReducer } from 'redux/reducers/filtered';
export { homepageReducer } from 'redux/reducers/homepage';
export { statsReducer } from 'redux/reducers/stats';
export { syncReducer } from 'redux/reducers/sync';
export { webReducer } from 'redux/reducers/web';
// selectors
export { selectAuthToken, selectIsAuthenticating } from 'redux/selectors/auth';
export {
makeSelectClaimRewardError,
makeSelectIsRewardClaimPending,
makeSelectRewardAmountByType,
makeSelectRewardByType,
selectUnclaimedRewardsByType,
selectClaimedRewardsById,
selectClaimedRewards,
selectClaimedRewardsByTransactionId,
selectUnclaimedRewards,
selectFetchingRewards,
selectUnclaimedRewardValue,
selectClaimsPendingByType,
selectClaimErrorsByType,
selectRewardContentClaimIds,
selectReferralReward,
} from 'redux/selectors/rewards';
export {
makeSelectIsNew,
makeSelectIsSubscribed,
makeSelectUnreadByChannel,
selectEnabledChannelNotifications,
selectSubscriptions,
selectIsFetchingSubscriptions,
selectViewMode,
selectSuggested,
selectIsFetchingSuggested,
selectSuggestedChannels,
selectFirstRunCompleted,
selectShowSuggestedSubs,
selectSubscriptionsBeingFetched,
selectUnreadByChannel,
selectUnreadAmount,
selectUnreadSubscriptions,
selectSubscriptionClaims,
} from 'redux/selectors/subscriptions';
export {
selectAuthenticationIsPending,
selectUserIsPending,
selectUser,
selectUserEmail,
selectUserPhone,
selectUserCountryCode,
selectEmailToVerify,
selectPhoneToVerify,
selectUserIsRewardApproved,
selectEmailNewIsPending,
selectEmailNewErrorMessage,
selectPhoneNewErrorMessage,
selectPhoneNewIsPending,
selectEmailVerifyIsPending,
selectEmailVerifyErrorMessage,
selectPhoneVerifyErrorMessage,
selectPhoneVerifyIsPending,
selectIdentityVerifyIsPending,
selectIdentityVerifyErrorMessage,
selectUserIsVerificationCandidate,
selectAccessToken,
selectUserInviteStatusIsPending,
selectUserInvitesRemaining,
selectUserInvitees,
selectUserInviteStatusFailed,
selectUserInviteNewIsPending,
selectUserInviteNewErrorMessage,
selectUserInviteReferralLink,
} from 'redux/selectors/user';
export {
makeSelectFetchingCostInfoForUri,
makeSelectCostInfoForUri,
selectAllCostInfoByUri,
selectFetchingCostInfo,
} from 'redux/selectors/cost_info';
export {
selectBlackListedOutpoints,
selectBlacklistedOutpointMap,
} from 'redux/selectors/blacklist';
export { selectFilteredOutpoints, selectFilteredOutpointMap } from 'redux/selectors/filtered';
export { selectBlackListedOutpoints } from 'redux/selectors/blacklist';
export { selectFilteredOutpoints } from 'redux/selectors/filtered';
export {
selectFeaturedUris,
selectFetchingFeaturedUris,
selectTrendingUris,
selectFetchingTrendingUris,
} from 'redux/selectors/homepage';
export {
selectViewCount,
makeSelectViewCountForUri,
makeSelectSubCountForUri,
} from 'redux/selectors/stats';
export { makeSelectViewCountForUri } from 'redux/selectors/stats';
export {
selectHasSyncedWallet,
selectSyncData,
selectSyncHash,
selectSetSyncErrorMessage,
selectGetSyncErrorMessage,
selectGetSyncIsPending,
selectSetSyncIsPending,
selectSyncApplyIsPending,
selectHashChanged,
selectSyncApplyErrorMessage,
selectSyncApplyPasswordError,
} from 'redux/selectors/sync';
export { selectCurrentUploads, selectUploadCount } from 'redux/selectors/web';

View file

@ -11,7 +11,6 @@ const Lbryio = {
};
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
const INTERNAL_APIS_DOWN = 'internal_apis_down';
// We can't use env's because they aren't passed into node_modules
Lbryio.setLocalApi = endpoint => {
@ -31,12 +30,6 @@ Lbryio.call = (resource, action, params = {}, method = 'get') => {
if (response.status >= 200 && response.status < 300) {
return response.json();
}
if (response.status === 500) {
return Promise.reject(INTERNAL_APIS_DOWN);
}
if (response)
return response.json().then(json => {
let error;
if (json.error) {
@ -55,13 +48,6 @@ Lbryio.call = (resource, action, params = {}, method = 'get') => {
return Lbryio.getAuthToken().then(token => {
const fullParams = { auth_token: token, ...params };
Object.keys(fullParams).forEach(key => {
const value = fullParams[key];
if (typeof value === 'object') {
fullParams[key] = JSON.stringify(value);
}
});
const qs = querystring.stringify(fullParams);
let url = `${Lbryio.CONNECTION_STRING}${resource}/${action}?${qs}`;
@ -94,7 +80,7 @@ Lbryio.getAuthToken = () =>
Lbryio.overrides.getAuthToken().then(token => {
resolve(token);
});
} else if (typeof window !== 'undefined') {
} else {
const { store } = window;
if (store) {
const state = store.getState();
@ -103,27 +89,23 @@ Lbryio.getAuthToken = () =>
resolve(token);
}
resolve(null);
} else {
resolve(null);
}
});
Lbryio.getCurrentUser = () => Lbryio.call('user', 'me');
Lbryio.authenticate = (domain, language) => {
Lbryio.authenticate = () => {
if (!Lbryio.enabled) {
const params = {
return new Promise(resolve => {
resolve({
id: 1,
language: 'en',
primary_email: 'disabled@lbry.io',
has_verified_email: true,
is_identity_verified: true,
is_reward_approved: false,
language: language || 'en',
};
return new Promise(resolve => {
resolve(params);
});
});
}
@ -138,34 +120,27 @@ Lbryio.authenticate = (domain, language) => {
// check that token works
return Lbryio.getCurrentUser()
.then(user => user)
.catch(error => {
if (error === INTERNAL_APIS_DOWN) {
throw new Error('Internal APIS down');
}
return false;
});
.catch(() => false);
})
.then(user => {
if (user) {
return user;
}
return Lbry.status()
.then(
status =>
new Promise((res, rej) => {
const appId =
domain && domain !== 'lbry.tv'
? (domain.replace(/[.]/gi, '') + status.installation_id).slice(0, 66)
: status.installation_id;
return Lbry.status().then(status => {
if (Lbryio.overrides.setAuthToken) {
return Lbryio.overrides.setAuthToken(status);
}
// simply call the logic to create a new user, and obtain the auth token
return new Promise((res, rej) => {
Lbryio.call(
'user',
'new',
{
auth_token: '',
language: language || 'en',
app_id: appId,
language: 'en',
app_id: status.installation_id,
},
'post'
)
@ -175,28 +150,25 @@ Lbryio.authenticate = (domain, language) => {
}
const { store } = window;
if (Lbryio.overrides.setAuthToken) {
Lbryio.overrides.setAuthToken(response.auth_token);
}
if (store) {
store.dispatch({
type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS,
data: { authToken: response.auth_token },
});
}
Lbryio.authToken = response.auth_token;
return res(response);
res(response);
})
.catch(error => rej(error));
});
});
})
)
.then(newUser => {
if (!newUser) {
.then(user => {
if (!user) {
return Lbryio.getCurrentUser();
}
return newUser;
});
return user;
})
.then(resolve, reject);
});

View file

@ -10,19 +10,17 @@ export function doFetchBlackListedOutpoints() {
});
const success = ({ outpoints }) => {
const splitOutpoints = [];
if (outpoints) {
const splitedOutpoints = [];
outpoints.forEach((outpoint, index) => {
const [txid, nout] = outpoint.split(':');
splitOutpoints[index] = { txid, nout: Number.parseInt(nout, 10) };
splitedOutpoints[index] = { txid, nout: Number.parseInt(nout, 10) };
});
}
dispatch({
type: ACTIONS.FETCH_BLACK_LISTED_CONTENT_COMPLETED,
data: {
outpoints: splitOutpoints,
outpoints: splitedOutpoints,
success: true,
},
});
@ -38,9 +36,7 @@ export function doFetchBlackListedOutpoints() {
});
};
Lbryio.call('file', 'list_blocked', {
auth_token: '',
}).then(success, failure);
Lbryio.call('file', 'list_blocked').then(success, failure);
};
}

View file

@ -10,13 +10,10 @@ export function doFetchFilteredOutpoints() {
});
const success = ({ outpoints }) => {
let formattedOutpoints = [];
if (outpoints) {
formattedOutpoints = outpoints.map(outpoint => {
const formattedOutpoints = outpoints.map(outpoint => {
const [txid, nout] = outpoint.split(':');
return { txid, nout: Number.parseInt(nout, 10) };
});
}
dispatch({
type: ACTIONS.FETCH_FILTERED_CONTENT_COMPLETED,
@ -35,7 +32,7 @@ export function doFetchFilteredOutpoints() {
});
};
Lbryio.call('file', 'list_filtered', { auth_token: '' }).then(success, failure);
Lbryio.call('file', 'list_filtered').then(success, failure);
};
}

View file

@ -0,0 +1,154 @@
import Lbryio from 'lbryio';
import { ACTIONS, doToast } from 'lbry-redux';
import { selectUnclaimedRewards } from 'redux/selectors/rewards';
import { selectUserIsRewardApproved } from 'redux/selectors/user';
import { doFetchInviteStatus } from 'redux/actions/user';
import rewards from 'rewards';
export function doRewardList() {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_REWARDS_STARTED,
});
Lbryio.call('reward', 'list', { multiple_rewards_per_type: true })
.then(userRewards => {
dispatch({
type: ACTIONS.FETCH_REWARDS_COMPLETED,
data: { userRewards },
});
})
.catch(() => {
dispatch({
type: ACTIONS.FETCH_REWARDS_COMPLETED,
data: { userRewards: [] },
});
});
};
}
export function doClaimRewardType(rewardType, options = {}) {
return (dispatch, getState) => {
const state = getState();
const userIsRewardApproved = selectUserIsRewardApproved(state);
const unclaimedRewards = selectUnclaimedRewards(state);
const reward =
rewardType === rewards.TYPE_REWARD_CODE
? { reward_type: rewards.TYPE_REWARD_CODE }
: unclaimedRewards.find(ur => ur.reward_type === rewardType);
if (rewardType !== rewards.TYPE_REWARD_CODE) {
if (!reward || reward.transaction_id) {
// already claimed or doesn't exist, do nothing
return;
}
}
if (!userIsRewardApproved && rewardType !== rewards.TYPE_CONFIRM_EMAIL) {
if (!options || (!options.failSilently && rewards.callbacks.rewardApprovalRequested)) {
rewards.callbacks.rewardApprovalRequested();
}
return;
}
// Set `claim_code` so the api knows which reward to give if there are multiple of the same type
const params = options.params || {};
params.claim_code = reward.claim_code;
dispatch({
type: ACTIONS.CLAIM_REWARD_STARTED,
data: { reward },
});
const success = successReward => {
dispatch({
type: ACTIONS.CLAIM_REWARD_SUCCESS,
data: {
reward: successReward,
},
});
if (
successReward.reward_type === rewards.TYPE_NEW_USER &&
rewards.callbacks.claimFirstRewardSuccess
) {
rewards.callbacks.claimFirstRewardSuccess();
} else if (successReward.reward_type === rewards.TYPE_REFERRAL) {
dispatch(doFetchInviteStatus());
}
dispatch(doRewardList());
};
const failure = error => {
dispatch({
type: ACTIONS.CLAIM_REWARD_FAILURE,
data: {
reward,
error: !options || !options.failSilently ? error : undefined,
},
});
if (options.notifyError) {
dispatch(doToast({ message: error.message, isError: true }));
}
};
rewards.claimReward(rewardType, params).then(success, failure);
};
}
export function doClaimEligiblePurchaseRewards() {
return (dispatch, getState) => {
const state = getState();
const unclaimedRewards = selectUnclaimedRewards(state);
const userIsRewardApproved = selectUserIsRewardApproved(state);
if (!userIsRewardApproved || !Lbryio.enabled) {
return;
}
if (unclaimedRewards.find(ur => ur.reward_type === rewards.TYPE_FIRST_STREAM)) {
dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM));
} else {
[rewards.TYPE_MANY_DOWNLOADS, rewards.TYPE_FEATURED_DOWNLOAD].forEach(type => {
dispatch(doClaimRewardType(type, { failSilently: true }));
});
}
};
}
export function doClaimRewardClearError(reward) {
return dispatch => {
dispatch({
type: ACTIONS.CLAIM_REWARD_CLEAR_ERROR,
data: { reward },
});
};
}
export function doFetchRewardedContent() {
return dispatch => {
const success = nameToClaimId => {
dispatch({
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: Object.values(nameToClaimId),
success: true,
},
});
};
const failure = () => {
dispatch({
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: [],
success: false,
},
});
};
Lbryio.call('reward', 'list_featured').then(success, failure);
};
}

View file

@ -2,31 +2,15 @@
import Lbryio from 'lbryio';
import * as ACTIONS from 'constants/action_types';
export const doFetchViewCount = (claimIdCsv: string) => dispatch => {
export const doFetchViewCount = (claimId: string) => dispatch => {
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_STARTED });
return Lbryio.call('file', 'view_count', { claim_id: claimIdCsv })
return Lbryio.call('file', 'view_count', { claim_id: claimId })
.then((result: Array<number>) => {
const viewCounts = result;
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimIdCsv, viewCounts } });
const viewCount = result[0];
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimId, viewCount } });
})
.catch(error => {
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_FAILED, data: error });
});
};
export const doFetchSubCount = (claimId: string) => dispatch => {
dispatch({ type: ACTIONS.FETCH_SUB_COUNT_STARTED });
return Lbryio.call('subscription', 'sub_count', { claim_id: claimId })
.then((result: Array<number>) => {
const subCount = result[0];
dispatch({
type: ACTIONS.FETCH_SUB_COUNT_COMPLETED,
data: { claimId, subCount },
});
})
.catch(error => {
dispatch({ type: ACTIONS.FETCH_SUB_COUNT_FAILED, data: error });
});
};

View file

@ -0,0 +1,449 @@
// @flow
import type { GetState } from 'types/redux';
import type {
Dispatch as ReduxDispatch,
SubscriptionState,
Subscription,
SubscriptionNotificationType,
ViewMode,
UnreadSubscription,
} from 'types/subscription';
import { PAGE_SIZE } from 'constants/claim';
import { doClaimRewardType } from 'redux/actions/rewards';
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { Lbry, parseURI, doResolveUris, doPurchaseUri } from 'lbry-redux';
import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import Lbryio from 'lbryio';
import rewards from 'rewards';
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
export const doSetViewMode = (viewMode: ViewMode) => (dispatch: ReduxDispatch) =>
dispatch({
type: ACTIONS.SET_VIEW_MODE,
data: viewMode,
});
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
dispatch: ReduxDispatch
) =>
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription,
uri,
},
});
// Populate a channels unread subscriptions or update the type
export const doUpdateUnreadSubscriptions = (
channelUri: string,
uris: ?Array<string>,
type: ?SubscriptionNotificationType
) => (dispatch: ReduxDispatch, getState: GetState) => {
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
const currentUnreadForChannel: UnreadSubscription = unreadByChannel[channelUri];
let newUris: Array = [];
let newType: string = null;
if (!currentUnreadForChannel) {
newUris = uris;
newType = type;
} else {
if (uris) {
// If a channel currently has no unread uris, just add them all
if (!currentUnreadForChannel.uris || !currentUnreadForChannel.uris.length) {
newUris = uris;
} else {
// They already have unreads and now there are new ones
// Add the new ones to the beginning of the list
// Make sure there are no duplicates
const currentUnreadUris = currentUnreadForChannel.uris;
newUris = uris.filter(uri => !currentUnreadUris.includes(uri)).concat(currentUnreadUris);
}
} else {
newUris = currentUnreadForChannel.uris;
}
newType = type || currentUnreadForChannel.type;
}
dispatch({
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: channelUri,
uris: newUris,
type: newType,
},
});
};
// Remove multiple files (or all) from a channels unread subscriptions
export const doRemoveUnreadSubscriptions = (channelUri: ?string, readUris: ?Array<string>) => (
dispatch: ReduxDispatch,
getState: GetState
) => {
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
// If no channel is passed in, remove all unread subscriptions from all channels
if (!channelUri) {
return dispatch({
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: { channel: null },
});
}
const currentChannelUnread = unreadByChannel[channelUri];
if (!currentChannelUnread || !currentChannelUnread.uris) {
// Channel passed in doesn't have any unreads
return null;
}
// For each uri passed in, remove it from the list of unread uris
// If no uris are passed in, remove them all
let newUris;
if (readUris) {
const urisToRemoveMap = readUris.reduce(
(acc, val) => ({
...acc,
[val]: true,
}),
{}
);
const filteredUris = currentChannelUnread.uris.filter(uri => !urisToRemoveMap[uri]);
newUris = filteredUris.length ? filteredUris : null;
} else {
newUris = null;
}
return dispatch({
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: channelUri,
uris: newUris,
},
});
};
// Remove a single file from a channels unread subscriptions
export const doRemoveUnreadSubscription = (channelUri: string, readUri: string) => (
dispatch: ReduxDispatch
) => {
dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri]));
};
export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: boolean) => (
dispatch: ReduxDispatch,
getState: GetState
) => {
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
const state = getState();
const shouldAutoDownload = false; // makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
const savedSubscription = state.subscriptions.subscriptions.find(
sub => sub.uri === subscriptionUri
);
if (!savedSubscription) {
throw Error(
`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`
);
}
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
Lbry.claim_search({
channel: subscriptionUri,
valid_channel_signature: true,
order_by: ['release_time'],
page: 1,
page_size: PAGE_SIZE,
}).then(claimListByChannel => {
const { items: claimsInChannel } = claimListByChannel;
// may happen if subscribed to an abandoned channel or an empty channel
if (!claimsInChannel || !claimsInChannel.length) {
return;
}
// Determine if the latest subscription currently saved is actually the latest subscription
const latestIndex = claimsInChannel.findIndex(
claim => claim.permanent_url === savedSubscription.latest
);
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
// If latest is 0, nothing has changed
// Do not download/notify about new content, it would download/notify 10 claims per channel
if (latestIndex !== 0 && savedSubscription.latest) {
let downloadCount = 0;
const newUnread = [];
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
const uri = claim.permanent_url;
const shouldDownload =
shouldAutoDownload &&
Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee);
// Add the new content to the list of "un-read" subscriptions
if (shouldNotify) {
newUnread.push(uri);
}
if (shouldDownload) {
downloadCount += 1;
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
}
});
dispatch(
doUpdateUnreadSubscriptions(
subscriptionUri,
newUnread,
downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
)
);
}
// Set the latest piece of content for a channel
// This allows the app to know if there has been new content since it was last set
const latest = claimsInChannel[0];
dispatch(
setSubscriptionLatest(
{
channelName: latest.signing_channel.name,
uri: latest.signing_channel.permanent_url,
},
latest.permanent_url
)
);
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
// means it will delete a non-existant fetchingChannelClaims[uri]
dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: {
uri: subscriptionUri,
claims: claimsInChannel || [],
page: 1,
},
});
});
};
export const doChannelSubscribe = (subscription: Subscription) => (
dispatch: ReduxDispatch,
getState: GetState
) => {
const {
settings: { daemonSettings },
} = getState();
const isSharingData = daemonSettings ? daemonSettings.share_usage_data : true;
const subscriptionUri = subscription.uri;
if (!subscriptionUri.startsWith('lbry://')) {
throw Error(
`Subscription uris must inclue the "lbry://" prefix.\nTried to subscribe to ${subscriptionUri}`
);
}
dispatch({
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: subscription,
});
// if the user isn't sharing data, keep the subscriptions entirely in the app
if (isSharingData) {
const { channelClaimId } = parseURI(subscription.uri);
// They are sharing data, we can store their subscriptions in our internal database
Lbryio.call('subscription', 'new', {
channel_name: subscription.channelName,
claim_id: channelClaimId,
});
dispatch(doClaimRewardType(rewards.TYPE_SUBSCRIPTION, { failSilently: true }));
}
dispatch(doCheckSubscription(subscription.uri, true));
};
export const doChannelUnsubscribe = (subscription: Subscription) => (
dispatch: ReduxDispatch,
getState: GetState
) => {
const {
settings: { daemonSettings },
} = getState();
const isSharingData = daemonSettings ? daemonSettings.share_usage_data : true;
dispatch({
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: subscription,
});
if (isSharingData) {
const { channelClaimId } = parseURI(subscription.uri);
Lbryio.call('subscription', 'delete', {
claim_id: channelClaimId,
});
}
};
export const doCheckSubscriptions = () => (dispatch: ReduxDispatch, getState: GetState) => {
const state = getState();
const subscriptions = selectSubscriptions(state);
subscriptions.forEach((sub: Subscription) => {
dispatch(doCheckSubscription(sub.uri, true));
});
};
export const doFetchMySubscriptions = () => (dispatch: ReduxDispatch, getState: GetState) => {
const state: { subscriptions: SubscriptionState, settings: any } = getState();
const { subscriptions: reduxSubscriptions } = state.subscriptions;
// default to true if daemonSettings not found
const isSharingData =
state.settings && state.settings.daemonSettings
? state.settings.daemonSettings.share_usage_data
: true;
if (!isSharingData && isSharingData !== undefined) {
// They aren't sharing their data, subscriptions will be handled by persisted redux state
return;
}
// most of this logic comes from scenarios where the db isn't synced with redux
// this will happen if the user stops sharing data
dispatch({ type: ACTIONS.FETCH_SUBSCRIPTIONS_START });
Lbryio.call('subscription', 'list')
.then(dbSubscriptions => {
const storedSubscriptions = dbSubscriptions || [];
// User has no subscriptions in db or redux
if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
return [];
}
// There is some mismatch between redux state and db state
// If something is in the db, but not in redux, add it to redux
// If something is in redux, but not in the db, add it to the db
if (storedSubscriptions.length !== reduxSubscriptions.length) {
const dbSubMap = {};
const reduxSubMap = {};
const subsNotInDB = [];
const subscriptionsToReturn = reduxSubscriptions.slice();
storedSubscriptions.forEach(sub => {
dbSubMap[sub.claim_id] = 1;
});
reduxSubscriptions.forEach(sub => {
const { channelClaimId } = parseURI(sub.uri);
reduxSubMap[channelClaimId] = 1;
});
storedSubscriptions.forEach(sub => {
if (!reduxSubMap[sub.claim_id]) {
const uri = `lbry://${sub.channel_name}#${sub.claim_id}`;
subscriptionsToReturn.push({ uri, channelName: sub.channel_name });
}
});
return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload)))
.then(() => subscriptionsToReturn)
.catch(
() =>
// let it fail, we will try again when the navigate to the subscriptions page
subscriptionsToReturn
);
}
// DB is already synced, just return the subscriptions in redux
return reduxSubscriptions;
})
.then((subscriptions: Array<Subscription>) => {
dispatch({
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: subscriptions,
});
dispatch(doResolveUris(subscriptions.map(({ uri }) => uri)));
dispatch(doCheckSubscriptions());
})
.catch(() => {
dispatch({
type: ACTIONS.FETCH_SUBSCRIPTIONS_FAIL,
});
});
};
export const doCheckSubscriptionsInit = () => (dispatch: ReduxDispatch) => {
// doCheckSubscriptionsInit is called by doDaemonReady
// setTimeout below is a hack to ensure redux is hydrated when subscriptions are checked
// this will be replaced with <PersistGate> which reqiures a package upgrade
setTimeout(() => dispatch(doFetchMySubscriptions()), 5000);
const checkSubscriptionsTimer = setInterval(
() => dispatch(doCheckSubscriptions()),
CHECK_SUBSCRIPTIONS_INTERVAL
);
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
data: { checkSubscriptionsTimer },
});
setInterval(() => dispatch(doCheckSubscriptions()), CHECK_SUBSCRIPTIONS_INTERVAL);
};
export const doFetchRecommendedSubscriptions = () => (dispatch: ReduxDispatch) => {
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
});
return Lbryio.call('subscription', 'suggest')
.then(suggested =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS,
data: suggested,
})
)
.catch(error =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL,
error,
})
);
};
export const doCompleteFirstRun = () => (dispatch: ReduxDispatch) =>
dispatch({
type: ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED,
});
export const doShowSuggestedSubs = () => (dispatch: ReduxDispatch) =>
dispatch({
type: ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS,
});
export const doChannelSubscriptionEnableNotifications = (channelName: string) => (
dispatch: ReduxDispatch
) =>
dispatch({
type: ACTIONS.CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS,
data: channelName,
});
export const doChannelSubscriptionDisableNotifications = (channelName: string) => (
dispatch: ReduxDispatch
) =>
dispatch({
type: ACTIONS.CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS,
data: channelName,
});

View file

@ -1,17 +1,43 @@
import * as ACTIONS from 'constants/action_types';
import Lbryio from 'lbryio';
import { Lbry, doWalletEncrypt, doWalletDecrypt } from 'lbry-redux';
import { Lbry } from 'lbry-redux';
const NO_WALLET_ERROR = 'no wallet found for this user';
export function doSetSync(oldHash, newHash, data) {
return dispatch => {
dispatch({
type: ACTIONS.SET_SYNC_STARTED,
});
export function doSetDefaultAccount(success, failure) {
Lbryio.call('sync', 'set', { old_hash: oldHash, new_hash: newHash, data }, 'post')
.then(response => {
if (!response.hash) {
return dispatch({
type: ACTIONS.SET_SYNC_FAILED,
data: { error: 'No hash returned for sync/set.' },
});
}
return dispatch({
type: ACTIONS.SET_SYNC_COMPLETED,
data: { syncHash: response.hash },
});
})
.catch(error => {
dispatch({
type: ACTIONS.SET_SYNC_FAILED,
data: { error },
});
});
};
}
export function doSetDefaultAccount() {
return dispatch => {
dispatch({
type: ACTIONS.SET_DEFAULT_ACCOUNT,
});
Lbry.account_list()
.then(accountList => {
Lbry.account_list().then(accountList => {
const { lbc_mainnet: accounts } = accountList;
let defaultId;
for (let i = 0; i < accounts.length; ++i) {
@ -29,149 +55,40 @@ export function doSetDefaultAccount(success, failure) {
// Set the default account
if (defaultId) {
Lbry.account_set({ account_id: defaultId, default: true })
.then(() => {
if (success) {
success();
}
})
.catch(err => {
if (failure) {
failure(err);
}
});
} else if (failure) {
// no default account to set
failure('Could not set a default account'); // fail
}
})
.catch(err => {
if (failure) {
failure(err);
Lbry.account_set({ account_id: defaultId, default: true });
}
});
};
}
export function doSetSync(oldHash, newHash, data) {
return dispatch => {
dispatch({
type: ACTIONS.SET_SYNC_STARTED,
});
return Lbryio.call('sync', 'set', { old_hash: oldHash, new_hash: newHash, data }, 'post')
.then(response => {
if (!response.hash) {
throw Error('No hash returned for sync/set.');
}
return dispatch({
type: ACTIONS.SET_SYNC_COMPLETED,
data: { syncHash: response.hash },
});
})
.catch(error => {
dispatch({
type: ACTIONS.SET_SYNC_FAILED,
data: { error },
});
});
};
}
export function doGetSync(passedPassword, callback) {
const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword;
function handleCallback(error, hasNewData) {
if (callback) {
if (typeof callback !== 'function') {
throw new Error('Second argument passed to "doGetSync" must be a function');
}
callback(error, hasNewData);
}
}
export function doGetSync(password) {
return dispatch => {
dispatch({
type: ACTIONS.GET_SYNC_STARTED,
});
const data = {};
Lbry.wallet_status()
.then(status => {
if (status.is_locked) {
return Lbry.wallet_unlock({ password });
}
// Wallet is already unlocked
return true;
})
.then(isUnlocked => {
if (isUnlocked) {
return Lbry.sync_hash();
}
data.unlockFailed = true;
throw new Error();
})
.then(hash => Lbryio.call('sync', 'get', { hash }, 'post'))
Lbry.sync_hash().then(hash => {
Lbryio.call('sync', 'get', { hash }, 'post')
.then(response => {
const data = { hasSyncedWallet: true };
if (response.changed) {
const syncHash = response.hash;
data.syncHash = syncHash;
data.syncData = response.data;
data.changed = response.changed;
data.hasSyncedWallet = true;
if (response.changed) {
return Lbry.sync_apply({ password, data: response.data, blocking: true });
}
})
.then(response => {
if (!response) {
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(null, data.changed);
return;
}
const { hash: walletHash, data: walletData } = response;
if (walletHash !== data.syncHash) {
Lbry.sync_apply({ password, data: response.data }).then(
({ hash: walletHash, data: walletData }) => {
if (walletHash !== syncHash) {
// different local hash, need to synchronise
dispatch(doSetSync(data.syncHash, walletHash, walletData));
dispatch(doSetSync(syncHash, walletHash, walletData));
}
}
);
}
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(null, data.changed);
})
.catch(syncAttemptError => {
if (data.unlockFailed) {
dispatch({ type: ACTIONS.GET_SYNC_FAILED, data: { error: syncAttemptError } });
if (password !== '') {
dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD });
}
handleCallback(syncAttemptError);
} else if (data.hasSyncedWallet) {
const error =
(syncAttemptError && syncAttemptError.message) || 'Error getting synced wallet';
dispatch({
type: ACTIONS.GET_SYNC_FAILED,
data: {
error,
},
});
// Temp solution until we have a bad password error code
// Don't fail on blank passwords so we don't show a "password error" message
// before users have ever entered a password
if (password !== '') {
dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD });
}
handleCallback(error);
} else {
.catch(() => {
// user doesn't have a synced wallet
dispatch({
type: ACTIONS.GET_SYNC_COMPLETED,
@ -180,17 +97,10 @@ export function doGetSync(passedPassword, callback) {
// call sync_apply to get data to sync
// first time sync. use any string for old hash
if (syncAttemptError.message === NO_WALLET_ERROR) {
Lbry.sync_apply({ password })
.then(({ hash: walletHash, data: syncApplyData }) => {
dispatch(doSetSync('', walletHash, syncApplyData, password));
handleCallback();
})
.catch(syncApplyError => {
handleCallback(syncApplyError);
Lbry.sync_apply({ password }).then(({ hash: walletHash, data }) =>
dispatch(doSetSync(null, walletHash, data))
);
});
}
}
});
};
}
@ -251,38 +161,3 @@ export function doCheckSync() {
});
};
}
export function doResetSync() {
return dispatch =>
new Promise(resolve => {
dispatch({ type: ACTIONS.SYNC_RESET });
resolve();
});
}
export function doSyncEncryptAndDecrypt(oldPassword, newPassword, encrypt) {
return dispatch => {
const data = {};
return Lbry.sync_hash()
.then(hash => Lbryio.call('sync', 'get', { hash }, 'post'))
.then(syncGetResponse => {
data.oldHash = syncGetResponse.hash;
return Lbry.sync_apply({ password: oldPassword, data: syncGetResponse.data });
})
.then(() => {
if (encrypt) {
dispatch(doWalletEncrypt(newPassword));
} else {
dispatch(doWalletDecrypt());
}
})
.then(() => Lbry.sync_apply({ password: newPassword }))
.then(syncApplyResponse => {
if (syncApplyResponse.hash !== data.oldHash) {
return dispatch(doSetSync(data.oldHash, syncApplyResponse.hash, syncApplyResponse.data));
}
})
.catch(console.error);
};
}

384
src/redux/actions/user.js Normal file
View file

@ -0,0 +1,384 @@
import { ACTIONS, Lbry, doToast } from 'lbry-redux';
import { doClaimRewardType, doRewardList } from 'redux/actions/rewards';
import {
selectEmailToVerify,
selectPhoneToVerify,
selectUserCountryCode,
} from 'redux/selectors/user';
import rewards from 'rewards';
import Lbryio from 'lbryio';
export function doFetchInviteStatus() {
return dispatch => {
dispatch({
type: ACTIONS.USER_INVITE_STATUS_FETCH_STARTED,
});
Promise.all([Lbryio.call('user', 'invite_status'), Lbryio.call('user_referral_code', 'list')])
.then(([status, code]) => {
dispatch(doRewardList());
dispatch({
type: ACTIONS.USER_INVITE_STATUS_FETCH_SUCCESS,
data: {
invitesRemaining: status.invites_remaining ? status.invites_remaining : 0,
invitees: status.invitees,
referralLink: `${Lbryio.CONNECTION_STRING}user/refer?r=${code}`,
},
});
})
.catch(error => {
dispatch({
type: ACTIONS.USER_INVITE_STATUS_FETCH_FAILURE,
data: { error },
});
});
};
}
export function doInstallNew(appVersion, os = null) {
const payload = { app_version: appVersion };
Lbry.status().then(status => {
payload.app_id = status.installation_id;
payload.node_id = status.lbry_id;
Lbry.version().then(version => {
payload.daemon_version = version.lbrynet_version;
payload.operating_system = os || version.os_system;
payload.platform = version.platform;
Lbryio.call('install', 'new', payload);
});
});
}
// TODO: Call doInstallNew separately so we don't have to pass appVersion and os_system params?
export function doAuthenticate(appVersion, os = null) {
return dispatch => {
dispatch({
type: ACTIONS.AUTHENTICATION_STARTED,
});
Lbryio.authenticate()
.then(user => {
// analytics.setUser(user);
dispatch({
type: ACTIONS.AUTHENTICATION_SUCCESS,
data: { user },
});
dispatch(doRewardList());
dispatch(doFetchInviteStatus());
doInstallNew(appVersion, os);
})
.catch(error => {
dispatch({
type: ACTIONS.AUTHENTICATION_FAILURE,
data: { error },
});
});
};
}
export function doUserFetch() {
return dispatch => {
dispatch({
type: ACTIONS.USER_FETCH_STARTED,
});
Lbryio.getCurrentUser()
.then(user => {
// analytics.setUser(user);
dispatch(doRewardList());
dispatch({
type: ACTIONS.USER_FETCH_SUCCESS,
data: { user },
});
})
.catch(error => {
dispatch({
type: ACTIONS.USER_FETCH_FAILURE,
data: { error },
});
});
};
}
export function doUserCheckEmailVerified() {
// This will happen in the background so we don't need loading booleans
return dispatch => {
Lbryio.getCurrentUser().then(user => {
if (user.has_verified_email) {
dispatch(doRewardList());
dispatch({
type: ACTIONS.USER_FETCH_SUCCESS,
data: { user },
});
}
});
};
}
export function doUserPhoneReset() {
return {
type: ACTIONS.USER_PHONE_RESET,
};
}
export function doUserPhoneNew(phone, countryCode) {
return dispatch => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_STARTED,
data: { phone, country_code: countryCode },
});
const success = () => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_SUCCESS,
data: { phone },
});
};
const failure = error => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_FAILURE,
data: { error },
});
};
Lbryio.call(
'user',
'phone_number_new',
{ phone_number: phone, country_code: countryCode },
'post'
).then(success, failure);
};
}
export function doUserPhoneVerifyFailure(error) {
return {
type: ACTIONS.USER_PHONE_VERIFY_FAILURE,
data: { error },
};
}
export function doUserPhoneVerify(verificationCode) {
return (dispatch, getState) => {
const phoneNumber = selectPhoneToVerify(getState());
const countryCode = selectUserCountryCode(getState());
dispatch({
type: ACTIONS.USER_PHONE_VERIFY_STARTED,
code: verificationCode,
});
Lbryio.call(
'user',
'phone_number_confirm',
{
verification_code: verificationCode,
phone_number: phoneNumber,
country_code: countryCode,
},
'post'
)
.then(user => {
if (user.is_identity_verified) {
dispatch({
type: ACTIONS.USER_PHONE_VERIFY_SUCCESS,
data: { user },
});
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
}
})
.catch(error => dispatch(doUserPhoneVerifyFailure(error)));
};
}
export function doUserEmailToVerify(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_SET,
data: { email },
});
};
}
export function doUserEmailNew(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_STARTED,
email,
});
const success = () => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
};
const failure = error => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_FAILURE,
data: { error },
});
};
Lbryio.call('user_email', 'new', { email, send_verification_email: true }, 'post')
.catch(error => {
if (error.response && error.response.status === 409) {
return Lbryio.call(
'user_email',
'resend_token',
{ email, only_if_expired: true },
'post'
).then(success, failure);
}
throw error;
})
.then(success, failure);
};
}
export function doUserResendVerificationEmail(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_RETRY,
email,
});
const success = () => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
};
const failure = error => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_FAILURE,
data: { error },
});
};
Lbryio.call('user_email', 'resend_token', { email }, 'post')
.catch(error => {
if (error.response && error.response.status === 409) {
throw error;
}
})
.then(success, failure);
};
}
export function doUserEmailVerifyFailure(error) {
return {
type: ACTIONS.USER_EMAIL_VERIFY_FAILURE,
data: { error },
};
}
export function doUserEmailVerify(verificationToken, recaptcha) {
return (dispatch, getState) => {
const email = selectEmailToVerify(getState());
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_STARTED,
code: verificationToken,
recaptcha,
});
Lbryio.call(
'user_email',
'confirm',
{
verification_token: verificationToken,
email,
recaptcha,
},
'post'
)
.then(userEmail => {
if (userEmail.is_verified) {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
} else {
throw new Error('Your email is still not verified.'); // shouldn't happen
}
})
.catch(error => dispatch(doUserEmailVerifyFailure(error)));
};
}
export function doFetchAccessToken() {
return dispatch => {
const success = token =>
dispatch({
type: ACTIONS.FETCH_ACCESS_TOKEN_SUCCESS,
data: { token },
});
Lbryio.getAuthToken().then(success);
};
}
export function doUserIdentityVerify(stripeToken) {
return dispatch => {
dispatch({
type: ACTIONS.USER_IDENTITY_VERIFY_STARTED,
token: stripeToken,
});
Lbryio.call('user', 'verify_identity', { stripe_token: stripeToken }, 'post')
.then(user => {
if (user.is_identity_verified) {
dispatch({
type: ACTIONS.USER_IDENTITY_VERIFY_SUCCESS,
data: { user },
});
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
} else {
throw new Error('Your identity is still not verified. This should not happen.'); // shouldn't happen
}
})
.catch(error => {
dispatch({
type: ACTIONS.USER_IDENTITY_VERIFY_FAILURE,
data: { error: error.toString() },
});
});
};
}
export function doUserInviteNew(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_INVITE_NEW_STARTED,
});
Lbryio.call('user', 'invite', { email }, 'post')
.then(() => {
dispatch({
type: ACTIONS.USER_INVITE_NEW_SUCCESS,
data: { email },
});
dispatch(
doToast({
message: __('Invite sent to %s', email),
})
);
dispatch(doFetchInviteStatus());
})
.catch(error => {
dispatch({
type: ACTIONS.USER_INVITE_NEW_FAILURE,
data: { error },
});
});
};
}

View file

@ -1,12 +0,0 @@
// @flow
import * as ACTIONS from 'constants/action_types';
export const doUpdateUploadProgress = (
progress: string,
params: { [key: string]: any },
xhr: any
) => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.UPDATE_UPLOAD_PROGRESS,
data: { progress, params, xhr },
});

View file

@ -0,0 +1,110 @@
import { ACTIONS } from 'lbry-redux';
const reducers = {};
const defaultState = {
fetching: false,
claimedRewardsById: {}, // id => reward
unclaimedRewards: [],
claimPendingByType: {},
claimErrorsByType: {},
rewardedContentClaimIds: [],
};
reducers[ACTIONS.FETCH_REWARDS_STARTED] = state =>
Object.assign({}, state, {
fetching: true,
});
reducers[ACTIONS.FETCH_REWARDS_COMPLETED] = (state, action) => {
const { userRewards } = action.data;
const unclaimedRewards = [];
const claimedRewards = {};
userRewards.forEach(reward => {
if (reward.transaction_id) {
claimedRewards[reward.id] = reward;
} else {
unclaimedRewards.push(reward);
}
});
return Object.assign({}, state, {
claimedRewardsById: claimedRewards,
unclaimedRewards,
fetching: false,
});
};
function setClaimRewardState(state, reward, isClaiming, errorMessage = '') {
const newClaimPendingByType = Object.assign({}, state.claimPendingByType);
const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType);
// Currently, for multiple rewards of the same type, they will both show "claiming" when one is beacuse we track this by `reward_type`
// To fix this we will need to use `claim_code` instead, and change all selectors to match
if (isClaiming) {
newClaimPendingByType[reward.reward_type] = isClaiming;
} else {
delete newClaimPendingByType[reward.reward_type];
}
if (errorMessage) {
newClaimErrorsByType[reward.reward_type] = errorMessage;
} else {
delete newClaimErrorsByType[reward.reward_type];
}
return Object.assign({}, state, {
claimPendingByType: newClaimPendingByType,
claimErrorsByType: newClaimErrorsByType,
});
}
reducers[ACTIONS.CLAIM_REWARD_STARTED] = (state, action) => {
const { reward } = action.data;
return setClaimRewardState(state, reward, true, '');
};
reducers[ACTIONS.CLAIM_REWARD_SUCCESS] = (state, action) => {
const { reward } = action.data;
const { unclaimedRewards } = state;
const index = unclaimedRewards.findIndex(ur => ur.claim_code === reward.claim_code);
unclaimedRewards.splice(index, 1);
const { claimedRewardsById } = state;
claimedRewardsById[reward.id] = reward;
const newState = {
...state,
unclaimedRewards: [...unclaimedRewards],
claimedRewardsById: { ...claimedRewardsById },
};
return setClaimRewardState(newState, reward, false, '');
};
reducers[ACTIONS.CLAIM_REWARD_FAILURE] = (state, action) => {
const { reward, error } = action.data;
return setClaimRewardState(state, reward, false, error ? error.message : '');
};
reducers[ACTIONS.CLAIM_REWARD_CLEAR_ERROR] = (state, action) => {
const { reward } = action.data;
return setClaimRewardState(state, reward, state.claimPendingByType[reward.reward_type], '');
};
reducers[ACTIONS.FETCH_REWARD_CONTENT_COMPLETED] = (state, action) => {
const { claimIds } = action.data;
return Object.assign({}, state, {
rewardedContentClaimIds: claimIds,
});
};
export function rewardsReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -5,9 +5,6 @@ const defaultState = {
fetchingViewCount: false,
viewCountError: undefined,
viewCountById: {},
fetchingSubCount: false,
subCountError: undefined,
subCountById: {},
};
export const statsReducer = handleActions(
@ -18,38 +15,15 @@ export const statsReducer = handleActions(
viewCountError: action.data,
}),
[ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => {
const { claimIdCsv, viewCounts } = action.data;
const viewCountById = Object.assign({}, state.viewCountById);
const claimIds = claimIdCsv.split(',');
if (claimIds.length === viewCounts.length) {
claimIds.forEach((claimId, index) => {
viewCountById[claimId] = viewCounts[index];
});
}
const { claimId, viewCount } = action.data;
const viewCountById = { ...state.viewCountById, [claimId]: viewCount };
return {
...state,
fetchingViewCount: false,
viewCountById,
};
},
[ACTIONS.FETCH_SUB_COUNT_STARTED]: state => ({ ...state, fetchingSubCount: true }),
[ACTIONS.FETCH_SUB_COUNT_FAILED]: (state, action) => ({
...state,
subCountError: action.data,
}),
[ACTIONS.FETCH_SUB_COUNT_COMPLETED]: (state, action) => {
const { claimId, subCount } = action.data;
const subCountById = { ...state.subCountById, [claimId]: subCount };
return {
...state,
fetchingSubCount: false,
subCountById,
};
},
},
defaultState
);

View file

@ -0,0 +1,213 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { VIEW_ALL } from 'constants/subscriptions';
import { handleActions } from 'util/redux-utils';
import type {
SubscriptionState,
Subscription,
DoChannelSubscribe,
DoChannelUnsubscribe,
DoChannelSubscriptionEnableNotifications,
DoChannelSubscriptionDisableNotifications,
SetSubscriptionLatest,
DoUpdateSubscriptionUnreads,
DoRemoveSubscriptionUnreads,
FetchedSubscriptionsSucess,
SetViewMode,
GetSuggestedSubscriptionsSuccess,
} from 'types/subscription';
const defaultState: SubscriptionState = {
enabledChannelNotifications: [],
subscriptions: [],
unread: {},
suggested: {},
loading: false,
viewMode: VIEW_ALL,
loadingSuggested: false,
firstRunCompleted: false,
showSuggestedSubs: false,
};
export default handleActions(
{
[ACTIONS.CHANNEL_SUBSCRIBE]: (
state: SubscriptionState,
action: DoChannelSubscribe
): SubscriptionState => {
const newSubscription: Subscription = action.data;
const newSubscriptions: Array<Subscription> = 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 => subscription.channelName !== subscriptionToRemove.channelName);
// Check if we need to remove it from the 'unread' state
const { unread } = state;
if (unread[subscriptionToRemove.uri]) {
delete unread[subscriptionToRemove.uri];
}
return {
...state,
unread: { ...unread },
subscriptions: newSubscriptions,
};
},
[ACTIONS.SET_SUBSCRIPTION_LATEST]: (
state: SubscriptionState,
action: SetSubscriptionLatest
): SubscriptionState => ({
...state,
subscriptions: state.subscriptions.map(
subscription =>
subscription.channelName === action.data.subscription.channelName
? { ...subscription, latest: action.data.uri }
: subscription
),
}),
[ACTIONS.UPDATE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: DoUpdateSubscriptionUnreads
): SubscriptionState => {
const { channel, uris, type } = action.data;
return {
...state,
unread: {
...state.unread,
[channel]: {
uris,
type,
},
},
};
},
[ACTIONS.REMOVE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: DoRemoveSubscriptionUnreads
): SubscriptionState => {
const { channel, uris } = action.data;
// If no channel is passed in, remove all unreads
let newUnread;
if (channel) {
newUnread = { ...state.unread };
if (!uris) {
delete newUnread[channel];
} else {
newUnread[channel].uris = uris;
}
} else {
newUnread = {};
}
return {
...state,
unread: {
...newUnread,
},
};
},
[ACTIONS.CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS]: (
state: SubscriptionState,
action: DoChannelSubscriptionEnableNotifications
): SubscriptionState => {
const channelName = action.data;
const newEnabledChannelNotifications: Array<
string
> = state.enabledChannelNotifications.slice();
if (
channelName &&
channelName.trim().length > 0 &&
newEnabledChannelNotifications.indexOf(channelName) === -1
) {
newEnabledChannelNotifications.push(channelName);
}
return {
...state,
enabledChannelNotifications: newEnabledChannelNotifications,
};
},
[ACTIONS.CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS]: (
state: SubscriptionState,
action: DoChannelSubscriptionDisableNotifications
): SubscriptionState => {
const channelName = action.data;
const newEnabledChannelNotifications: Array<
string
> = state.enabledChannelNotifications.slice();
const index = newEnabledChannelNotifications.indexOf(channelName);
if (index > -1) {
newEnabledChannelNotifications.splice(index, 1);
}
return {
...state,
enabledChannelNotifications: newEnabledChannelNotifications,
};
},
[ACTIONS.FETCH_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
...state,
loading: true,
}),
[ACTIONS.FETCH_SUBSCRIPTIONS_FAIL]: (state: SubscriptionState): SubscriptionState => ({
...state,
loading: false,
}),
[ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: (
state: SubscriptionState,
action: FetchedSubscriptionsSucess
): SubscriptionState => ({
...state,
loading: false,
subscriptions: action.data,
}),
[ACTIONS.SET_VIEW_MODE]: (
state: SubscriptionState,
action: SetViewMode
): SubscriptionState => ({
...state,
viewMode: action.data,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: true,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS]: (
state: SubscriptionState,
action: GetSuggestedSubscriptionsSuccess
): SubscriptionState => ({
...state,
suggested: action.data,
loadingSuggested: false,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: false,
}),
[ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED]: (state: SubscriptionState): SubscriptionState => ({
...state,
firstRunCompleted: true,
}),
[ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({
...state,
showSuggestedSubs: true,
}),
},
defaultState
);

View file

@ -6,10 +6,8 @@ const defaultState = {
syncHash: null,
syncData: null,
setSyncErrorMessage: null,
getSyncErrorMessage: null,
syncApplyErrorMessage: '',
syncApplyIsPending: false,
syncApplyPasswordError: false,
getSyncIsPending: false,
setSyncIsPending: false,
hashChanged: false,
@ -18,7 +16,6 @@ const defaultState = {
reducers[ACTIONS.GET_SYNC_STARTED] = state =>
Object.assign({}, state, {
getSyncIsPending: true,
getSyncErrorMessage: null,
});
reducers[ACTIONS.GET_SYNC_COMPLETED] = (state, action) =>
@ -30,12 +27,6 @@ reducers[ACTIONS.GET_SYNC_COMPLETED] = (state, action) =>
hashChanged: action.data.hashChanged,
});
reducers[ACTIONS.GET_SYNC_FAILED] = (state, action) =>
Object.assign({}, state, {
getSyncIsPending: false,
getSyncErrorMessage: action.data.error,
});
reducers[ACTIONS.SET_SYNC_STARTED] = state =>
Object.assign({}, state, {
setSyncIsPending: true,
@ -58,7 +49,6 @@ reducers[ACTIONS.SET_SYNC_COMPLETED] = (state, action) =>
reducers[ACTIONS.SYNC_APPLY_STARTED] = state =>
Object.assign({}, state, {
syncApplyPasswordError: false,
syncApplyIsPending: true,
syncApplyErrorMessage: '',
});
@ -75,13 +65,6 @@ reducers[ACTIONS.SYNC_APPLY_FAILED] = (state, action) =>
syncApplyErrorMessage: action.data.error,
});
reducers[ACTIONS.SYNC_APPLY_BAD_PASSWORD] = state =>
Object.assign({}, state, {
syncApplyPasswordError: true,
});
reducers[ACTIONS.SYNC_RESET] = () => defaultState;
export function syncReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

227
src/redux/reducers/user.js Normal file
View file

@ -0,0 +1,227 @@
import { ACTIONS } from 'lbry-redux';
const reducers = {};
const defaultState = {
authenticationIsPending: false,
userIsPending: false,
emailNewIsPending: false,
emailNewErrorMessage: '',
emailToVerify: '',
inviteNewErrorMessage: '',
inviteNewIsPending: false,
inviteStatusIsPending: false,
invitesRemaining: undefined,
invitees: undefined,
user: undefined,
};
reducers[ACTIONS.AUTHENTICATION_STARTED] = state =>
Object.assign({}, state, {
authenticationIsPending: true,
userIsPending: true,
user: defaultState.user,
});
reducers[ACTIONS.AUTHENTICATION_SUCCESS] = (state, action) =>
Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
user: action.data.user,
});
reducers[ACTIONS.AUTHENTICATION_FAILURE] = state =>
Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
user: null,
});
reducers[ACTIONS.USER_FETCH_STARTED] = state =>
Object.assign({}, state, {
userIsPending: true,
});
reducers[ACTIONS.USER_FETCH_SUCCESS] = (state, action) =>
Object.assign({}, state, {
userIsPending: false,
user: action.data.user,
});
reducers[ACTIONS.USER_FETCH_FAILURE] = state =>
Object.assign({}, state, {
userIsPending: true,
user: null,
});
reducers[ACTIONS.USER_PHONE_NEW_STARTED] = (state, action) => {
const user = Object.assign({}, state.user);
user.country_code = action.data.country_code;
return Object.assign({}, state, {
phoneNewIsPending: true,
phoneNewErrorMessage: '',
user,
});
};
reducers[ACTIONS.USER_PHONE_NEW_SUCCESS] = (state, action) =>
Object.assign({}, state, {
phoneToVerify: action.data.phone,
phoneNewIsPending: false,
});
reducers[ACTIONS.USER_PHONE_RESET] = state =>
Object.assign({}, state, {
phoneToVerify: null,
});
reducers[ACTIONS.USER_PHONE_NEW_FAILURE] = (state, action) =>
Object.assign({}, state, {
phoneNewIsPending: false,
phoneNewErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_PHONE_VERIFY_STARTED] = state =>
Object.assign({}, state, {
phoneVerifyIsPending: true,
phoneVerifyErrorMessage: '',
});
reducers[ACTIONS.USER_PHONE_VERIFY_SUCCESS] = (state, action) =>
Object.assign({}, state, {
phoneToVerify: '',
phoneVerifyIsPending: false,
user: action.data.user,
});
reducers[ACTIONS.USER_PHONE_VERIFY_FAILURE] = (state, action) =>
Object.assign({}, state, {
phoneVerifyIsPending: false,
phoneVerifyErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_EMAIL_NEW_STARTED] = state =>
Object.assign({}, state, {
emailNewIsPending: true,
emailNewErrorMessage: '',
});
reducers[ACTIONS.USER_EMAIL_NEW_SUCCESS] = (state, action) => {
const user = Object.assign({}, state.user);
user.primary_email = action.data.email;
return Object.assign({}, state, {
emailToVerify: action.data.email,
emailNewIsPending: false,
user,
});
};
reducers[ACTIONS.USER_EMAIL_NEW_EXISTS] = (state, action) =>
Object.assign({}, state, {
emailToVerify: action.data.email,
emailNewIsPending: false,
});
reducers[ACTIONS.USER_EMAIL_NEW_FAILURE] = (state, action) =>
Object.assign({}, state, {
emailNewIsPending: false,
emailNewErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_EMAIL_VERIFY_STARTED] = state =>
Object.assign({}, state, {
emailVerifyIsPending: true,
emailVerifyErrorMessage: '',
});
reducers[ACTIONS.USER_EMAIL_VERIFY_SUCCESS] = (state, action) => {
const user = Object.assign({}, state.user);
user.primary_email = action.data.email;
return Object.assign({}, state, {
emailToVerify: '',
emailVerifyIsPending: false,
user,
});
};
reducers[ACTIONS.USER_EMAIL_VERIFY_FAILURE] = (state, action) =>
Object.assign({}, state, {
emailVerifyIsPending: false,
emailVerifyErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_EMAIL_VERIFY_SET] = (state, action) =>
Object.assign({}, state, {
emailToVerify: action.data.email,
});
reducers[ACTIONS.USER_IDENTITY_VERIFY_STARTED] = state =>
Object.assign({}, state, {
identityVerifyIsPending: true,
identityVerifyErrorMessage: '',
});
reducers[ACTIONS.USER_IDENTITY_VERIFY_SUCCESS] = (state, action) =>
Object.assign({}, state, {
identityVerifyIsPending: false,
identityVerifyErrorMessage: '',
user: action.data.user,
});
reducers[ACTIONS.USER_IDENTITY_VERIFY_FAILURE] = (state, action) =>
Object.assign({}, state, {
identityVerifyIsPending: false,
identityVerifyErrorMessage: action.data.error,
});
reducers[ACTIONS.FETCH_ACCESS_TOKEN_SUCCESS] = (state, action) => {
const { token } = action.data;
return Object.assign({}, state, {
accessToken: token,
});
};
reducers[ACTIONS.USER_INVITE_STATUS_FETCH_STARTED] = state =>
Object.assign({}, state, {
inviteStatusIsPending: true,
});
reducers[ACTIONS.USER_INVITE_STATUS_FETCH_SUCCESS] = (state, action) =>
Object.assign({}, state, {
inviteStatusIsPending: false,
invitesRemaining: action.data.invitesRemaining,
invitees: action.data.invitees,
referralLink: action.data.referralLink,
});
reducers[ACTIONS.USER_INVITE_NEW_STARTED] = state =>
Object.assign({}, state, {
inviteNewIsPending: true,
inviteNewErrorMessage: '',
});
reducers[ACTIONS.USER_INVITE_NEW_SUCCESS] = state =>
Object.assign({}, state, {
inviteNewIsPending: false,
inviteNewErrorMessage: '',
});
reducers[ACTIONS.USER_INVITE_NEW_FAILURE] = (state, action) =>
Object.assign({}, state, {
inviteNewIsPending: false,
inviteNewErrorMessage: action.data.error.message,
});
reducers[ACTIONS.USER_INVITE_STATUS_FETCH_FAILURE] = state =>
Object.assign({}, state, {
inviteStatusIsPending: false,
invitesRemaining: null,
invitees: null,
});
export function userReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -1,62 +0,0 @@
// @flow
import * as ACTIONS from 'constants/action_types';
/*
test mock:
currentUploads: {
'test#upload': {
progress: 50,
params: {
name: 'steve',
thumbnail_url: 'https://dev2.spee.ch/4/KMNtoSZ009fawGz59VG8PrID.jpeg',
},
},
},
*/
export type Params = {
channel?: string,
name: string,
thumbnail_url: ?string,
title: ?string,
};
export type UploadItem = {
progess: string,
params: Params,
xhr?: any,
};
export type TvState = {
currentUploads: { [key: string]: UploadItem },
};
const reducers = {};
const defaultState: TvState = {
currentUploads: {},
};
reducers[ACTIONS.UPDATE_UPLOAD_PROGRESS] = (state: TvState, action) => {
const { progress, params, xhr } = action.data;
const key = params.channel ? `${params.name}#${params.channel}` : `${params.name}#anonymous`;
let currentUploads;
if (!progress) {
currentUploads = Object.assign({}, state.currentUploads);
Object.keys(currentUploads).forEach(k => {
if (k === key) {
delete currentUploads[key];
}
});
} else {
currentUploads = Object.assign({}, state.currentUploads);
currentUploads[key] = { progress, params, xhr };
}
return { ...state, currentUploads };
};
export function webReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -6,15 +6,3 @@ export const selectBlackListedOutpoints = createSelector(
selectState,
state => state.blackListedOutpoints
);
export const selectBlacklistedOutpointMap = createSelector(
selectBlackListedOutpoints,
outpoints =>
outpoints
? outpoints.reduce((acc, val) => {
const outpoint = `${val.txid}:${val.nout}`;
acc[outpoint] = 1;
return acc;
}, {})
: {}
);

View file

@ -6,15 +6,3 @@ export const selectFilteredOutpoints = createSelector(
selectState,
state => state.filteredOutpoints
);
export const selectFilteredOutpointMap = createSelector(
selectFilteredOutpoints,
outpoints =>
outpoints
? outpoints.reduce((acc, val) => {
const outpoint = `${val.txid}:${val.nout}`;
acc[outpoint] = 1;
return acc;
}, {})
: {}
);

View file

@ -0,0 +1,76 @@
import { createSelector } from 'reselect';
import REWARDS from 'rewards';
const selectState = state => state.rewards || {};
export const selectUnclaimedRewardsByType = createSelector(
selectState,
state => state.unclaimedRewardsByType
);
export const selectClaimedRewardsById = createSelector(
selectState,
state => state.claimedRewardsById
);
export const selectClaimedRewards = createSelector(
selectClaimedRewardsById,
byId => Object.values(byId) || []
);
export const selectClaimedRewardsByTransactionId = createSelector(selectClaimedRewards, rewards =>
rewards.reduce((mapParam, reward) => {
const map = mapParam;
map[reward.transaction_id] = reward;
return map;
}, {})
);
export const selectUnclaimedRewards = createSelector(selectState, state => state.unclaimedRewards);
export const selectFetchingRewards = createSelector(selectState, state => !!state.fetching);
export const selectUnclaimedRewardValue = createSelector(selectUnclaimedRewards, rewards =>
rewards.reduce((sum, reward) => sum + reward.reward_amount, 0)
);
export const selectClaimsPendingByType = createSelector(
selectState,
state => state.claimPendingByType
);
const selectIsClaimRewardPending = (state, props) =>
selectClaimsPendingByType(state, props)[props.reward_type];
export const makeSelectIsRewardClaimPending = () =>
createSelector(selectIsClaimRewardPending, isClaiming => isClaiming);
export const selectClaimErrorsByType = createSelector(
selectState,
state => state.claimErrorsByType
);
const selectClaimRewardError = (state, props) =>
selectClaimErrorsByType(state, props)[props.reward_type];
export const makeSelectClaimRewardError = () =>
createSelector(selectClaimRewardError, errorMessage => errorMessage);
const selectRewardByType = (state, rewardType) =>
selectUnclaimedRewards(state).find(reward => reward.reward_type === rewardType);
export const makeSelectRewardByType = () => createSelector(selectRewardByType, reward => reward);
export const makeSelectRewardAmountByType = () =>
createSelector(selectRewardByType, reward => (reward ? reward.reward_amount : 0));
export const selectRewardContentClaimIds = createSelector(
selectState,
state => state.rewardedContentClaimIds
);
export const selectReferralReward = createSelector(
selectUnclaimedRewards,
unclaimedRewards =>
unclaimedRewards.filter(reward => reward.reward_type === REWARDS.TYPE_REFERRAL)[0]
);

View file

@ -3,18 +3,10 @@ import { makeSelectClaimForUri } from 'lbry-redux';
const selectState = state => state.stats || {};
export const selectViewCount = createSelector(selectState, state => state.viewCountById);
export const selectSubCount = createSelector(selectState, state => state.subCountById);
export const makeSelectViewCountForUri = uri =>
createSelector(
makeSelectClaimForUri(uri),
selectViewCount,
(claim, viewCountById) => (claim ? viewCountById[claim.claim_id] || 0 : 0)
);
export const makeSelectSubCountForUri = uri =>
createSelector(
makeSelectClaimForUri(uri),
selectSubCount,
(claim, subCountById) => (claim ? subCountById[claim.claim_id] || 0 : 0)
(claim, viewCountById) => viewCountById[claim.claim_id] || 0
);

View file

@ -0,0 +1,283 @@
import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions';
import { createSelector } from 'reselect';
import {
selectAllClaimsByChannel,
selectClaimsById,
selectAllFetchingChannelClaims,
makeSelectChannelForClaimUri,
selectClaimsByUri,
parseURI,
} from 'lbry-redux';
import { swapKeyAndValue } from 'util/swap-json';
// Returns the entire subscriptions state
const selectState = state => state.subscriptions || {};
// Returns the list of channel uris a user is subscribed to
export const selectSubscriptions = createSelector(selectState, state => state.subscriptions);
// Fetching list of users subscriptions
export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading);
// The current view mode on the subscriptions page
export const selectViewMode = createSelector(selectState, state => state.viewMode);
// Suggested subscriptions from internal apis
export const selectSuggested = createSelector(selectState, state => state.suggested);
export const selectIsFetchingSuggested = createSelector(
selectState,
state => state.loadingSuggested
);
export const selectSuggestedChannels = createSelector(
selectSubscriptions,
selectSuggested,
(userSubscriptions, suggested) => {
if (!suggested) {
return null;
}
// Swap the key/value because we will use the uri for everything, this just makes it easier
// suggested is returned from the api with the form:
// {
// featured: { "Channel label": uri, ... },
// top_subscribed: { "@channel": uri, ... }
// top_bid: { "@channel": uri, ... }
// }
// To properly compare the suggested subscriptions from our current subscribed channels
// We only care about the uri, not the label
// We also only care about top_subscribed and featured
// top_bid could just be porn or a channel with no content
const topSubscribedSuggestions = swapKeyAndValue(suggested[SUGGESTED_TOP_SUBSCRIBED]);
const featuredSuggestions = swapKeyAndValue(suggested[SUGGESTED_FEATURED]);
// Make sure there are no duplicates
// If a uri isn't already in the suggested object, add it
const suggestedChannels = { ...topSubscribedSuggestions };
Object.keys(featuredSuggestions).forEach(uri => {
if (!suggestedChannels[uri]) {
const channelLabel = featuredSuggestions[uri];
suggestedChannels[uri] = channelLabel;
}
});
userSubscriptions.forEach(({ uri }) => {
// Note to passer bys:
// Maybe we should just remove the `lbry://` prefix from subscription uris
// Most places don't store them like that
const subscribedUri = uri.slice('lbry://'.length);
if (suggestedChannels[subscribedUri]) {
delete suggestedChannels[subscribedUri];
}
});
return Object.keys(suggestedChannels)
.map(uri => ({
uri,
label: suggestedChannels[uri],
}))
.slice(0, 5);
}
);
export const selectFirstRunCompleted = createSelector(
selectState,
state => state.firstRunCompleted
);
export const selectShowSuggestedSubs = createSelector(
selectState,
state => state.showSuggestedSubs
);
// Fetching any claims that are a part of a users subscriptions
export const selectSubscriptionsBeingFetched = createSelector(
selectSubscriptions,
selectAllFetchingChannelClaims,
(subscriptions, fetchingChannelClaims) => {
const fetchingSubscriptionMap = {};
subscriptions.forEach(sub => {
const isFetching = fetchingChannelClaims && fetchingChannelClaims[sub.uri];
if (isFetching) {
fetchingSubscriptionMap[sub.uri] = true;
}
});
return fetchingSubscriptionMap;
}
);
export const selectUnreadByChannel = createSelector(selectState, state => state.unread);
// Returns the current total of unread subscriptions
export const selectUnreadAmount = createSelector(selectUnreadByChannel, unreadByChannel => {
const unreadChannels = Object.keys(unreadByChannel);
let badges = 0;
if (!unreadChannels.length) {
return badges;
}
unreadChannels.forEach(channel => {
badges += unreadByChannel[channel].uris.length;
});
return badges;
});
// Returns the uris with channels as an array with the channel with the newest content first
// If you just want the `unread` state, use selectUnread
export const selectUnreadSubscriptions = createSelector(
selectUnreadAmount,
selectUnreadByChannel,
selectClaimsByUri,
(unreadAmount, unreadByChannel, claimsByUri) => {
// determine which channel has the newest content
const unreadList = [];
if (!unreadAmount) {
return unreadList;
}
const channelUriList = Object.keys(unreadByChannel);
// There is only one channel with unread notifications
if (unreadAmount === 1) {
channelUriList.forEach(channel => {
const unreadChannel = {
channel,
uris: unreadByChannel[channel].uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
channelUriList
.sort((channel1, channel2) => {
const latestUriFromChannel1 = unreadByChannel[channel1].uris[0];
const latestClaimFromChannel1 = claimsByUri[latestUriFromChannel1] || {};
const latestUriFromChannel2 = unreadByChannel[channel2].uris[0];
const latestClaimFromChannel2 = claimsByUri[latestUriFromChannel2] || {};
const latestHeightFromChannel1 = latestClaimFromChannel1.height || 0;
const latestHeightFromChannel2 = latestClaimFromChannel2.height || 0;
if (latestHeightFromChannel1 !== latestHeightFromChannel2) {
return latestHeightFromChannel2 - latestHeightFromChannel1;
}
return 0;
})
.forEach(channel => {
const unreadSubscription = unreadByChannel[channel];
const unreadChannel = {
channel,
uris: unreadSubscription.uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
);
// Returns all unread subscriptions for a uri passed in
export const makeSelectUnreadByChannel = uri =>
createSelector(selectUnreadByChannel, unread => unread[uri]);
// Returns the first page of claims for every channel a user is subscribed to
export const selectSubscriptionClaims = createSelector(
selectAllClaimsByChannel,
selectClaimsById,
selectSubscriptions,
selectUnreadByChannel,
(channelIds, allClaims, savedSubscriptions, unreadByChannel) => {
// 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] && channelIds[subscription.uri]['1']) {
// This will need to be more robust, we will want to be able to load more than the first page
// Strip out any ids that will be shown as notifications
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];
if (
unreadByChannel[subscription.uri] &&
unreadByChannel[subscription.uri].uris.some(uri => uri.includes(id))
) {
grabbedClaim.isNew = true;
}
channelClaims = channelClaims.concat([grabbedClaim]);
});
}
fetchedSubscriptions = fetchedSubscriptions.concat(channelClaims);
});
return fetchedSubscriptions;
}
);
// Returns true if a user is subscribed to the channel associated with the uri passed in
// Accepts content or channel uris
export const makeSelectIsSubscribed = uri =>
createSelector(
selectSubscriptions,
makeSelectChannelForClaimUri(uri, true),
(subscriptions, channelUri) => {
if (channelUri) {
return subscriptions.some(sub => sub.uri === channelUri);
}
// If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already
const { isChannel } = parseURI(uri);
if (isChannel) {
const uriWithPrefix = uri.startsWith('lbry://') ? uri : `lbry://${uri}`;
return subscriptions.some(sub => sub.uri === uriWithPrefix);
}
return false;
}
);
export const makeSelectIsNew = uri =>
createSelector(
makeSelectIsSubscribed(uri),
makeSelectChannelForClaimUri(uri),
selectUnreadByChannel,
(isSubscribed, channel, unreadByChannel) => {
if (!isSubscribed) {
return false;
}
const unreadForChannel = unreadByChannel[`lbry://${channel}`];
if (unreadForChannel) {
return unreadForChannel.uris.includes(uri);
}
return false;
// If they are subscribed, check to see if this uri is in the list of unreads
}
);
export const selectEnabledChannelNotifications = createSelector(
selectState,
state => state.enabledChannelNotifications
);

View file

@ -13,11 +13,6 @@ export const selectSetSyncErrorMessage = createSelector(
state => state.setSyncErrorMessage
);
export const selectGetSyncErrorMessage = createSelector(
selectState,
state => state.getSyncErrorMessage
);
export const selectGetSyncIsPending = createSelector(selectState, state => state.getSyncIsPending);
export const selectSetSyncIsPending = createSelector(selectState, state => state.setSyncIsPending);
@ -33,8 +28,3 @@ export const selectSyncApplyErrorMessage = createSelector(
selectState,
state => state.syncApplyErrorMessage
);
export const selectSyncApplyPasswordError = createSelector(
selectState,
state => state.syncApplyPasswordError
);

133
src/redux/selectors/user.js Normal file
View file

@ -0,0 +1,133 @@
import { createSelector } from 'reselect';
export const selectState = state => state.user || {};
export const selectAuthenticationIsPending = createSelector(
selectState,
state => state.authenticationIsPending
);
export const selectUserIsPending = createSelector(selectState, state => state.userIsPending);
export const selectUser = createSelector(selectState, state => state.user);
export const selectUserEmail = createSelector(
selectUser,
user => (user ? user.primary_email : null)
);
export const selectUserPhone = createSelector(
selectUser,
user => (user ? user.phone_number : null)
);
export const selectUserCountryCode = createSelector(
selectUser,
user => (user ? user.country_code : null)
);
export const selectEmailToVerify = createSelector(
selectState,
selectUserEmail,
(state, userEmail) => state.emailToVerify || userEmail
);
export const selectPhoneToVerify = createSelector(
selectState,
selectUserPhone,
(state, userPhone) => state.phoneToVerify || userPhone
);
export const selectUserIsRewardApproved = createSelector(
selectUser,
user => user && user.is_reward_approved
);
export const selectEmailNewIsPending = createSelector(
selectState,
state => state.emailNewIsPending
);
export const selectEmailNewErrorMessage = createSelector(
selectState,
state => state.emailNewErrorMessage
);
export const selectPhoneNewErrorMessage = createSelector(
selectState,
state => state.phoneNewErrorMessage
);
export const selectEmailVerifyIsPending = createSelector(
selectState,
state => state.emailVerifyIsPending
);
export const selectEmailVerifyErrorMessage = createSelector(
selectState,
state => state.emailVerifyErrorMessage
);
export const selectPhoneNewIsPending = createSelector(
selectState,
state => state.phoneNewIsPending
);
export const selectPhoneVerifyIsPending = createSelector(
selectState,
state => state.phoneVerifyIsPending
);
export const selectPhoneVerifyErrorMessage = createSelector(
selectState,
state => state.phoneVerifyErrorMessage
);
export const selectIdentityVerifyIsPending = createSelector(
selectState,
state => state.identityVerifyIsPending
);
export const selectIdentityVerifyErrorMessage = createSelector(
selectState,
state => state.identityVerifyErrorMessage
);
export const selectUserIsVerificationCandidate = createSelector(
selectUser,
user => user && (!user.has_verified_email || !user.is_identity_verified)
);
export const selectAccessToken = createSelector(selectState, state => state.accessToken);
export const selectUserInviteStatusIsPending = createSelector(
selectState,
state => state.inviteStatusIsPending
);
export const selectUserInvitesRemaining = createSelector(
selectState,
state => state.invitesRemaining
);
export const selectUserInvitees = createSelector(selectState, state => state.invitees);
export const selectUserInviteStatusFailed = createSelector(
selectUserInvitesRemaining,
() => selectUserInvitesRemaining === null
);
export const selectUserInviteNewIsPending = createSelector(
selectState,
state => state.inviteNewIsPending
);
export const selectUserInviteNewErrorMessage = createSelector(
selectState,
state => state.inviteNewErrorMessage
);
export const selectUserInviteReferralLink = createSelector(
selectState,
state => state.referralLink
);

View file

@ -1,10 +0,0 @@
import { createSelector } from 'reselect';
const selectState = state => state.web || {};
export const selectCurrentUploads = createSelector(selectState, state => state.currentUploads);
export const selectUploadCount = createSelector(
selectCurrentUploads,
currentUploads => currentUploads && Object.keys(currentUploads).length
);

122
src/rewards.js Normal file
View file

@ -0,0 +1,122 @@
import { Lbry, doToast } from 'lbry-redux';
import Lbryio from 'lbryio';
const rewards = {};
rewards.TYPE_NEW_DEVELOPER = 'new_developer';
rewards.TYPE_NEW_USER = 'new_user';
rewards.TYPE_CONFIRM_EMAIL = 'verified_email';
rewards.TYPE_FIRST_CHANNEL = 'new_channel';
rewards.TYPE_FIRST_STREAM = 'first_stream';
rewards.TYPE_MANY_DOWNLOADS = 'many_downloads';
rewards.TYPE_FIRST_PUBLISH = 'first_publish';
rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download';
rewards.TYPE_REFERRAL = 'referral';
rewards.TYPE_REWARD_CODE = 'reward_code';
rewards.TYPE_SUBSCRIPTION = 'subscription';
rewards.YOUTUBE_CREATOR = 'youtube_creator';
rewards.claimReward = (type, rewardParams) => {
function requestReward(resolve, reject, params) {
if (!Lbryio.enabled) {
reject(new Error(__('Rewards are not enabled.')));
return;
}
Lbryio.call('reward', 'new', params, 'post').then(reward => {
const message =
reward.reward_notification || `You have claimed a ${reward.reward_amount} LBC reward.`;
// Display global notice
const action = doToast({
message,
linkText: __('Show All'),
linkTarget: '/rewards',
});
window.store.dispatch(action);
if (rewards.callbacks.claimRewardSuccess) {
rewards.callbacks.claimRewardSuccess();
}
resolve(reward);
}, reject);
}
return new Promise((resolve, reject) => {
Lbry.address_unused().then(address => {
const params = {
reward_type: type,
wallet_address: address,
...rewardParams,
};
switch (type) {
case rewards.TYPE_FIRST_CHANNEL:
Lbry.claim_list()
.then(claims => {
const claim = claims.find(
foundClaim =>
foundClaim.name.length &&
foundClaim.name[0] === '@' &&
foundClaim.txid.length &&
foundClaim.type === 'claim'
);
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(new Error(__('Please create a channel identity first.')));
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_PUBLISH:
Lbry.claim_list()
.then(claims => {
const claim = claims.find(
foundClaim =>
foundClaim.name.length &&
foundClaim.name[0] !== '@' &&
foundClaim.txid.length &&
foundClaim.type === 'claim'
);
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(
claims.length
? new Error(
__(
'Please publish something and wait for confirmation by the network to claim this reward.'
)
)
: new Error(__('Please publish something to claim this reward.'))
);
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER:
default:
requestReward(resolve, reject, params);
}
});
});
};
rewards.callbacks = {
// Set any callbacks that require code not found in this project
claimRewardSuccess: null,
claimFirstRewardSuccess: null,
rewardApprovalRequired: null,
};
rewards.setCallback = (name, method) => {
rewards.callbacks[name] = method;
};
export default rewards;

6
src/types/redux.js Normal file
View file

@ -0,0 +1,6 @@
// @flow
// eslint-disable-next-line no-use-before-define
export type Dispatch<T> = (action: T | Promise<T> | Array<T> | ThunkAction<T>) => any; // Need to refer to ThunkAction
export type GetState = () => any;
export type ThunkAction<T> = (dispatch: Dispatch<T>, getState: GetState) => any;

137
src/types/subscription.js Normal file
View file

@ -0,0 +1,137 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'types/redux';
import * as ACTIONS from 'constants/action_types';
import {
DOWNLOADED,
DOWNLOADING,
NOTIFY_ONLY,
VIEW_ALL,
VIEW_LATEST_FIRST,
SUGGESTED_TOP_BID,
SUGGESTED_TOP_SUBSCRIBED,
SUGGESTED_FEATURED,
} from 'constants/subscriptions';
export type Subscription = {
channelName: string, // @CryptoCandor,
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
latest?: string, // substratum#b0ab143243020e7831fd070d9f871e1fda948620
};
// Tracking for new content
// i.e. If a subscription has a DOWNLOADING type, we will trigger an OS notification
// to tell users there is new content from their subscriptions
export type SubscriptionNotificationType = DOWNLOADED | DOWNLOADING | NOTIFY_ONLY;
export type UnreadSubscription = {
type: SubscriptionNotificationType,
uris: Array<string>,
};
export type UnreadSubscriptions = {
[string]: UnreadSubscription,
};
export type ViewMode = VIEW_LATEST_FIRST | VIEW_ALL;
export type SuggestedType = SUGGESTED_TOP_BID | SUGGESTED_TOP_SUBSCRIBED | SUGGESTED_FEATURED;
export type SuggestedSubscriptions = {
[SuggestedType]: string,
};
export type SubscriptionState = {
enabledChannelNotifications: Array<string>,
subscriptions: Array<Subscription>,
unread: UnreadSubscriptions,
loading: boolean,
viewMode: ViewMode,
suggested: SuggestedSubscriptions,
loadingSuggested: boolean,
firstRunCompleted: boolean,
showSuggestedSubs: boolean,
};
//
// Action types
//
export type DoChannelSubscriptionEnableNotifications = {
type: ACTIONS.CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS,
data: string,
};
export type DoChannelSubscriptionDisableNotifications = {
type: ACTIONS.CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS,
data: string,
};
export type DoChannelSubscribe = {
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: Subscription,
};
export type DoChannelUnsubscribe = {
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: Subscription,
};
export type DoUpdateSubscriptionUnreads = {
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
type?: SubscriptionNotificationType,
},
};
export type DoRemoveSubscriptionUnreads = {
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
},
};
export type SetSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
export type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
export type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
export type FetchedSubscriptionsSucess = {
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: Array<Subscription>,
};
export type SetViewMode = {
type: ACTIONS.SET_VIEW_MODE,
data: ViewMode,
};
export type GetSuggestedSubscriptionsSuccess = {
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
data: SuggestedSubscriptions,
};
export type Action =
| DoChannelSubscribe
| DoChannelUnsubscribe
| DoUpdateSubscriptionUnreads
| DoRemoveSubscriptionUnreads
| SetSubscriptionLatest
| CheckSubscriptionStarted
| CheckSubscriptionCompleted
| SetViewMode
| Function;
export type Dispatch = ReduxDispatch<Action>;

View file

@ -1,78 +0,0 @@
const apiBaseUrl = 'https://www.transifex.com/api/2/project';
const resource = 'app-strings';
export function doTransifexUpload(contents, project, token, success, fail) {
const url = `${apiBaseUrl}/${project}/resources/`;
const updateUrl = `${apiBaseUrl}/${project}/resource/${resource}/content/`;
const headers = {
Authorization: `Basic ${Buffer.from(`api:${token}`).toString('base64')}`,
'Content-Type': 'application/json',
};
const req = {
accept_translations: true,
i18n_type: 'KEYVALUEJSON',
name: resource,
slug: resource,
content: contents,
};
function handleResponse(text) {
let json;
try {
// transifex api returns Python dicts for some reason.
// Any way to get the api to return valid JSON?
json = JSON.parse(text);
} catch (e) {
// ignore
}
if (success) {
success(json || text);
}
}
function handleError(err) {
if (fail) {
fail(err.message ? err.message : 'Could not upload strings resource to Transifex');
}
}
// check if the resource exists
fetch(updateUrl, { headers })
.then(response => response.json())
.then(() => {
// perform an update
fetch(updateUrl, {
method: 'PUT',
headers,
body: JSON.stringify({ content: contents }),
})
.then(response => {
if (response.status !== 200 && response.status !== 201) {
throw new Error('failed to update transifex');
}
return response.text();
})
.then(handleResponse)
.catch(handleError);
})
.catch(() => {
// resource doesn't exist, create a fresh resource
fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(req),
})
.then(response => {
if (response.status !== 200 && response.status !== 201) {
throw new Error('failed to upload to transifex');
}
return response.text();
})
.then(handleResponse)
.catch(handleError);
});
}

View file

@ -20,9 +20,6 @@ module.exports = {
},
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
alias: {
'flow-typed': path.resolve(__dirname, './flow-typed'),
},
},
externals: 'lbry-redux',
};

251
yarn.lock
View file

@ -642,27 +642,6 @@
lodash "^4.17.11"
to-fast-properties "^2.0.0"
"@nodelib/fs.scandir@2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.2.tgz#1f981cd5b83e85cfdeb386fc693d4baab392fa54"
integrity sha512-wrIBsjA5pl13f0RN4Zx4FNWmU71lv03meGKnqRUoCyan17s4V3WL92f3w3AIuWbNnpcrQyFBU5qMavJoB8d27w==
dependencies:
"@nodelib/fs.stat" "2.0.2"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.2", "@nodelib/fs.stat@^2.0.1":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.2.tgz#2762aea8fe78ea256860182dcb52d61ee4b8fda6"
integrity sha512-z8+wGWV2dgUhLqrtRYa03yDx4HWMvXKi1z8g3m2JyxAx8F7xk74asqPk5LAETjqDSGLFML/6CDl0+yFunSYicw==
"@nodelib/fs.walk@^1.2.1":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.3.tgz#a555dc256acaf00c62b0db29529028dd4d4cb141"
integrity sha512-l6t8xEhfK9Sa4YO5mIRdau7XSOADfmh3jCr0evNHdY+HNkW6xuQhgMH7D73VV6WpZOagrW0UludvMTiifiwTfA==
dependencies:
"@nodelib/fs.scandir" "2.1.2"
fastq "^1.6.0"
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
@ -673,37 +652,6 @@
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/fs-extra@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.0.0.tgz#d3e2c313ca29f95059f198dd60d1f774642d4b25"
integrity sha512-bCtL5v9zdbQW86yexOlXWTEGvLNqWxMFyi7gQA7Gcthbezr2cPSOb8SkESVKA937QD5cIwOFLDFt0MQoXOEr9Q==
dependencies:
"@types/node" "*"
"@types/glob@^7.1.1":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
version "12.7.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.5.tgz#e19436e7f8e9b4601005d73673b6dc4784ffcc2f"
integrity sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==
"@types/node@^11.11.6":
version "11.13.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.0.tgz#b0df8d6ef9b5001b2be3a94d909ce3c29a80f9e1"
@ -1026,11 +974,6 @@ array-union@^1.0.1:
dependencies:
array-uniq "^1.0.1"
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
array-uniq@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@ -1211,13 +1154,6 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2"
to-regex "^3.0.1"
braces@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@ -1541,11 +1477,6 @@ color-name@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
colorette@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.1.0.tgz#1f943e5a357fac10b4e0f5aaef3b14cdc1af6ec7"
integrity sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==
colors@^1.1.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e"
@ -1859,13 +1790,6 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
doctrine@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@ -2277,18 +2201,6 @@ fast-diff@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
fast-glob@^3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602"
integrity sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==
dependencies:
"@nodelib/fs.stat" "^2.0.1"
"@nodelib/fs.walk" "^1.2.1"
glob-parent "^5.0.0"
is-glob "^4.0.1"
merge2 "^1.2.3"
micromatch "^4.0.2"
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@ -2297,13 +2209,6 @@ fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
fastq@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2"
integrity sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==
dependencies:
reusify "^1.0.0"
figgy-pudding@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@ -2338,13 +2243,6 @@ fill-range@^4.0.0:
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
find-babel-config@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.1.0.tgz#acc01043a6749fec34429be6b64f542ebb5d6355"
@ -2448,7 +2346,6 @@ flush-write-stream@^1.0.0:
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
fragment-cache@^0.2.1:
version "0.2.1"
@ -2472,15 +2369,6 @@ fs-extra@^5.0.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-minipass@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
@ -2587,13 +2475,6 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
glob-parent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954"
integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==
dependencies:
is-glob "^4.0.1"
glob@^7.0.3, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@ -2656,20 +2537,6 @@ globals@^11.0.1, globals@^11.1.0:
version "11.7.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673"
globby@10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22"
integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==
dependencies:
"@types/glob" "^7.1.1"
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.0.3"
glob "^7.1.3"
ignore "^5.1.1"
merge2 "^1.2.3"
slash "^3.0.0"
globby@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
@ -2700,7 +2567,7 @@ got@^7.1.0:
url-parse-lax "^1.0.0"
url-to-options "^1.0.1"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.2.0:
graceful-fs@^4.1.11, graceful-fs@^4.1.15:
version "4.2.2"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
@ -2849,11 +2716,6 @@ ignore@^3.3.3:
version "3.3.10"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
ignore@^5.1.1:
version "5.1.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
import-local@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
@ -3045,7 +2907,6 @@ is-extendable@^0.1.0, is-extendable@^0.1.1:
is-extendable@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
dependencies:
is-plain-object "^2.0.4"
@ -3082,24 +2943,12 @@ is-glob@^4.0.0:
dependencies:
is-extglob "^2.1.1"
is-glob@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
dependencies:
is-extglob "^2.1.1"
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
dependencies:
kind-of "^3.0.2"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@ -3137,17 +2986,9 @@ is-plain-obj@^1.1.0:
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
dependencies:
isobject "^3.0.1"
is-plain-object@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
dependencies:
isobject "^4.0.0"
is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
@ -3226,12 +3067,6 @@ isobject@^2.0.0:
isobject@^3.0.0, isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isobject@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isurl@^1.0.0-alpha5:
version "1.0.0"
@ -3343,6 +3178,14 @@ kind-of@^6.0.0, kind-of@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
lbry-redux@lbryio/lbry-redux:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/f140db38dd73cead9e87549340fa9434da62ba8a"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
uuid "^3.3.2"
lcid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@ -3621,11 +3464,6 @@ memory-fs@^0.4.0, memory-fs@^0.4.1:
errno "^0.1.3"
readable-stream "^2.0.1"
merge2@^1.2.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@ -3644,14 +3482,6 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
snapdragon "^0.8.1"
to-regex "^3.0.2"
micromatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
dependencies:
braces "^3.0.1"
picomatch "^2.0.5"
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@ -3730,9 +3560,8 @@ mississippi@^3.0.0:
through2 "^2.0.0"
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
version "1.3.1"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
dependencies:
for-in "^1.0.2"
is-extendable "^1.0.1"
@ -4239,11 +4068,6 @@ path-type@^2.0.0:
dependencies:
pify "^2.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pbkdf2@^3.0.3:
version "3.0.16"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
@ -4254,11 +4078,6 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
picomatch@^2.0.5:
version "2.0.7"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6"
integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -4357,6 +4176,10 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
proxy-polyfill@0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/proxy-polyfill/-/proxy-polyfill-0.1.6.tgz#ef41ec6c66f534db15db36c54493a62d184b364e"
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@ -4668,11 +4491,6 @@ ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
reusify@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@2, rimraf@^2.2.8, rimraf@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
@ -4693,13 +4511,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rollup-plugin-alias@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-alias/-/rollup-plugin-alias-2.0.0.tgz#eea77466a8a8a063007c8edb2e9d7a3d06cbb889"
integrity sha512-JVwxV9nwzjc0q7JmrV9jvZlJ8FykNd5r7RmYps8i/7v+vcmmVpeMvXrljhCboYErf4VZ2z9FEj+fP7eJlEGfug==
dependencies:
slash "^3.0.0"
rollup-plugin-babel@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.3.2.tgz#8c0e1bd7aa9826e90769cf76895007098ffd1413"
@ -4707,17 +4518,6 @@ rollup-plugin-babel@^4.3.2:
"@babel/helper-module-imports" "^7.0.0"
rollup-pluginutils "^2.3.0"
rollup-plugin-copy@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.1.0.tgz#435589857f4e346ea63fc137d99dcc1b25f51c3b"
integrity sha512-oVw3ljRV5jv7Yw/6eCEHntVs9Mc+NFglc0iU0J8ei76gldYmtBQ0M/j6WAkZUFVRSrhgfCrEakUllnN87V2f4w==
dependencies:
"@types/fs-extra" "^8.0.0"
colorette "^1.1.0"
fs-extra "^8.1.0"
globby "10.0.1"
is-plain-object "^3.0.0"
rollup-plugin-flow@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/rollup-plugin-flow/-/rollup-plugin-flow-1.1.1.tgz#6ce568f1dd559666b77ab76b4bae251407528db6"
@ -4757,11 +4557,6 @@ run-async@^2.2.0:
dependencies:
is-promise "^2.1.0"
run-parallel@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
@ -4882,11 +4677,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slice-ansi@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
@ -5269,13 +5059,6 @@ to-regex-range@^2.1.0:
is-number "^3.0.0"
repeat-string "^1.6.1"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@ -5450,6 +5233,10 @@ util@^0.11.0:
dependencies:
inherits "2.0.3"
uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
v8-compile-cache@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"