Compare commits

..

No commits in common. "master" and "stats" have entirely different histories.

46 changed files with 14465 additions and 8633 deletions

View file

@ -1,6 +1,4 @@
{ {
"presets": ["@babel/preset-env"], "presets": ["env", "stage-2"],
"plugins": [ "plugins": ["transform-flow-comments"]
"@babel/plugin-transform-flow-strip-types"
]
} }

View file

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

3
.gitignore vendored
View file

@ -1,5 +1,2 @@
/node_modules /node_modules
yarn-error.log yarn-error.log
# Jetbrains
.idea/

View file

@ -1,6 +1,6 @@
The MIT License (MIT) 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 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, "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. 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 cd lbryinc
yarn link sudo npm link
cd /<path>/<to>/<project> (ex: cd ~/lbry-desktop) cd /<path>/<to>/<project>/node_modules
yarn link lbryinc npm link lbryinc
```` ````
### Build ### 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. 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 ## License
[MIT © LBRY](LICENSE) [MIT © LBRY](LICENSE)

1611
dist/bundle.es.js vendored

File diff suppressed because it is too large Load diff

13873
dist/bundle.js vendored

File diff suppressed because it is too large Load diff

View file

@ -19,30 +19,28 @@
"email": "hello@lbry.io" "email": "hello@lbry.io"
}, },
"main": "dist/bundle.js", "main": "dist/bundle.js",
"module": "dist/bundle.es.js",
"scripts": { "scripts": {
"build": "rollup --config && webpack", "build": "webpack",
"dev": "rollup --config --watch", "dev": "webpack --watch",
"precommit": "lint-staged", "precommit": "lint-staged",
"preinstall": "yarn cache clean lbry-redux", "preinstall": "yarn cache clean lbry-redux",
"lint": "eslint 'src/**/*.js' --fix", "lint": "eslint 'src/**/*.js' --fix",
"format": "prettier 'src/**/*.{js,json}' --write" "format": "prettier 'src/**/*.{js,json}' --write"
}, },
"dependencies": { "dependencies": {
"bluebird": "^3.5.1",
"reselect": "^3.0.0" "reselect": "^3.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"lbry-redux": "lbryio/lbry-redux" "lbry-redux": "lbryio/lbry-redux"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.4.3", "babel-core": "^6.26.0",
"@babel/plugin-transform-flow-strip-types": "^7.4.0",
"@babel/preset-env": "^7.4.3",
"@babel/preset-stage-2": "^7.0.0",
"babel-eslint": "^8.0.3", "babel-eslint": "^8.0.3",
"babel-loader": "^8.0.5", "babel-loader": "^7.1.4",
"babel-plugin-module-resolver": "^3.0.0", "babel-plugin-module-resolver": "^3.0.0",
"cross-env": "^5.2.0", "babel-preset-env": "^1.6.1",
"babel-preset-stage-2": "^6.18.0",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
@ -50,19 +48,15 @@
"eslint-plugin-flowtype": "^2.40.1", "eslint-plugin-flowtype": "^2.40.1",
"eslint-plugin-import": "^2.10.0", "eslint-plugin-import": "^2.10.0",
"eslint-plugin-prettier": "^2.4.0", "eslint-plugin-prettier": "^2.4.0",
"flow-babel-webpack-plugin": "^1.1.1",
"flow-bin": "^0.69.0", "flow-bin": "^0.69.0",
"flow-typed": "^2.4.0", "flow-typed": "^2.4.0",
"husky": "^0.14.3", "husky": "^0.14.3",
"lbry-redux": "lbryio/lbry-redux",
"lint-staged": "^7.0.4", "lint-staged": "^7.0.4",
"prettier": "^1.4.2", "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", "webpack": "^4.5.0",
"webpack-cli": "^3.3.7" "webpack-cli": "^2.0.14"
}, },
"engines": { "engines": {
"yarn": "^1.3" "yarn": "^1.3"

View file

@ -1,38 +0,0 @@
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'],
};
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.es.js',
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'],
};

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 // Claims
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'; export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED';
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'; export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED';
@ -56,42 +61,11 @@ export const FETCH_BLACK_LISTED_CONTENT_COMPLETED = 'FETCH_BLACK_LISTED_CONTENT_
export const FETCH_BLACK_LISTED_CONTENT_FAILED = 'FETCH_BLACK_LISTED_CONTENT_FAILED'; export const FETCH_BLACK_LISTED_CONTENT_FAILED = 'FETCH_BLACK_LISTED_CONTENT_FAILED';
export const BLACK_LISTED_CONTENT_SUBSCRIBE = 'BLACK_LISTED_CONTENT_SUBSCRIBE'; export const BLACK_LISTED_CONTENT_SUBSCRIBE = 'BLACK_LISTED_CONTENT_SUBSCRIBE';
// Filtered list
export const FETCH_FILTERED_CONTENT_STARTED = 'FETCH_FILTERED_CONTENT_STARTED';
export const FETCH_FILTERED_CONTENT_COMPLETED = 'FETCH_FILTERED_CONTENT_COMPLETED';
export const FETCH_FILTERED_CONTENT_FAILED = 'FETCH_FILTERED_CONTENT_FAILED';
export const FILTERED_CONTENT_SUBSCRIBE = 'FILTERED_CONTENT_SUBSCRIBE';
// Cost Info // Cost Info
export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED'; export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED';
export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED'; 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_STARTED = 'FETCH_VIEW_COUNT_STARTED';
export const FETCH_VIEW_COUNT_FAILED = 'FETCH_VIEW_COUNT_FAILED'; export const FETCH_VIEW_COUNT_FAILED = 'FETCH_VIEW_COUNT_FAILED';
export const FETCH_VIEW_COUNT_COMPLETED = 'FETCH_VIEW_COUNT_COMPLETED'; 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';
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,158 @@
import * as LBRYINC_ACTIONS from 'constants/action_types'; 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'; import Lbryio from 'lbryio';
import rewards from 'rewards';
export { Lbryio }; import subscriptionsReducer from 'redux/reducers/subscriptions';
// constants // constants
export { LBRYINC_ACTIONS, YOUTUBE_STATUSES, ERRORS }; export { LBRYINC_ACTIONS };
// utils // Lbryio and rewards
export { doTransifexUpload } from 'util/transifex-upload'; export { Lbryio, rewards };
// actions // actions
export { doGenerateAuthToken } from 'redux/actions/auth'; 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 { doFetchCostInfoForUri } from 'redux/actions/cost_info';
export { doBlackListedOutpointsSubscribe } from 'redux/actions/blacklist'; export { doBlackListedOutpointsSubscribe } from 'redux/actions/blacklist';
export { doFilteredOutpointsSubscribe } from 'redux/actions/filtered';
export { doFetchFeaturedUris, doFetchTrendingUris } from 'redux/actions/homepage'; 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 // reducers
export { authReducer } from 'redux/reducers/auth'; 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 { costInfoReducer } from 'redux/reducers/cost_info';
export { blacklistReducer } from 'redux/reducers/blacklist'; export { blacklistReducer } from 'redux/reducers/blacklist';
export { filteredReducer } from 'redux/reducers/filtered';
export { homepageReducer } from 'redux/reducers/homepage'; export { homepageReducer } from 'redux/reducers/homepage';
export { statsReducer } from 'redux/reducers/stats'; export { statsReducer } from 'redux/reducers/stats';
export { syncReducer } from 'redux/reducers/sync';
export { webReducer } from 'redux/reducers/web';
// selectors // selectors
export { selectAuthToken, selectIsAuthenticating } from 'redux/selectors/auth'; export { selectAuthToken, selectIsAuthenticating } from 'redux/selectors/auth';
export {
makeSelectClaimRewardError,
makeSelectIsRewardClaimPending,
makeSelectRewardAmountByType,
makeSelectRewardByType,
selectUnclaimedRewardsByType,
selectClaimedRewardsById,
selectClaimedRewards,
selectClaimedRewardsByTransactionId,
selectUnclaimedRewards,
selectFetchingRewards,
selectUnclaimedRewardValue,
selectClaimsPendingByType,
selectIsClaimRewardPending,
selectClaimErrorsByType,
selectClaimRewardError,
selectRewardByType,
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 { export {
makeSelectFetchingCostInfoForUri, makeSelectFetchingCostInfoForUri,
makeSelectCostInfoForUri, makeSelectCostInfoForUri,
selectAllCostInfoByUri, selectAllCostInfoByUri,
selectCostForCurrentPageUri,
selectFetchingCostInfo, selectFetchingCostInfo,
} from 'redux/selectors/cost_info'; } from 'redux/selectors/cost_info';
export { export { selectBlackListedOutpoints } from 'redux/selectors/blacklist';
selectBlackListedOutpoints,
selectBlacklistedOutpointMap,
} from 'redux/selectors/blacklist';
export { selectFilteredOutpoints, selectFilteredOutpointMap } from 'redux/selectors/filtered';
export { export {
selectFeaturedUris, selectFeaturedUris,
selectFetchingFeaturedUris, selectFetchingFeaturedUris,
selectTrendingUris, selectTrendingUris,
selectFetchingTrendingUris, selectFetchingTrendingUris,
} from 'redux/selectors/homepage'; } from 'redux/selectors/homepage';
export { export { makeSelectViewCountForUri } from 'redux/selectors/stats';
selectViewCount,
makeSelectViewCountForUri,
makeSelectSubCountForUri,
} 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

@ -7,11 +7,10 @@ const Lbryio = {
authenticationPromise: null, authenticationPromise: null,
exchangePromise: null, exchangePromise: null,
exchangeLastFetched: null, exchangeLastFetched: null,
CONNECTION_STRING: 'https://api.lbry.com/', CONNECTION_STRING: 'https://api.lbry.io/',
}; };
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; 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 // We can't use env's because they aren't passed into node_modules
Lbryio.setLocalApi = endpoint => { Lbryio.setLocalApi = endpoint => {
@ -31,22 +30,16 @@ Lbryio.call = (resource, action, params = {}, method = 'get') => {
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response.json(); return response.json();
} }
return response.json().then(json => {
if (response.status === 500) { let error;
return Promise.reject(INTERNAL_APIS_DOWN); if (json.error) {
} error = new Error(json.error);
} else {
if (response) error = new Error('Unknown API error signature');
return response.json().then(json => { }
let error; error.response = response; // This is primarily a hack used in actions/user.js
if (json.error) { return Promise.reject(error);
error = new Error(json.error); });
} else {
error = new Error('Unknown API error signature');
}
error.response = response; // This is primarily a hack used in actions/user.js
return Promise.reject(error);
});
} }
function makeRequest(url, options) { function makeRequest(url, options) {
@ -55,13 +48,6 @@ Lbryio.call = (resource, action, params = {}, method = 'get') => {
return Lbryio.getAuthToken().then(token => { return Lbryio.getAuthToken().then(token => {
const fullParams = { auth_token: token, ...params }; 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); const qs = querystring.stringify(fullParams);
let url = `${Lbryio.CONNECTION_STRING}${resource}/${action}?${qs}`; let url = `${Lbryio.CONNECTION_STRING}${resource}/${action}?${qs}`;
@ -94,7 +80,7 @@ Lbryio.getAuthToken = () =>
Lbryio.overrides.getAuthToken().then(token => { Lbryio.overrides.getAuthToken().then(token => {
resolve(token); resolve(token);
}); });
} else if (typeof window !== 'undefined') { } else {
const { store } = window; const { store } = window;
if (store) { if (store) {
const state = store.getState(); const state = store.getState();
@ -103,27 +89,23 @@ Lbryio.getAuthToken = () =>
resolve(token); resolve(token);
} }
resolve(null);
} else {
resolve(null); resolve(null);
} }
}); });
Lbryio.getCurrentUser = () => Lbryio.call('user', 'me'); Lbryio.getCurrentUser = () => Lbryio.call('user', 'me');
Lbryio.authenticate = (domain, language) => { Lbryio.authenticate = () => {
if (!Lbryio.enabled) { if (!Lbryio.enabled) {
const params = {
id: 1,
primary_email: 'disabled@lbry.io',
has_verified_email: true,
is_identity_verified: true,
is_reward_approved: false,
language: language || 'en',
};
return new Promise(resolve => { return new Promise(resolve => {
resolve(params); resolve({
id: 1,
language: 'en',
primary_email: 'disabled@lbry.io',
has_verified_email: true,
is_identity_verified: true,
is_reward_approved: false,
});
}); });
} }
@ -138,65 +120,55 @@ Lbryio.authenticate = (domain, language) => {
// check that token works // check that token works
return Lbryio.getCurrentUser() return Lbryio.getCurrentUser()
.then(user => user) .then(user => user)
.catch(error => { .catch(() => false);
if (error === INTERNAL_APIS_DOWN) {
throw new Error('Internal APIS down');
}
return false;
});
}) })
.then(user => { .then(user => {
if (user) { if (user) {
return user; return user;
} }
return Lbry.status() return Lbry.status().then(status => {
.then( if (Lbryio.overrides.setAuthToken) {
status => return Lbryio.overrides.setAuthToken(status);
new Promise((res, rej) => { }
const appId =
domain && domain !== 'lbry.tv'
? (domain.replace(/[.]/gi, '') + status.installation_id).slice(0, 66)
: status.installation_id;
Lbryio.call(
'user',
'new',
{
auth_token: '',
language: language || 'en',
app_id: appId,
},
'post'
)
.then(response => {
if (!response.auth_token) {
throw new Error('auth_token was not set in the response');
}
const { store } = window; // simply call the logic to create a new user, and obtain the auth token
if (Lbryio.overrides.setAuthToken) { return new Promise((res, rej) => {
Lbryio.overrides.setAuthToken(response.auth_token); Lbryio.call(
} 'user',
'new',
{
auth_token: '',
language: 'en',
app_id: status.installation_id,
},
'post'
)
.then(response => {
if (!response.auth_token) {
throw new Error('auth_token was not set in the response');
}
if (store) { const { store } = window;
store.dispatch({ if (store) {
type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS, store.dispatch({
data: { authToken: response.auth_token }, type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS,
}); data: { authToken: response.auth_token },
} });
Lbryio.authToken = response.auth_token; }
return res(response);
}) Lbryio.authToken = response.auth_token;
.catch(error => rej(error)); res(response);
}) })
) .catch(() => rej());
.then(newUser => {
if (!newUser) {
return Lbryio.getCurrentUser();
}
return newUser;
}); });
});
})
.then(user => {
if (!user) {
return Lbryio.getCurrentUser();
}
return user;
}) })
.then(resolve, reject); .then(resolve, reject);
}); });

View file

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

View file

@ -20,7 +20,10 @@ export function doFetchCostInfoForUri(uri) {
}); });
} }
const fee = claim.value ? claim.value.fee : undefined; const fee =
claim.value && claim.value.stream && claim.value.stream.metadata
? claim.value.stream.metadata.fee
: undefined;
if (fee === undefined) { if (fee === undefined) {
resolve({ cost: 0, includesData: true }); resolve({ cost: 0, includesData: true });

View file

@ -1,47 +0,0 @@
import Lbryio from 'lbryio';
import * as ACTIONS from 'constants/action_types';
const CHECK_FILTERED_CONTENT_INTERVAL = 60 * 60 * 1000;
export function doFetchFilteredOutpoints() {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_FILTERED_CONTENT_STARTED,
});
const success = ({ outpoints }) => {
let formattedOutpoints = [];
if (outpoints) {
formattedOutpoints = outpoints.map(outpoint => {
const [txid, nout] = outpoint.split(':');
return { txid, nout: Number.parseInt(nout, 10) };
});
}
dispatch({
type: ACTIONS.FETCH_FILTERED_CONTENT_COMPLETED,
data: {
outpoints: formattedOutpoints,
},
});
};
const failure = ({ error }) => {
dispatch({
type: ACTIONS.FETCH_FILTERED_CONTENT_FAILED,
data: {
error,
},
});
};
Lbryio.call('file', 'list_filtered', { auth_token: '' }).then(success, failure);
};
}
export function doFilteredOutpointsSubscribe() {
return dispatch => {
dispatch(doFetchFilteredOutpoints());
setInterval(() => dispatch(doFetchFilteredOutpoints()), CHECK_FILTERED_CONTENT_INTERVAL);
};
}

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 Lbryio from 'lbryio';
import * as ACTIONS from 'constants/action_types'; 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 }); 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>) => { .then((result: Array<number>) => {
const viewCounts = result; const viewCount = result[0];
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimIdCsv, viewCounts } }); dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimId, viewCount } });
}) })
.catch(error => { .catch(error => {
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_FAILED, data: 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,466 @@
// @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, buildURI, 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';
import Promise from 'bluebird';
// import * as SETTINGS from 'constants/settings';
// import { makeSelectClientSetting } from 'redux/selectors/settings';
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_list_by_channel({ uri: subscriptionUri, page: 1, page_size: PAGE_SIZE }).then(
claimListByChannel => {
const claimResult = claimListByChannel[subscriptionUri] || {};
const { claims_in_channel: claimsInChannel } = claimResult;
// 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.name}#${claim.claim_id}` === 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 = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
const shouldDownload =
shouldAutoDownload &&
Boolean(
downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.stream.metadata.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
dispatch(
setSubscriptionLatest(
{
channelName: claimsInChannel[0].channel_name,
uri: buildURI(
{
channelName: claimsInChannel[0].channel_name,
claimId: claimsInChannel[0].claim_id,
},
false
),
},
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
)
)
);
// 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 { claimId } = 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: claimId,
});
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 { claimId } = parseURI(subscription.uri);
Lbryio.call('subscription', 'delete', {
claim_id: claimId,
});
}
};
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 { claimId } = parseURI(sub.uri);
reduxSubMap[claimId] = 1;
if (!dbSubMap[claimId]) {
subsNotInDB.push({
claim_id: claimId,
channel_name: sub.channelName,
});
}
});
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,288 +0,0 @@
import * as ACTIONS from 'constants/action_types';
import Lbryio from 'lbryio';
import { Lbry, doWalletEncrypt, doWalletDecrypt } from 'lbry-redux';
const NO_WALLET_ERROR = 'no wallet found for this user';
export function doSetDefaultAccount(success, failure) {
return dispatch => {
dispatch({
type: ACTIONS.SET_DEFAULT_ACCOUNT,
});
Lbry.account_list()
.then(accountList => {
const { lbc_mainnet: accounts } = accountList;
let defaultId;
for (let i = 0; i < accounts.length; ++i) {
if (accounts[i].satoshis > 0) {
defaultId = accounts[i].id;
break;
}
}
// In a case where there's no balance on either account
// assume the second (which is created after sync) as default
if (!defaultId && accounts.length > 1) {
defaultId = accounts[1].id;
}
// 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);
}
});
};
}
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);
}
}
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'))
.then(response => {
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) {
// different local hash, need to synchronise
dispatch(doSetSync(data.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 {
// user doesn't have a synced wallet
dispatch({
type: ACTIONS.GET_SYNC_COMPLETED,
data: { hasSyncedWallet: false, syncHash: null },
});
// 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);
});
}
}
});
};
}
export function doSyncApply(syncHash, syncData, password) {
return dispatch => {
dispatch({
type: ACTIONS.SYNC_APPLY_STARTED,
});
Lbry.sync_apply({ password, data: syncData })
.then(({ hash: walletHash, data: walletData }) => {
dispatch({
type: ACTIONS.SYNC_APPLY_COMPLETED,
});
if (walletHash !== syncHash) {
// different local hash, need to synchronise
dispatch(doSetSync(syncHash, walletHash, walletData));
}
})
.catch(() => {
dispatch({
type: ACTIONS.SYNC_APPLY_FAILED,
data: {
error:
'Invalid password specified. Please enter the password for your previously synchronised wallet.',
},
});
});
};
}
export function doCheckSync() {
return dispatch => {
dispatch({
type: ACTIONS.GET_SYNC_STARTED,
});
Lbry.sync_hash().then(hash => {
Lbryio.call('sync', 'get', { hash }, 'post')
.then(response => {
const data = {
hasSyncedWallet: true,
syncHash: response.hash,
syncData: response.data,
hashChanged: response.changed,
};
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
})
.catch(() => {
// user doesn't have a synced wallet
dispatch({
type: ACTIONS.GET_SYNC_COMPLETED,
data: { hasSyncedWallet: false, syncHash: null },
});
});
});
};
}
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);
};
}

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

@ -0,0 +1,385 @@
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';
import Promise from 'bluebird';
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

@ -1,34 +0,0 @@
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const defaultState = {
loading: false,
filteredOutpoints: undefined,
};
export const filteredReducer = handleActions(
{
[ACTIONS.FETCH_FILTERED_CONTENT_STARTED]: state => ({
...state,
loading: true,
}),
[ACTIONS.FETCH_FILTERED_CONTENT_COMPLETED]: (state, action) => {
const { outpoints } = action.data;
return {
...state,
loading: false,
filteredOutpoints: outpoints,
};
},
[ACTIONS.FETCH_FILTERED_CONTENT_FAILED]: (state, action) => {
const { error } = action.data;
return {
...state,
loading: false,
fetchingFilteredOutpointsError: error,
};
},
},
defaultState
);

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, fetchingViewCount: false,
viewCountError: undefined, viewCountError: undefined,
viewCountById: {}, viewCountById: {},
fetchingSubCount: false,
subCountError: undefined,
subCountById: {},
}; };
export const statsReducer = handleActions( export const statsReducer = handleActions(
@ -18,38 +15,15 @@ export const statsReducer = handleActions(
viewCountError: action.data, viewCountError: action.data,
}), }),
[ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => { [ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => {
const { claimIdCsv, viewCounts } = action.data; const { claimId, viewCount } = 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 viewCountById = { ...state.viewCountById, [claimId]: viewCount };
return { return {
...state, ...state,
fetchingViewCount: false, fetchingViewCount: false,
viewCountById, 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 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

@ -1,89 +0,0 @@
import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
hasSyncedWallet: false,
syncHash: null,
syncData: null,
setSyncErrorMessage: null,
getSyncErrorMessage: null,
syncApplyErrorMessage: '',
syncApplyIsPending: false,
syncApplyPasswordError: false,
getSyncIsPending: false,
setSyncIsPending: false,
hashChanged: false,
};
reducers[ACTIONS.GET_SYNC_STARTED] = state =>
Object.assign({}, state, {
getSyncIsPending: true,
getSyncErrorMessage: null,
});
reducers[ACTIONS.GET_SYNC_COMPLETED] = (state, action) =>
Object.assign({}, state, {
syncHash: action.data.syncHash,
syncData: action.data.syncData,
hasSyncedWallet: action.data.hasSyncedWallet,
getSyncIsPending: false,
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,
setSyncErrorMessage: null,
});
reducers[ACTIONS.SET_SYNC_FAILED] = (state, action) =>
Object.assign({}, state, {
setSyncIsPending: false,
setSyncErrorMessage: action.data.error,
});
reducers[ACTIONS.SET_SYNC_COMPLETED] = (state, action) =>
Object.assign({}, state, {
setSyncIsPending: false,
setSyncErrorMessage: null,
hasSyncedWallet: true, // sync was successful, so the user has a synced wallet at this point
syncHash: action.data.syncHash,
});
reducers[ACTIONS.SYNC_APPLY_STARTED] = state =>
Object.assign({}, state, {
syncApplyPasswordError: false,
syncApplyIsPending: true,
syncApplyErrorMessage: '',
});
reducers[ACTIONS.SYNC_APPLY_COMPLETED] = state =>
Object.assign({}, state, {
syncApplyIsPending: false,
syncApplyErrorMessage: '',
});
reducers[ACTIONS.SYNC_APPLY_FAILED] = (state, action) =>
Object.assign({}, state, {
syncApplyIsPending: false,
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);
return state;
}

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

@ -0,0 +1,228 @@
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,
user: defaultState.user,
});
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, selectState,
state => state.blackListedOutpoints 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

@ -1,4 +1,5 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { selectCurrentParams } from 'lbry-redux';
export const selectState = state => state.costInfo || {}; export const selectState = state => state.costInfo || {};
@ -7,6 +8,12 @@ export const selectAllCostInfoByUri = createSelector(selectState, state => state
export const makeSelectCostInfoForUri = uri => export const makeSelectCostInfoForUri = uri =>
createSelector(selectAllCostInfoByUri, costInfos => costInfos && costInfos[uri]); createSelector(selectAllCostInfoByUri, costInfos => costInfos && costInfos[uri]);
export const selectCostForCurrentPageUri = createSelector(
selectAllCostInfoByUri,
selectCurrentParams,
(costInfo, params) => (params.uri && costInfo[params.uri] ? costInfo[params.uri].cost : undefined)
);
export const selectFetchingCostInfo = createSelector(selectState, state => state.fetching || {}); export const selectFetchingCostInfo = createSelector(selectState, state => state.fetching || {});
export const makeSelectFetchingCostInfoForUri = uri => export const makeSelectFetchingCostInfoForUri = uri =>

View file

@ -1,20 +0,0 @@
import { createSelector } from 'reselect';
export const selectState = state => state.filtered || {};
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 || {}; const selectState = state => state.stats || {};
export const selectViewCount = createSelector(selectState, state => state.viewCountById); export const selectViewCount = createSelector(selectState, state => state.viewCountById);
export const selectSubCount = createSelector(selectState, state => state.subCountById);
export const makeSelectViewCountForUri = uri => export const makeSelectViewCountForUri = uri =>
createSelector( createSelector(
makeSelectClaimForUri(uri), makeSelectClaimForUri(uri),
selectViewCount, selectViewCount,
(claim, viewCountById) => (claim ? viewCountById[claim.claim_id] || 0 : 0) (claim, viewCountById) => viewCountById[claim.claim_id] || 0
);
export const makeSelectSubCountForUri = uri =>
createSelector(
makeSelectClaimForUri(uri),
selectSubCount,
(claim, subCountById) => (claim ? subCountById[claim.claim_id] || 0 : 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

@ -1,40 +0,0 @@
import { createSelector } from 'reselect';
const selectState = state => state.sync || {};
export const selectHasSyncedWallet = createSelector(selectState, state => state.hasSyncedWallet);
export const selectSyncHash = createSelector(selectState, state => state.syncHash);
export const selectSyncData = createSelector(selectState, state => state.syncData);
export const selectSetSyncErrorMessage = createSelector(
selectState,
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);
export const selectHashChanged = createSelector(selectState, state => state.hashChanged);
export const selectSyncApplyIsPending = createSelector(
selectState,
state => state.syncApplyIsPending
);
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_mine()
.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_mine()
.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

@ -1,5 +1,6 @@
/* eslint-disable import/no-commonjs */ /* eslint-disable import/no-commonjs */
const path = require('path'); const path = require('path');
const FlowBabelWebpackPlugin = require('flow-babel-webpack-plugin');
module.exports = { module.exports = {
mode: 'none', mode: 'none',
@ -20,9 +21,7 @@ module.exports = {
}, },
resolve: { resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'], modules: [path.resolve(__dirname, 'src'), 'node_modules'],
alias: {
'flow-typed': path.resolve(__dirname, './flow-typed'),
},
}, },
plugins: [new FlowBabelWebpackPlugin()],
externals: 'lbry-redux', externals: 'lbry-redux',
}; };

4036
yarn.lock

File diff suppressed because it is too large Load diff