Compare commits
No commits in common. "master" and "lbryredux" have entirely different histories.
47 changed files with 21475 additions and 8738 deletions
6
.babelrc
6
.babelrc
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env"],
|
"presets": ["env", "stage-2"],
|
||||||
"plugins": [
|
"plugins": ["transform-flow-comments"]
|
||||||
"@babel/plugin-transform-flow-strip-types"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
[ignore]
|
[ignore]
|
||||||
node_modules/
|
|
||||||
|
|
||||||
[include]
|
[include]
|
||||||
|
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,2 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
# Jetbrains
|
|
||||||
.idea/
|
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -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,
|
||||||
|
|
|
@ -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
1611
dist/bundle.es.js
vendored
File diff suppressed because it is too large
Load diff
20939
dist/bundle.js
vendored
20939
dist/bundle.js
vendored
File diff suppressed because it is too large
Load diff
26
package.json
26
package.json
|
@ -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"
|
||||||
|
|
|
@ -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'],
|
|
||||||
};
|
|
|
@ -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,6 @@ 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
|
|
||||||
export const FETCH_VIEW_COUNT_STARTED = 'FETCH_VIEW_COUNT_STARTED';
|
|
||||||
export const FETCH_VIEW_COUNT_FAILED = 'FETCH_VIEW_COUNT_FAILED';
|
|
||||||
export const FETCH_VIEW_COUNT_COMPLETED = 'FETCH_VIEW_COUNT_COMPLETED';
|
|
||||||
export const FETCH_SUB_COUNT_STARTED = 'FETCH_SUB_COUNT_STARTED';
|
|
||||||
export const FETCH_SUB_COUNT_FAILED = 'FETCH_SUB_COUNT_FAILED';
|
|
||||||
export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED';
|
|
||||||
|
|
||||||
// Cross-device Sync
|
|
||||||
export const GET_SYNC_STARTED = 'GET_SYNC_STARTED';
|
|
||||||
export const GET_SYNC_COMPLETED = 'GET_SYNC_COMPLETED';
|
|
||||||
export const GET_SYNC_FAILED = 'GET_SYNC_FAILED';
|
|
||||||
export const SET_SYNC_STARTED = 'SET_SYNC_STARTED';
|
|
||||||
export const SET_SYNC_FAILED = 'SET_SYNC_FAILED';
|
|
||||||
export const SET_SYNC_COMPLETED = 'SET_SYNC_COMPLETED';
|
|
||||||
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';
|
|
||||||
|
|
|
@ -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.';
|
|
12
src/constants/subscriptions.js
Normal file
12
src/constants/subscriptions.js
Normal 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';
|
|
@ -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';
|
|
170
src/index.js
170
src/index.js
|
@ -1,79 +1,155 @@
|
||||||
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 {
|
|
||||||
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 { 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 {
|
|
||||||
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';
|
|
||||||
|
|
148
src/lbryio.js
148
src/lbryio.js
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
|
||||||
}
|
|
154
src/redux/actions/rewards.js
Normal file
154
src/redux/actions/rewards.js
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
// @flow
|
|
||||||
import Lbryio from 'lbryio';
|
|
||||||
import * as ACTIONS from 'constants/action_types';
|
|
||||||
|
|
||||||
export const doFetchViewCount = (claimIdCsv: string) => dispatch => {
|
|
||||||
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_STARTED });
|
|
||||||
|
|
||||||
return Lbryio.call('file', 'view_count', { claim_id: claimIdCsv })
|
|
||||||
.then((result: Array<number>) => {
|
|
||||||
const viewCounts = result;
|
|
||||||
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimIdCsv, viewCounts } });
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_FAILED, data: error });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const doFetchSubCount = (claimId: string) => dispatch => {
|
|
||||||
dispatch({ type: ACTIONS.FETCH_SUB_COUNT_STARTED });
|
|
||||||
|
|
||||||
return Lbryio.call('subscription', 'sub_count', { claim_id: claimId })
|
|
||||||
.then((result: Array<number>) => {
|
|
||||||
const subCount = result[0];
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.FETCH_SUB_COUNT_COMPLETED,
|
|
||||||
data: { claimId, subCount },
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
dispatch({ type: ACTIONS.FETCH_SUB_COUNT_FAILED, data: error });
|
|
||||||
});
|
|
||||||
};
|
|
466
src/redux/actions/subscriptions.js
Normal file
466
src/redux/actions/subscriptions.js
Normal 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,
|
||||||
|
});
|
|
@ -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
385
src/redux/actions/user.js
Normal 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 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -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 },
|
|
||||||
});
|
|
|
@ -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
|
|
||||||
);
|
|
110
src/redux/reducers/rewards.js
Normal file
110
src/redux/reducers/rewards.js
Normal 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;
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
import { handleActions } from 'util/redux-utils';
|
|
||||||
import * as ACTIONS from 'constants/action_types';
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
fetchingViewCount: false,
|
|
||||||
viewCountError: undefined,
|
|
||||||
viewCountById: {},
|
|
||||||
fetchingSubCount: false,
|
|
||||||
subCountError: undefined,
|
|
||||||
subCountById: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const statsReducer = handleActions(
|
|
||||||
{
|
|
||||||
[ACTIONS.FETCH_VIEW_COUNT_STARTED]: state => ({ ...state, fetchingViewCount: true }),
|
|
||||||
[ACTIONS.FETCH_VIEW_COUNT_FAILED]: (state, action) => ({
|
|
||||||
...state,
|
|
||||||
viewCountError: action.data,
|
|
||||||
}),
|
|
||||||
[ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => {
|
|
||||||
const { claimIdCsv, viewCounts } = action.data;
|
|
||||||
|
|
||||||
const viewCountById = Object.assign({}, state.viewCountById);
|
|
||||||
const claimIds = claimIdCsv.split(',');
|
|
||||||
|
|
||||||
if (claimIds.length === viewCounts.length) {
|
|
||||||
claimIds.forEach((claimId, index) => {
|
|
||||||
viewCountById[claimId] = viewCounts[index];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
fetchingViewCount: false,
|
|
||||||
viewCountById,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[ACTIONS.FETCH_SUB_COUNT_STARTED]: state => ({ ...state, fetchingSubCount: true }),
|
|
||||||
[ACTIONS.FETCH_SUB_COUNT_FAILED]: (state, action) => ({
|
|
||||||
...state,
|
|
||||||
subCountError: action.data,
|
|
||||||
}),
|
|
||||||
[ACTIONS.FETCH_SUB_COUNT_COMPLETED]: (state, action) => {
|
|
||||||
const { claimId, subCount } = action.data;
|
|
||||||
|
|
||||||
const subCountById = { ...state.subCountById, [claimId]: subCount };
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
fetchingSubCount: false,
|
|
||||||
subCountById,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultState
|
|
||||||
);
|
|
213
src/redux/reducers/subscriptions.js
Normal file
213
src/redux/reducers/subscriptions.js
Normal 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
|
||||||
|
);
|
|
@ -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
228
src/redux/reducers/user.js
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}, {})
|
|
||||||
: {}
|
|
||||||
);
|
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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;
|
|
||||||
}, {})
|
|
||||||
: {}
|
|
||||||
);
|
|
76
src/redux/selectors/rewards.js
Normal file
76
src/redux/selectors/rewards.js
Normal 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]
|
||||||
|
);
|
|
@ -1,20 +0,0 @@
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
|
||||||
|
|
||||||
const selectState = state => state.stats || {};
|
|
||||||
export const selectViewCount = createSelector(selectState, state => state.viewCountById);
|
|
||||||
export const selectSubCount = createSelector(selectState, state => state.subCountById);
|
|
||||||
|
|
||||||
export const makeSelectViewCountForUri = uri =>
|
|
||||||
createSelector(
|
|
||||||
makeSelectClaimForUri(uri),
|
|
||||||
selectViewCount,
|
|
||||||
(claim, viewCountById) => (claim ? viewCountById[claim.claim_id] || 0 : 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const makeSelectSubCountForUri = uri =>
|
|
||||||
createSelector(
|
|
||||||
makeSelectClaimForUri(uri),
|
|
||||||
selectSubCount,
|
|
||||||
(claim, subCountById) => (claim ? subCountById[claim.claim_id] || 0 : 0)
|
|
||||||
);
|
|
283
src/redux/selectors/subscriptions.js
Normal file
283
src/redux/selectors/subscriptions.js
Normal 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
|
||||||
|
);
|
|
@ -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
133
src/redux/selectors/user.js
Normal 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
|
||||||
|
);
|
|
@ -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
122
src/rewards.js
Normal 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
6
src/types/redux.js
Normal 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
137
src/types/subscription.js
Normal 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>;
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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',
|
||||||
|
@ -7,7 +8,7 @@ module.exports = {
|
||||||
output: {
|
output: {
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
libraryTarget: 'umd',
|
libraryTarget: 'umd'
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -20,9 +21,6 @@ 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'),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
externals: 'lbry-redux',
|
plugins: [new FlowBabelWebpackPlugin()],
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue