Common components refactor (#1)
This commit is contained in:
parent
8f766fa930
commit
6418b5a9f2
39 changed files with 7302 additions and 753 deletions
30
.eslintrc.json
Normal file
30
.eslintrc.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"plugins": ["flowtype"],
|
||||||
|
"extends": [
|
||||||
|
"airbnb",
|
||||||
|
"plugin:import/electron",
|
||||||
|
"plugin:flowtype/recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack": {
|
||||||
|
"config": "webpack.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"__": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"import/no-commonjs": "warn",
|
||||||
|
"import/no-amd": "warn",
|
||||||
|
"import/prefer-default-export": "ignore",
|
||||||
|
"func-names": ["warn", "as-needed"]
|
||||||
|
}
|
||||||
|
}
|
9
.lintstagedrc
Normal file
9
.lintstagedrc
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"linters": {
|
||||||
|
"src/**/*.js": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# lbry-redux
|
||||||
|
lbry-redux is a module which contains common React and redux code shared between lbry-app and lbry-android.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Add `lbry-redux` as a dependency to your `package.json` file.
|
||||||
|
`"lbry-redux": "lbryio/lbry-redux"`
|
||||||
|
|
||||||
|
### Local development
|
||||||
|
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 lbry-redux
|
||||||
|
sudo npm link
|
||||||
|
cd /<path>/<to>/<project>/node_modules
|
||||||
|
npm link lbry-redux
|
||||||
|
````
|
||||||
|
|
||||||
|
### Build
|
||||||
|
Run `$ npm build`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT © LBRY](LICENSE)
|
4910
build/index.js
4910
build/index.js
File diff suppressed because it is too large
Load diff
23
package.json
23
package.json
|
@ -17,6 +17,7 @@
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
|
"precommit": "lint-staged",
|
||||||
"lint": "eslint 'src/**/*.{js,jsx}' --fix",
|
"lint": "eslint 'src/**/*.{js,jsx}' --fix",
|
||||||
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write"
|
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write"
|
||||||
},
|
},
|
||||||
|
@ -28,35 +29,22 @@
|
||||||
"bluebird": "^3.5.1",
|
"bluebird": "^3.5.1",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"electron-dl": "^1.6.0",
|
"electron-dl": "^1.6.0",
|
||||||
"formik": "^0.10.4",
|
|
||||||
"from2": "^2.3.0",
|
"from2": "^2.3.0",
|
||||||
"install": "^0.10.2",
|
"install": "^0.10.2",
|
||||||
"jayson": "^2.0.2",
|
"jayson": "^2.0.2",
|
||||||
"jshashes": "^1.0.7",
|
"jshashes": "^1.0.7",
|
||||||
"keytar": "^4.0.3",
|
"proxy-polyfill": "0.1.6",
|
||||||
"localforage": "^1.5.0",
|
|
||||||
"npm": "^5.5.1",
|
|
||||||
"qrcode.react": "^0.7.2",
|
|
||||||
"rc-progress": "^2.0.6",
|
"rc-progress": "^2.0.6",
|
||||||
"react": "^16.2.0",
|
"react": "^16.2.0",
|
||||||
"react-dom": "^16.2.0",
|
|
||||||
"react-markdown": "^2.5.0",
|
|
||||||
"react-modal": "^3.1.7",
|
|
||||||
"react-paginate": "^5.0.0",
|
|
||||||
"react-redux": "^5.0.3",
|
"react-redux": "^5.0.3",
|
||||||
"react-simplemde-editor": "^3.6.11",
|
|
||||||
"redux": "^3.6.0",
|
"redux": "^3.6.0",
|
||||||
"redux-action-buffer": "^1.1.0",
|
"redux-action-buffer": "^1.1.0",
|
||||||
"redux-logger": "^3.0.1",
|
"redux-logger": "^3.0.1",
|
||||||
"redux-persist": "^4.8.0",
|
"redux-persist": "^4.8.0",
|
||||||
"redux-persist-transform-compress": "^4.2.0",
|
"redux-persist-transform-compress": "^4.2.0",
|
||||||
"redux-persist-transform-filter": "0.0.10",
|
"redux-persist-transform-filter": "0.0.10",
|
||||||
"redux-thunk": "^2.2.0",
|
|
||||||
"render-media": "^2.10.0",
|
|
||||||
"reselect": "^3.0.0",
|
"reselect": "^3.0.0",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"shapeshift.io": "^1.3.1",
|
|
||||||
"source-map-support": "^0.5.0",
|
|
||||||
"tree-kill": "^1.1.0",
|
"tree-kill": "^1.1.0",
|
||||||
"y18n": "^4.0.0"
|
"y18n": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
@ -96,13 +84,6 @@
|
||||||
"webpack": "^3.10.0",
|
"webpack": "^3.10.0",
|
||||||
"webpack-build-notifier": "^0.1.18"
|
"webpack-build-notifier": "^0.1.18"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
|
||||||
"webpack/webpack-sources": "1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6",
|
|
||||||
"yarn": "^1.3"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.18.0",
|
"lbrynetDaemonVersion": "0.18.0",
|
||||||
|
|
|
@ -28,6 +28,8 @@ export const UPDATE_VERSION = 'UPDATE_VERSION';
|
||||||
export const UPDATE_REMOTE_VERSION = 'UPDATE_REMOTE_VERSION';
|
export const UPDATE_REMOTE_VERSION = 'UPDATE_REMOTE_VERSION';
|
||||||
export const SKIP_UPGRADE = 'SKIP_UPGRADE';
|
export const SKIP_UPGRADE = 'SKIP_UPGRADE';
|
||||||
export const START_UPGRADE = 'START_UPGRADE';
|
export const START_UPGRADE = 'START_UPGRADE';
|
||||||
|
export const AUTO_UPDATE_DECLINED = 'AUTO_UPDATE_DECLINED';
|
||||||
|
export const AUTO_UPDATE_DOWNLOADED = 'AUTO_UPDATE_DOWNLOADED';
|
||||||
|
|
||||||
// Wallet
|
// Wallet
|
||||||
export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED';
|
export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED';
|
||||||
|
@ -37,8 +39,6 @@ export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED';
|
||||||
export const UPDATE_BALANCE = 'UPDATE_BALANCE';
|
export const UPDATE_BALANCE = 'UPDATE_BALANCE';
|
||||||
export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED';
|
export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED';
|
||||||
export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED';
|
export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED';
|
||||||
export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT';
|
|
||||||
export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS';
|
|
||||||
export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED';
|
export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED';
|
||||||
export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED';
|
export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED';
|
||||||
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED';
|
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED';
|
||||||
|
@ -60,8 +60,8 @@ export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED';
|
||||||
export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED';
|
export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED';
|
||||||
export const ABANDON_CLAIM_STARTED = 'ABANDON_CLAIM_STARTED';
|
export const ABANDON_CLAIM_STARTED = 'ABANDON_CLAIM_STARTED';
|
||||||
export const ABANDON_CLAIM_SUCCEEDED = 'ABANDON_CLAIM_SUCCEEDED';
|
export const ABANDON_CLAIM_SUCCEEDED = 'ABANDON_CLAIM_SUCCEEDED';
|
||||||
export const FETCH_CHANNEL_LIST_MINE_STARTED = 'FETCH_CHANNEL_LIST_MINE_STARTED';
|
export const FETCH_CHANNEL_LIST_STARTED = 'FETCH_CHANNEL_LIST_STARTED';
|
||||||
export const FETCH_CHANNEL_LIST_MINE_COMPLETED = 'FETCH_CHANNEL_LIST_MINE_COMPLETED';
|
export const FETCH_CHANNEL_LIST_COMPLETED = 'FETCH_CHANNEL_LIST_COMPLETED';
|
||||||
export const CREATE_CHANNEL_STARTED = 'CREATE_CHANNEL_STARTED';
|
export const CREATE_CHANNEL_STARTED = 'CREATE_CHANNEL_STARTED';
|
||||||
export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED';
|
export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED';
|
||||||
export const PUBLISH_STARTED = 'PUBLISH_STARTED';
|
export const PUBLISH_STARTED = 'PUBLISH_STARTED';
|
||||||
|
@ -82,19 +82,23 @@ export const LOADING_VIDEO_FAILED = 'LOADING_VIDEO_FAILED';
|
||||||
export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED';
|
export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED';
|
||||||
export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED';
|
export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED';
|
||||||
export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED';
|
export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED';
|
||||||
|
export const DOWNLOADING_CANCELED = 'DOWNLOADING_CANCELED';
|
||||||
export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED';
|
export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED';
|
||||||
export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED';
|
export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED';
|
||||||
export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED';
|
export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED';
|
||||||
export const FILE_DELETE = 'FILE_DELETE';
|
export const FILE_DELETE = 'FILE_DELETE';
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
export const SEARCH_STARTED = 'SEARCH_STARTED';
|
export const SEARCH_START = 'SEARCH_START';
|
||||||
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED';
|
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
|
||||||
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED';
|
export const SEARCH_FAIL = 'SEARCH_FAIL';
|
||||||
|
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY';
|
||||||
|
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';
|
||||||
export const CLIENT_SETTING_CHANGED = 'CLIENT_SETTING_CHANGED';
|
export const CLIENT_SETTING_CHANGED = 'CLIENT_SETTING_CHANGED';
|
||||||
|
export const UPDATE_IS_NIGHT = 'UPDATE_IS_NIGHT';
|
||||||
|
|
||||||
// User
|
// User
|
||||||
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
|
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
|
||||||
|
@ -108,6 +112,13 @@ export const USER_EMAIL_NEW_FAILURE = 'USER_EMAIL_NEW_FAILURE';
|
||||||
export const USER_EMAIL_VERIFY_STARTED = 'USER_EMAIL_VERIFY_STARTED';
|
export const USER_EMAIL_VERIFY_STARTED = 'USER_EMAIL_VERIFY_STARTED';
|
||||||
export const USER_EMAIL_VERIFY_SUCCESS = 'USER_EMAIL_VERIFY_SUCCESS';
|
export const USER_EMAIL_VERIFY_SUCCESS = 'USER_EMAIL_VERIFY_SUCCESS';
|
||||||
export const USER_EMAIL_VERIFY_FAILURE = 'USER_EMAIL_VERIFY_FAILURE';
|
export const USER_EMAIL_VERIFY_FAILURE = 'USER_EMAIL_VERIFY_FAILURE';
|
||||||
|
export const USER_PHONE_RESET = 'USER_PHONE_RESET';
|
||||||
|
export const USER_PHONE_NEW_STARTED = 'USER_PHONE_NEW_STARTED';
|
||||||
|
export const USER_PHONE_NEW_SUCCESS = 'USER_PHONE_NEW_SUCCESS';
|
||||||
|
export const USER_PHONE_NEW_FAILURE = 'USER_PHONE_NEW_FAILURE';
|
||||||
|
export const USER_PHONE_VERIFY_STARTED = 'USER_PHONE_VERIFY_STARTED';
|
||||||
|
export const USER_PHONE_VERIFY_SUCCESS = 'USER_PHONE_VERIFY_SUCCESS';
|
||||||
|
export const USER_PHONE_VERIFY_FAILURE = 'USER_PHONE_VERIFY_FAILURE';
|
||||||
export const USER_IDENTITY_VERIFY_STARTED = 'USER_IDENTITY_VERIFY_STARTED';
|
export const USER_IDENTITY_VERIFY_STARTED = 'USER_IDENTITY_VERIFY_STARTED';
|
||||||
export const USER_IDENTITY_VERIFY_SUCCESS = 'USER_IDENTITY_VERIFY_SUCCESS';
|
export const USER_IDENTITY_VERIFY_SUCCESS = 'USER_IDENTITY_VERIFY_SUCCESS';
|
||||||
export const USER_IDENTITY_VERIFY_FAILURE = 'USER_IDENTITY_VERIFY_FAILURE';
|
export const USER_IDENTITY_VERIFY_FAILURE = 'USER_IDENTITY_VERIFY_FAILURE';
|
||||||
|
@ -154,6 +165,12 @@ export const CLEAR_SHAPE_SHIFT = 'CLEAR_SHAPE_SHIFT';
|
||||||
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
|
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
|
||||||
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
|
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
|
||||||
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
|
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
|
||||||
|
export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST';
|
||||||
|
export const SET_SUBSCRIPTION_NOTIFICATION = 'SET_SUBSCRIPTION_NOTIFICATION';
|
||||||
|
export const SET_SUBSCRIPTION_NOTIFICATIONS = 'SET_SUBSCRIPTION_NOTIFICATIONS';
|
||||||
|
export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
|
||||||
|
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
|
||||||
|
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
|
||||||
|
|
||||||
// Video controls
|
// Video controls
|
||||||
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';
|
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';
|
||||||
|
@ -162,3 +179,13 @@ export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';
|
||||||
export const MEDIA_PLAY = 'MEDIA_PLAY';
|
export const MEDIA_PLAY = 'MEDIA_PLAY';
|
||||||
export const MEDIA_PAUSE = 'MEDIA_PAUSE';
|
export const MEDIA_PAUSE = 'MEDIA_PAUSE';
|
||||||
export const MEDIA_POSITION = 'MEDIA_POSITION';
|
export const MEDIA_POSITION = 'MEDIA_POSITION';
|
||||||
|
|
||||||
|
// Publishing
|
||||||
|
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
||||||
|
export const UPDATE_PUBLISH_FORM = 'UPDATE_PUBLISH_FORM';
|
||||||
|
export const PUBLISH_START = 'PUBLISH_START';
|
||||||
|
export const PUBLISH_SUCCESS = 'PUBLISH_SUCCESS';
|
||||||
|
export const PUBLISH_FAIL = 'PUBLISH_FAIL';
|
||||||
|
export const CLEAR_PUBLISH_ERROR = 'CLEAR_PUBLISH_ERROR';
|
||||||
|
export const REMOVE_PENDING_PUBLISH = 'REMOVE_PENDING_PUBLISH';
|
||||||
|
export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT';
|
||||||
|
|
3
src/constants/search.js
Normal file
3
src/constants/search.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const FILE = 'file';
|
||||||
|
export const CHANNEL = 'channel';
|
||||||
|
export const SEARCH = 'search';
|
|
@ -11,3 +11,7 @@ export const INSTANT_PURCHASE_ENABLED = 'instantPurchaseEnabled';
|
||||||
export const INSTANT_PURCHASE_MAX = 'instantPurchaseMax';
|
export const INSTANT_PURCHASE_MAX = 'instantPurchaseMax';
|
||||||
export const THEME = 'theme';
|
export const THEME = 'theme';
|
||||||
export const THEMES = 'themes';
|
export const THEMES = 'themes';
|
||||||
|
export const AUTOMATIC_DARK_MODE_ENABLED = 'automaticDarkModeEnabled';
|
||||||
|
|
||||||
|
// mobile settings
|
||||||
|
export const KEEP_DAEMON_RUNNING = 'keepDaemonRunning';
|
||||||
|
|
176
src/index.js
176
src/index.js
|
@ -1,4 +1,176 @@
|
||||||
export { Lbry } from 'lbry';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
export { LbryApi } from 'lbryapi';
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import Lbry from 'lbry';
|
||||||
|
import Lbryapi from 'lbryapi';
|
||||||
|
import { selectState as selectSearchState } from 'redux/selectors/search';
|
||||||
|
|
||||||
|
// types
|
||||||
|
export { Notification } from 'types/Notification';
|
||||||
|
|
||||||
|
// constants
|
||||||
|
export { ACTIONS, SETTINGS };
|
||||||
|
|
||||||
|
// common
|
||||||
|
export { Lbry, Lbryapi };
|
||||||
|
export {
|
||||||
|
regexInvalidURI,
|
||||||
|
regexAddress,
|
||||||
|
parseURI,
|
||||||
|
buildURI,
|
||||||
|
normalizeURI,
|
||||||
|
isURIValid,
|
||||||
|
isURIClaimable,
|
||||||
|
} from 'lbryURI';
|
||||||
|
|
||||||
|
// actions
|
||||||
|
export { doNotify } from 'redux/actions/notifications';
|
||||||
|
|
||||||
|
export {
|
||||||
|
doFetchClaimListMine,
|
||||||
|
doAbandonClaim,
|
||||||
|
doResolveUris,
|
||||||
|
doResolveUri,
|
||||||
|
doFetchFeaturedUris,
|
||||||
|
doFetchRewardedContent,
|
||||||
|
} from 'redux/actions/claims';
|
||||||
|
|
||||||
|
export { doFetchCostInfoForUri } from 'redux/actions/cost_info';
|
||||||
|
|
||||||
|
export {
|
||||||
|
doFetchFileInfo,
|
||||||
|
doFileList,
|
||||||
|
doFetchFileInfosAndPublishedClaims,
|
||||||
|
} from 'redux/actions/file_info';
|
||||||
|
|
||||||
|
export { doSearch, doUpdateSearchQuery } from 'redux/actions/search';
|
||||||
|
|
||||||
|
export {
|
||||||
|
doUpdateBalance,
|
||||||
|
doBalanceSubscribe,
|
||||||
|
doFetchTransactions,
|
||||||
|
doFetchBlock,
|
||||||
|
doGetNewAddress,
|
||||||
|
doCheckAddressIsMine,
|
||||||
|
doSendDraftTransaction,
|
||||||
|
doSetDraftTransactionAmount,
|
||||||
|
doSetDraftTransactionAddress,
|
||||||
|
doSendSupport,
|
||||||
|
} from 'redux/actions/wallet';
|
||||||
|
|
||||||
|
// utils
|
||||||
|
export { batchActions } from 'util/batchActions';
|
||||||
|
export { parseQueryParams, toQueryString } from 'util/query_params';
|
||||||
|
export { formatCredits, formatFullPrice } from 'util/formatCredits';
|
||||||
|
|
||||||
|
// reducers
|
||||||
|
export { claimsReducer } from 'redux/reducers/claims';
|
||||||
export { costInfoReducer } from 'redux/reducers/cost_info';
|
export { costInfoReducer } from 'redux/reducers/cost_info';
|
||||||
|
export { fileInfoReducer } from 'redux/reducers/file_info';
|
||||||
|
export { notificationsReducer } from 'redux/reducers/notifications';
|
||||||
|
export { searchReducer } from 'redux/reducers/search';
|
||||||
|
export { walletReducer } from 'redux/reducers/wallet';
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
export { selectNotification } from 'redux/selectors/notifications';
|
||||||
|
|
||||||
|
export {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectClaimIsMine,
|
||||||
|
makeSelectFetchingChannelClaims,
|
||||||
|
makeSelectClaimsInChannelForCurrentPage,
|
||||||
|
makeSelectMetadataForUri,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
makeSelectContentTypeForUri,
|
||||||
|
makeSelectIsUriResolving,
|
||||||
|
makeSelectTotalItemsForChannel,
|
||||||
|
makeSelectTotalPagesForChannel,
|
||||||
|
selectClaimsById,
|
||||||
|
selectClaimsByUri,
|
||||||
|
selectAllClaimsByChannel,
|
||||||
|
selectMyClaimsRaw,
|
||||||
|
selectAbandoningIds,
|
||||||
|
selectMyActiveClaims,
|
||||||
|
selectAllFetchingChannelClaims,
|
||||||
|
selectIsFetchingClaimListMine,
|
||||||
|
selectPendingClaims,
|
||||||
|
selectMyClaims,
|
||||||
|
selectMyClaimsWithoutChannels,
|
||||||
|
selectAllMyClaimsByOutpoint,
|
||||||
|
selectMyClaimsOutpoints,
|
||||||
|
selectFetchingMyChannels,
|
||||||
|
selectMyChannelClaims,
|
||||||
|
selectResolvingUris,
|
||||||
|
selectFeaturedUris,
|
||||||
|
selectFetchingFeaturedUris,
|
||||||
|
selectPlayingUri,
|
||||||
|
selectChannelClaimCounts,
|
||||||
|
selectRewardContentClaimIds,
|
||||||
|
} from 'redux/selectors/claims';
|
||||||
|
|
||||||
|
export {
|
||||||
|
makeSelectFetchingCostInfoForUri,
|
||||||
|
makeSelectCostInfoForUri,
|
||||||
|
selectAllCostInfoByUri,
|
||||||
|
selectCostForCurrentPageUri,
|
||||||
|
selectFetchingCostInfo,
|
||||||
|
} from 'redux/selectors/cost_info';
|
||||||
|
|
||||||
|
export {
|
||||||
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectDownloadingForUri,
|
||||||
|
makeSelectLoadingForUri,
|
||||||
|
selectFileInfosByOutpoint,
|
||||||
|
selectIsFetchingFileList,
|
||||||
|
selectIsFetchingFileListDownloadedOrPublished,
|
||||||
|
selectDownloadingByOutpoint,
|
||||||
|
selectUrisLoading,
|
||||||
|
selectFileInfosDownloaded,
|
||||||
|
selectDownloadingFileInfos,
|
||||||
|
selectTotalDownloadProgress,
|
||||||
|
selectSearchDownloadUris,
|
||||||
|
} from 'redux/selectors/file_info';
|
||||||
|
|
||||||
|
export {
|
||||||
|
computePageFromPath,
|
||||||
|
makeSelectCurrentParam,
|
||||||
|
selectCurrentPath,
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentParams,
|
||||||
|
selectHeaderLinks,
|
||||||
|
selectPageTitle,
|
||||||
|
selectPathAfterAuth,
|
||||||
|
selectIsBackDisabled,
|
||||||
|
selectIsForwardDisabled,
|
||||||
|
selectHistoryIndex,
|
||||||
|
selectHistoryStack,
|
||||||
|
selectActiveHistoryEntry,
|
||||||
|
selectNavLinks,
|
||||||
|
} from 'redux/selectors/navigation';
|
||||||
|
|
||||||
|
export { selectSearchState };
|
||||||
|
export {
|
||||||
|
makeSelectSearchUris,
|
||||||
|
selectSearchQuery,
|
||||||
|
selectSearchValue,
|
||||||
|
selectIsSearching,
|
||||||
|
selectSearchUrisByQuery,
|
||||||
|
selectWunderBarAddress,
|
||||||
|
} from 'redux/selectors/search';
|
||||||
|
|
||||||
|
export {
|
||||||
|
makeSelectBlockDate,
|
||||||
|
selectBalance,
|
||||||
|
selectTransactionsById,
|
||||||
|
selectTransactionItems,
|
||||||
|
selectRecentTransactions,
|
||||||
|
selectHasTransactions,
|
||||||
|
selectIsFetchingTransactions,
|
||||||
|
selectIsSendingSupport,
|
||||||
|
selectReceiveAddress,
|
||||||
|
selectGettingNewAddress,
|
||||||
|
selectDraftTransaction,
|
||||||
|
selectDraftTransactionAmount,
|
||||||
|
selectDraftTransactionAddress,
|
||||||
|
selectDraftTransactionError,
|
||||||
|
selectBlocks,
|
||||||
|
} from 'redux/selectors/wallet';
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
const jsonrpc = {};
|
|
||||||
|
|
||||||
jsonrpc.call = (
|
|
||||||
connectionString,
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
callback,
|
|
||||||
errorCallback,
|
|
||||||
connectFailedCallback
|
|
||||||
) => {
|
|
||||||
function checkAndParse(response) {
|
|
||||||
if (response.status >= 200 && response.status < 300) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return response.json().then(json => {
|
|
||||||
let error;
|
|
||||||
if (json.error) {
|
|
||||||
error = new Error(json.error);
|
|
||||||
} else {
|
|
||||||
error = new Error('Protocol error with unknown response signature');
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const counter = parseInt(sessionStorage.getItem('JSONRPCCounter') || 0, 10);
|
|
||||||
const url = connectionString;
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
id: counter,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
sessionStorage.setItem('JSONRPCCounter', counter + 1);
|
|
||||||
|
|
||||||
return fetch(url, options)
|
|
||||||
.then(checkAndParse)
|
|
||||||
.then(response => {
|
|
||||||
const error = response.error || (response.result && response.result.error);
|
|
||||||
|
|
||||||
if (!error && typeof callback === 'function') {
|
|
||||||
return callback(response.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error && typeof errorCallback === 'function') {
|
|
||||||
return errorCallback(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorEvent = new CustomEvent('unhandledError', {
|
|
||||||
detail: {
|
|
||||||
connectionString,
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
code: error.code,
|
|
||||||
message: error.message || error,
|
|
||||||
data: error.data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
document.dispatchEvent(errorEvent);
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (connectFailedCallback) {
|
|
||||||
return connectFailedCallback(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorEvent = new CustomEvent('unhandledError', {
|
|
||||||
detail: {
|
|
||||||
connectionString,
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
code: error.response && error.response.status,
|
|
||||||
message: __('Connection to API server failed'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
document.dispatchEvent(errorEvent);
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default jsonrpc;
|
|
247
src/lbry.js
247
src/lbry.js
|
@ -1,4 +1,5 @@
|
||||||
import jsonrpc from 'jsonrpc';
|
// @flow
|
||||||
|
import 'proxy-polyfill';
|
||||||
|
|
||||||
const CHECK_DAEMON_STARTED_TRY_NUMBER = 200;
|
const CHECK_DAEMON_STARTED_TRY_NUMBER = 200;
|
||||||
|
|
||||||
|
@ -8,116 +9,88 @@ const Lbry = {
|
||||||
pendingPublishTimeout: 20 * 60 * 1000,
|
pendingPublishTimeout: 20 * 60 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
function apiCall(method, params, resolve, reject) {
|
function checkAndParse(response) {
|
||||||
return jsonrpc.call(Lbry.daemonConnectionString, method, params, resolve, reject, reject);
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return response.json().then(json => {
|
||||||
|
let error;
|
||||||
|
if (json.error) {
|
||||||
|
error = new Error(json.error);
|
||||||
|
} else {
|
||||||
|
error = new Error('Protocol error with unknown response signature');
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const lbryProxy = new Proxy(Lbry, {
|
function apiCall(method: string, params: ?{}, resolve: Function, reject: Function) {
|
||||||
get(target, name) {
|
const counter = new Date().getTime();
|
||||||
if (name in target) {
|
const options = {
|
||||||
return target[name];
|
method: 'POST',
|
||||||
}
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
id: counter,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
return (params = {}) =>
|
return fetch(Lbry.daemonConnectionString, options)
|
||||||
new Promise((resolve, reject) => {
|
.then(checkAndParse)
|
||||||
apiCall(name, params, resolve, reject);
|
.then(response => {
|
||||||
});
|
const error = response.error || (response.result && response.result.error);
|
||||||
},
|
|
||||||
});
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
return resolve(response.result);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
}
|
||||||
|
|
||||||
function getLocal(key, fallback = undefined) {
|
function getLocal(key, fallback = undefined) {
|
||||||
const itemRaw = localStorage.getItem(key);
|
// const itemRaw = localStorage.getItem(key);
|
||||||
|
const itemRaw = null;
|
||||||
return itemRaw === null ? fallback : JSON.parse(itemRaw);
|
return itemRaw === null ? fallback : JSON.parse(itemRaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLocal(key, value) {
|
function setLocal(key, value) {
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
// localStorage.setItem(key, JSON.stringify(value));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to
|
|
||||||
* needed to make a dummy claim or file info object.
|
|
||||||
*/
|
|
||||||
let pendingId = 0;
|
|
||||||
function savePendingPublish({ name, channelName }) {
|
|
||||||
pendingId += 1;
|
|
||||||
const pendingPublishes = getLocal('pendingPublishes') || [];
|
|
||||||
const newPendingPublish = {
|
|
||||||
name,
|
|
||||||
channelName,
|
|
||||||
claim_id: `pending-${pendingId}`,
|
|
||||||
txid: `pending-${pendingId}`,
|
|
||||||
nout: 0,
|
|
||||||
outpoint: `pending-${pendingId}:0`,
|
|
||||||
time: Date.now(),
|
|
||||||
};
|
|
||||||
setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]);
|
|
||||||
return newPendingPublish;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If there is a pending publish with the given name or outpoint, remove it.
|
|
||||||
* A channel name may also be provided along with name.
|
|
||||||
*/
|
|
||||||
function removePendingPublishIfNeeded({ name, channelName, outpoint }) {
|
|
||||||
function pubMatches(pub) {
|
|
||||||
return (
|
|
||||||
pub.outpoint === outpoint ||
|
|
||||||
(pub.name === name && (!channelName || pub.channel_name === channelName))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLocal('pendingPublishes', Lbry.getPendingPublishes().filter(pub => !pubMatches(pub)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current list of pending publish attempts. Filters out any that have timed out and
|
|
||||||
* removes them from the list.
|
|
||||||
*/
|
|
||||||
Lbry.getPendingPublishes = () => {
|
|
||||||
const pendingPublishes = getLocal('pendingPublishes') || [];
|
|
||||||
const newPendingPublishes = pendingPublishes.filter(
|
|
||||||
pub => Date.now() - pub.time <= Lbry.pendingPublishTimeout
|
|
||||||
);
|
|
||||||
setLocal('pendingPublishes', newPendingPublishes);
|
|
||||||
return newPendingPublishes;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be
|
|
||||||
* provided along withe the name. If no pending publish is found, returns null.
|
|
||||||
*/
|
|
||||||
function getPendingPublish({ name, channelName, outpoint }) {
|
|
||||||
const pendingPublishes = Lbry.getPendingPublishes();
|
|
||||||
return (
|
|
||||||
pendingPublishes.find(
|
|
||||||
pub =>
|
|
||||||
pub.outpoint === outpoint ||
|
|
||||||
(pub.name === name && (!channelName || pub.channel_name === channelName))
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pendingPublishToDummyClaim({ channelName, name, outpoint, claimId, txid, nout }) {
|
|
||||||
return { name, outpoint, claimId, txid, nout, channelName };
|
|
||||||
}
|
|
||||||
|
|
||||||
function pendingPublishToDummyFileInfo({ name, outpoint, claimId }) {
|
|
||||||
return { name, outpoint, claimId, metadata: null };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// core
|
// core
|
||||||
|
Lbry.status = () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
apiCall(
|
||||||
|
'status',
|
||||||
|
{},
|
||||||
|
status => {
|
||||||
|
resolve(status);
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.file_delete = (params = {}) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
apiCall('file_delete', params, resolve, reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.file_set_status = (params = {}) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
apiCall('file_set_status', params, resolve, reject);
|
||||||
|
});
|
||||||
|
|
||||||
Lbry.connectPromise = null;
|
Lbry.connectPromise = null;
|
||||||
Lbry.connect = () => {
|
Lbry.connect = () => {
|
||||||
if (Lbry.connectPromise === null) {
|
if (Lbry.connectPromise === null) {
|
||||||
Lbry.connectPromise = new Promise((resolve, reject) => {
|
Lbry.connectPromise = new Promise((resolve, reject) => {
|
||||||
let tryNum = 0;
|
let tryNum = 0;
|
||||||
|
|
||||||
// Check every half second to see if the daemon is accepting connections
|
// Check every half second to see if the daemon is accepting connections
|
||||||
function checkDaemonStarted() {
|
function checkDaemonStarted() {
|
||||||
tryNum += 1;
|
tryNum += 1;
|
||||||
lbryProxy
|
Lbry.status()
|
||||||
.status()
|
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (tryNum <= CHECK_DAEMON_STARTED_TRY_NUMBER) {
|
if (tryNum <= CHECK_DAEMON_STARTED_TRY_NUMBER) {
|
||||||
|
@ -135,42 +108,6 @@ Lbry.connect = () => {
|
||||||
return Lbry.connectPromise;
|
return Lbry.connectPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Publishes a file. The optional fileListedCallback is called when the file becomes available in
|
|
||||||
* lbry.file_list() during the publish process.
|
|
||||||
*
|
|
||||||
* This currently includes a work-around to cache the file in local storage so that the pending
|
|
||||||
* publish can appear in the UI immediately.
|
|
||||||
*/
|
|
||||||
Lbry.publishDeprecated = (params, fileListedCallback, publishedCallback, errorCallback) => {
|
|
||||||
// Give a short grace period in case publish() returns right away or (more likely) gives an error
|
|
||||||
const returnPendingTimeout = setTimeout(
|
|
||||||
() => {
|
|
||||||
const { name, channel_name: channelName } = params;
|
|
||||||
if (publishedCallback || fileListedCallback) {
|
|
||||||
savePendingPublish({
|
|
||||||
name,
|
|
||||||
channelName,
|
|
||||||
});
|
|
||||||
publishedCallback(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
2000,
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
lbryProxy.publish(params).then(
|
|
||||||
result => {
|
|
||||||
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
|
|
||||||
publishedCallback(result);
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
|
|
||||||
errorCallback(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Lbry.getMediaType = (contentType, fileName) => {
|
Lbry.getMediaType = (contentType, fileName) => {
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
return /^[^/]+/.exec(contentType)[0];
|
return /^[^/]+/.exec(contentType)[0];
|
||||||
|
@ -204,35 +141,13 @@ Lbry.getMediaType = (contentType, fileName) => {
|
||||||
*/
|
*/
|
||||||
Lbry.file_list = (params = {}) =>
|
Lbry.file_list = (params = {}) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const { name, channel_name: channelName, outpoint } = params;
|
const { claim_name: claimName, channel_name: channelName, outpoint } = params;
|
||||||
|
|
||||||
/**
|
|
||||||
* If we're searching by outpoint, check first to see if there's a matching pending publish.
|
|
||||||
* Pending publishes use their own faux outpoints that are always unique, so we don't need
|
|
||||||
* to check if there's a real file.
|
|
||||||
*/
|
|
||||||
if (outpoint) {
|
|
||||||
const pendingPublish = getPendingPublish({ outpoint });
|
|
||||||
if (pendingPublish) {
|
|
||||||
resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apiCall(
|
apiCall(
|
||||||
'file_list',
|
'file_list',
|
||||||
params,
|
params,
|
||||||
fileInfos => {
|
fileInfos => {
|
||||||
removePendingPublishIfNeeded({ name, channelName, outpoint });
|
|
||||||
|
|
||||||
// if a naked file_list call, append the pending file infos
|
|
||||||
if (!name && !channelName && !outpoint) {
|
|
||||||
const dummyFileInfos = Lbry.getPendingPublishes().map(pendingPublishToDummyFileInfo);
|
|
||||||
|
|
||||||
resolve([...fileInfos, ...dummyFileInfos]);
|
|
||||||
} else {
|
|
||||||
resolve(fileInfos);
|
resolve(fileInfos);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
reject
|
reject
|
||||||
);
|
);
|
||||||
|
@ -244,16 +159,19 @@ Lbry.claim_list_mine = (params = {}) =>
|
||||||
'claim_list_mine',
|
'claim_list_mine',
|
||||||
params,
|
params,
|
||||||
claims => {
|
claims => {
|
||||||
claims.forEach(({ name, channel_name: channelName, txid, nout }) => {
|
resolve(claims);
|
||||||
removePendingPublishIfNeeded({
|
},
|
||||||
name,
|
reject
|
||||||
channelName,
|
);
|
||||||
outpoint: `${txid}:${nout}`,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dummyClaims = Lbry.getPendingPublishes().map(pendingPublishToDummyClaim);
|
Lbry.get = (params = {}) =>
|
||||||
resolve([...claims, ...dummyClaims]);
|
new Promise((resolve, reject) => {
|
||||||
|
apiCall(
|
||||||
|
'get',
|
||||||
|
params,
|
||||||
|
streamInfo => {
|
||||||
|
resolve(streamInfo);
|
||||||
},
|
},
|
||||||
reject
|
reject
|
||||||
);
|
);
|
||||||
|
@ -276,4 +194,17 @@ Lbry.resolve = (params = {}) =>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lbryProxy = new Proxy(Lbry, {
|
||||||
|
get(target, name) {
|
||||||
|
if (name in target) {
|
||||||
|
return target[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (params = {}) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
apiCall(name, params, resolve, reject);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default lbryProxy;
|
export default lbryProxy;
|
||||||
|
|
230
src/lbryURI.js
Normal file
230
src/lbryURI.js
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
const channelNameMinLength = 1;
|
||||||
|
const claimIdMaxLength = 40;
|
||||||
|
|
||||||
|
export const regexInvalidURI = /[^A-Za-z0-9-]/g;
|
||||||
|
export const regexAddress = /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a LBRY name into its component parts. Throws errors with user-friendly
|
||||||
|
* messages for invalid names.
|
||||||
|
*
|
||||||
|
* N.B. that "name" indicates the value in the name position of the URI. For
|
||||||
|
* claims for channel content, this will actually be the channel name, and
|
||||||
|
* the content name is in the path (e.g. lbry://@channel/content)
|
||||||
|
*
|
||||||
|
* In most situations, you'll want to use the contentName and channelName keys
|
||||||
|
* and ignore the name key.
|
||||||
|
*
|
||||||
|
* Returns a dictionary with keys:
|
||||||
|
* - name (string): The value in the "name" position in the URI. Note that this
|
||||||
|
* could be either content name or channel name; see above.
|
||||||
|
* - path (string, if persent)
|
||||||
|
* - claimSequence (int, if present)
|
||||||
|
* - bidPosition (int, if present)
|
||||||
|
* - claimId (string, if present)
|
||||||
|
* - isChannel (boolean)
|
||||||
|
* - contentName (string): For anon claims, the name; for channel claims, the path
|
||||||
|
* - channelName (string, if present): Channel name without @
|
||||||
|
*/
|
||||||
|
export function parseURI(URI, requireProto = false) {
|
||||||
|
// Break into components. Empty sub-matches are converted to null
|
||||||
|
const componentsRegex = new RegExp(
|
||||||
|
'^((?:lbry://)?)' + // protocol
|
||||||
|
'([^:$#/]*)' + // claim name (stops at the first separator or end)
|
||||||
|
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
|
||||||
|
'(/?)(.*)' // path separator, path
|
||||||
|
);
|
||||||
|
const [proto, claimName, modSep, modVal, pathSep, path] = componentsRegex
|
||||||
|
.exec(URI)
|
||||||
|
.slice(1)
|
||||||
|
.map(match => match || null);
|
||||||
|
|
||||||
|
let contentName;
|
||||||
|
|
||||||
|
// Validate protocol
|
||||||
|
if (requireProto && !proto) {
|
||||||
|
throw new Error(__('LBRY URIs must include a protocol prefix (lbry://).'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and process name
|
||||||
|
if (!claimName) {
|
||||||
|
throw new Error(__('URI does not include name.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChannel = claimName.startsWith('@');
|
||||||
|
const channelName = isChannel ? claimName.slice(1) : claimName;
|
||||||
|
|
||||||
|
if (isChannel) {
|
||||||
|
if (!channelName) {
|
||||||
|
throw new Error(__('No channel name after @.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelName.length < channelNameMinLength) {
|
||||||
|
throw new Error(__(`Channel names must be at least %s characters.`, channelNameMinLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
contentName = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameBadChars = (channelName || claimName).match(regexInvalidURI);
|
||||||
|
if (nameBadChars) {
|
||||||
|
throw new Error(
|
||||||
|
__(
|
||||||
|
`Invalid character %s in name: %s.`,
|
||||||
|
nameBadChars.length === 1 ? '' : 's',
|
||||||
|
nameBadChars.join(', ')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and process modifier (claim ID, bid position or claim sequence)
|
||||||
|
let claimId;
|
||||||
|
let claimSequence;
|
||||||
|
let bidPosition;
|
||||||
|
if (modSep) {
|
||||||
|
if (!modVal) {
|
||||||
|
throw new Error(__(`No modifier provided after separator %s.`, modSep));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modSep === '#') {
|
||||||
|
claimId = modVal;
|
||||||
|
} else if (modSep === ':') {
|
||||||
|
claimSequence = modVal;
|
||||||
|
} else if (modSep === '$') {
|
||||||
|
bidPosition = modVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
claimId &&
|
||||||
|
(claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/)) &&
|
||||||
|
!claimId.match(/^pending/) // ought to be dropped when savePendingPublish drops hack
|
||||||
|
) {
|
||||||
|
throw new Error(__(`Invalid claim ID %s.`, claimId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
|
||||||
|
throw new Error(__('Claim sequence must be a number.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
|
||||||
|
throw new Error(__('Bid position must be a number.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and process path
|
||||||
|
if (path) {
|
||||||
|
if (!isChannel) {
|
||||||
|
throw new Error(__('Only channel URIs may have a path.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathBadChars = path.match(regexInvalidURI);
|
||||||
|
if (pathBadChars) {
|
||||||
|
throw new Error(__(`Invalid character in path: %s`, pathBadChars.join(', ')));
|
||||||
|
}
|
||||||
|
|
||||||
|
contentName = path;
|
||||||
|
} else if (pathSep) {
|
||||||
|
throw new Error(__('No path provided after /'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimName,
|
||||||
|
path,
|
||||||
|
isChannel,
|
||||||
|
...(contentName ? { contentName } : {}),
|
||||||
|
...(channelName ? { channelName } : {}),
|
||||||
|
...(claimSequence ? { claimSequence: parseInt(claimSequence, 10) } : {}),
|
||||||
|
...(bidPosition ? { bidPosition: parseInt(bidPosition, 10) } : {}),
|
||||||
|
...(claimId ? { claimId } : {}),
|
||||||
|
...(path ? { path } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes an object in the same format returned by parse() and builds a URI.
|
||||||
|
*
|
||||||
|
* The channelName key will accept names with or without the @ prefix.
|
||||||
|
*/
|
||||||
|
export function buildURI(URIObj, includeProto = true) {
|
||||||
|
const { claimId, claimSequence, bidPosition, contentName, channelName } = URIObj;
|
||||||
|
|
||||||
|
let { claimName, path } = URIObj;
|
||||||
|
|
||||||
|
if (channelName) {
|
||||||
|
const channelNameFormatted = channelName.startsWith('@') ? channelName : `@${channelName}`;
|
||||||
|
if (!claimName) {
|
||||||
|
claimName = channelNameFormatted;
|
||||||
|
} else if (claimName !== channelNameFormatted) {
|
||||||
|
throw new Error(
|
||||||
|
__(
|
||||||
|
'Received a channel content URI, but claim name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentName) {
|
||||||
|
if (!claimName) {
|
||||||
|
claimName = contentName;
|
||||||
|
} else if (!path) {
|
||||||
|
path = contentName;
|
||||||
|
}
|
||||||
|
if (path && path !== contentName) {
|
||||||
|
throw new Error(
|
||||||
|
__(
|
||||||
|
'Path and contentName do not match. Only one is required; most likely you wanted contentName.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(includeProto ? 'lbry://' : '') +
|
||||||
|
claimName +
|
||||||
|
(claimId ? `#${claimId}` : '') +
|
||||||
|
(claimSequence ? `:${claimSequence}` : '') +
|
||||||
|
(bidPosition ? `${bidPosition}` : '') +
|
||||||
|
(path ? `/${path}` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Takes a parseable LBRY URI and converts it to standard, canonical format */
|
||||||
|
export function normalizeURI(URI) {
|
||||||
|
if (URI.match(/pending_claim/)) return URI;
|
||||||
|
|
||||||
|
const { claimName, path, bidPosition, claimSequence, claimId } = parseURI(URI);
|
||||||
|
return buildURI({ claimName, path, claimSequence, bidPosition, claimId });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isURIValid(URI) {
|
||||||
|
let parts;
|
||||||
|
try {
|
||||||
|
parts = parseURI(normalizeURI(URI));
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return parts && parts.claimName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNameValid(claimName, checkCase = true) {
|
||||||
|
const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i');
|
||||||
|
return regexp.test(claimName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isURIClaimable(URI) {
|
||||||
|
let parts;
|
||||||
|
try {
|
||||||
|
parts = parseURI(normalizeURI(URI));
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
parts &&
|
||||||
|
parts.claimName &&
|
||||||
|
!parts.claimId &&
|
||||||
|
!parts.bidPosition &&
|
||||||
|
!parts.claimSequence &&
|
||||||
|
!parts.isChannel &&
|
||||||
|
!parts.path
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import Lbry from 'lbry';
|
|
||||||
import querystring from 'querystring';
|
import querystring from 'querystring';
|
||||||
|
|
||||||
const LbryApi = {
|
const Lbryapi = {
|
||||||
|
enabled: true,
|
||||||
exchangePromise: null,
|
exchangePromise: null,
|
||||||
exchangeLastFetched: null,
|
exchangeLastFetched: null,
|
||||||
};
|
};
|
||||||
|
@ -12,27 +12,26 @@ const CONNECTION_STRING = process.env.LBRY_APP_API_URL
|
||||||
|
|
||||||
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
|
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
|
||||||
|
|
||||||
LbryApi.getExchangeRates = () => {
|
Lbryapi.getExchangeRates = () => {
|
||||||
if (
|
if (
|
||||||
!LbryApi.exchangeLastFetched ||
|
!Lbryapi.exchangeLastFetched ||
|
||||||
Date.now() - LbryApi.exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
|
Date.now() - Lbryapi.exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
|
||||||
) {
|
) {
|
||||||
LbryApi.exchangePromise = new Promise((resolve, reject) => {
|
Lbryapi.exchangePromise = new Promise((resolve, reject) => {
|
||||||
LbryApi.call('lbc', 'exchange_rate', {}, 'get', true)
|
Lbryapi.call('lbc', 'exchange_rate', {}, 'get', true)
|
||||||
.then(({ lbc_usd: LBC_USD, lbc_btc: LBC_BTC, btc_usd: BTC_USD }) => {
|
.then(({ lbc_usd: LBC_USD, lbc_btc: LBC_BTC, btc_usd: BTC_USD }) => {
|
||||||
const rates = { LBC_USD, LBC_BTC, BTC_USD };
|
const rates = { LBC_USD, LBC_BTC, BTC_USD };
|
||||||
resolve(rates);
|
resolve(rates);
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
LbryApi.exchangeLastFetched = Date.now();
|
Lbryapi.exchangeLastFetched = Date.now();
|
||||||
}
|
}
|
||||||
return LbryApi.exchangePromise;
|
return Lbryapi.exchangePromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
LbryApi.call = (resource, action, params = {}, method = 'get') => {
|
Lbryapi.call = (resource, action, params = {}, method = 'get') => {
|
||||||
if (!Lbryio.enabled) {
|
if (!Lbryapi.enabled) {
|
||||||
console.log(__('Internal API disabled'));
|
|
||||||
return Promise.reject(new Error(__('LBRY internal API is disabled')));
|
return Promise.reject(new Error(__('LBRY internal API is disabled')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,4 +81,4 @@ LbryApi.call = (resource, action, params = {}, method = 'get') => {
|
||||||
return makeRequest(url, options).then(response => response.data);
|
return makeRequest(url, options).then(response => response.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LbryApi;
|
export default Lbryapi;
|
||||||
|
|
188
src/redux/actions/claims.js
Normal file
188
src/redux/actions/claims.js
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import Lbry from 'lbry';
|
||||||
|
import Lbryapi from 'lbryapi';
|
||||||
|
import { buildURI, normalizeURI } from 'lbryURI';
|
||||||
|
import { doNotify } from 'redux/actions/notifications';
|
||||||
|
import { selectMyClaimsRaw, selectResolvingUris } from 'redux/selectors/claims';
|
||||||
|
import { batchActions } from 'util/batchActions';
|
||||||
|
|
||||||
|
export function doResolveUris(uris) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const normalizedUris = uris.map(normalizeURI);
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
// Filter out URIs that are already resolving
|
||||||
|
const resolvingUris = selectResolvingUris(state);
|
||||||
|
const urisToResolve = normalizedUris.filter(uri => !resolvingUris.includes(uri));
|
||||||
|
|
||||||
|
if (urisToResolve.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.RESOLVE_URIS_STARTED,
|
||||||
|
data: { uris: normalizedUris },
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolveInfo = {};
|
||||||
|
Lbry.resolve({ uris: urisToResolve }).then(result => {
|
||||||
|
Object.entries(result).forEach(([uri, uriResolveInfo]) => {
|
||||||
|
const fallbackResolveInfo = {
|
||||||
|
claim: null,
|
||||||
|
claimsInChannel: null,
|
||||||
|
certificate: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { claim, certificate, claims_in_channel: claimsInChannel } =
|
||||||
|
uriResolveInfo && !uriResolveInfo.error ? uriResolveInfo : fallbackResolveInfo;
|
||||||
|
|
||||||
|
resolveInfo[uri] = { claim, certificate, claimsInChannel };
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.RESOLVE_URIS_COMPLETED,
|
||||||
|
data: { resolveInfo },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doResolveUri(uri) {
|
||||||
|
return doResolveUris([uri]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchClaimListMine() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_CLAIM_LIST_MINE_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.claim_list_mine().then(claims => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
|
||||||
|
data: {
|
||||||
|
claims,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doAbandonClaim(txid, nout) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const myClaims = selectMyClaimsRaw(state);
|
||||||
|
const { claim_id: claimId, name } = myClaims.find(
|
||||||
|
claim => claim.txid === txid && claim.nout === nout
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.ABANDON_CLAIM_STARTED,
|
||||||
|
data: {
|
||||||
|
claimId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorCallback = () => {
|
||||||
|
dispatch(
|
||||||
|
doNotify({
|
||||||
|
title: 'Transaction failed',
|
||||||
|
message: 'Transaction failed',
|
||||||
|
type: 'error',
|
||||||
|
displayType: ['modal', 'toast'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const successCallback = results => {
|
||||||
|
if (results.txid) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.ABANDON_CLAIM_SUCCEEDED,
|
||||||
|
data: {
|
||||||
|
claimId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch(doResolveUri(buildURI({ name, claimId })));
|
||||||
|
dispatch(doFetchClaimListMine());
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
doNotify({
|
||||||
|
title: 'Transaction failed',
|
||||||
|
message: 'Transaction failed',
|
||||||
|
type: 'error',
|
||||||
|
displayType: ['modal', 'toast'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Lbry.claim_abandon({
|
||||||
|
txid,
|
||||||
|
nout,
|
||||||
|
}).then(successCallback, errorCallback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchFeaturedUris() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_FEATURED_CONTENT_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
const success = ({ Uris }) => {
|
||||||
|
let urisToResolve = [];
|
||||||
|
Object.keys(Uris).forEach(category => {
|
||||||
|
urisToResolve = [...urisToResolve, ...Uris[category]];
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
doResolveUris(urisToResolve),
|
||||||
|
{
|
||||||
|
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uris: Uris,
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
dispatch(batchActions(...actions));
|
||||||
|
};
|
||||||
|
|
||||||
|
const failure = () => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
|
||||||
|
data: {
|
||||||
|
uris: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Lbryapi.call('file', 'list_homepage').then(success, failure);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Lbryapi.call('reward', 'list_featured').then(success, failure);
|
||||||
|
};
|
||||||
|
}
|
66
src/redux/actions/file_info.js
Normal file
66
src/redux/actions/file_info.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import Lbry from 'lbry';
|
||||||
|
import { doFetchClaimListMine } from 'redux/actions/claims';
|
||||||
|
import { selectClaimsByUri, selectIsFetchingClaimListMine } from 'redux/selectors/claims';
|
||||||
|
import { selectIsFetchingFileList, selectUrisLoading } from 'redux/selectors/file_info';
|
||||||
|
|
||||||
|
export function doFetchFileInfo(uri) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const claim = selectClaimsByUri(state)[uri];
|
||||||
|
const outpoint = claim ? `${claim.txid}:${claim.nout}` : null;
|
||||||
|
const alreadyFetching = !!selectUrisLoading(state)[uri];
|
||||||
|
|
||||||
|
if (!alreadyFetching) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_FILE_INFO_STARTED,
|
||||||
|
data: {
|
||||||
|
outpoint,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.file_list({ outpoint, full_status: true }).then(fileInfos => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_FILE_INFO_COMPLETED,
|
||||||
|
data: {
|
||||||
|
outpoint,
|
||||||
|
fileInfo: fileInfos && fileInfos.length ? fileInfos[0] : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFileList() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const isFetching = selectIsFetchingFileList(state);
|
||||||
|
|
||||||
|
if (!isFetching) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FILE_LIST_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.file_list().then(fileInfos => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FILE_LIST_SUCCEEDED,
|
||||||
|
data: {
|
||||||
|
fileInfos,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchFileInfosAndPublishedClaims() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const isFetchingClaimListMine = selectIsFetchingClaimListMine(state);
|
||||||
|
const isFetchingFileInfo = selectIsFetchingFileList(state);
|
||||||
|
|
||||||
|
if (!isFetchingClaimListMine) dispatch(doFetchClaimListMine());
|
||||||
|
if (!isFetchingFileInfo) dispatch(doFileList());
|
||||||
|
};
|
||||||
|
}
|
10
src/redux/actions/notifications.js
Normal file
10
src/redux/actions/notifications.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// @flow
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import Notification from 'types/Notification';
|
||||||
|
|
||||||
|
export function doNotify(data: Notification) {
|
||||||
|
return {
|
||||||
|
type: ACTIONS.CREATE_NOTIFICATION,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
174
src/redux/actions/search.js
Normal file
174
src/redux/actions/search.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
// @flow
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import * as SEARCH_TYPES from 'constants/search';
|
||||||
|
import { normalizeURI, buildURI, parseURI } from 'lbryURI';
|
||||||
|
import { doResolveUri } from 'redux/actions/claims';
|
||||||
|
import { makeSelectSearchUris } from 'redux/selectors/search';
|
||||||
|
import { batchActions } from 'util/batchActions';
|
||||||
|
import handleFetchResponse from 'util/handle-fetch';
|
||||||
|
|
||||||
|
export const doSearch = rawQuery => (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const query = rawQuery.replace(/^lbry:\/\//i, '');
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SEARCH_FAIL,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have already searched for something, we don't need to do anything
|
||||||
|
const urisForQuery = makeSelectSearchUris(query)(state);
|
||||||
|
if (urisForQuery && !!urisForQuery.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SEARCH_START,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the user is on the file page with a pre-populated uri and they select
|
||||||
|
// the search option without typing anything, searchQuery will be empty
|
||||||
|
// We need to populate it so the input is filled on the search page
|
||||||
|
if (!state.search.searchQuery) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_QUERY,
|
||||||
|
data: { searchQuery: query },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`https://lighthouse.lbry.io/search?s=${query}`)
|
||||||
|
.then(handleFetchResponse)
|
||||||
|
.then(data => {
|
||||||
|
const uris = [];
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
data.forEach(result => {
|
||||||
|
const uri = buildURI({
|
||||||
|
claimName: result.name,
|
||||||
|
claimId: result.claimId,
|
||||||
|
});
|
||||||
|
actions.push(doResolveUri(uri));
|
||||||
|
uris.push(uri);
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
type: ACTIONS.SEARCH_SUCCESS,
|
||||||
|
data: {
|
||||||
|
query,
|
||||||
|
uris,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch(batchActions(...actions));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SEARCH_FAIL,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boolean) => dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_QUERY,
|
||||||
|
data: { query },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't fetch new suggestions if the user just added a space
|
||||||
|
if (!query.endsWith(' ') || !shouldSkipSuggestions) {
|
||||||
|
dispatch(getSearchSuggestions(query));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSearchSuggestions = (value: string) => dispatch => {
|
||||||
|
const query = value.trim();
|
||||||
|
|
||||||
|
const isPrefix = () =>
|
||||||
|
query === '@' || query === 'lbry:' || query === 'lbry:/' || query === 'lbry://';
|
||||||
|
|
||||||
|
if (!query || isPrefix()) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
|
||||||
|
data: { suggestions: [] },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let suggestions = [];
|
||||||
|
try {
|
||||||
|
// If the user is about to manually add the claim id ignore it until they
|
||||||
|
// actually add one. This would hardly ever happen, but then the search
|
||||||
|
// suggestions won't change just from adding a '#' after a uri
|
||||||
|
let uriQuery = query;
|
||||||
|
if (uriQuery.endsWith('#')) {
|
||||||
|
uriQuery = uriQuery.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = normalizeURI(uriQuery);
|
||||||
|
const { claimName, isChannel } = parseURI(uri);
|
||||||
|
|
||||||
|
suggestions.push(
|
||||||
|
{
|
||||||
|
value: uri,
|
||||||
|
shorthand: isChannel ? claimName.slice(1) : claimName,
|
||||||
|
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: claimName,
|
||||||
|
type: SEARCH_TYPES.SEARCH,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If it's a valid url, don't fetch any extra search results
|
||||||
|
return dispatch({
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
|
||||||
|
data: { suggestions },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
suggestions.push({
|
||||||
|
value: query,
|
||||||
|
type: SEARCH_TYPES.SEARCH,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the current search query suggestion before fetching results
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
|
||||||
|
data: { suggestions },
|
||||||
|
});
|
||||||
|
|
||||||
|
// strip out any basic stuff for more accurate search results
|
||||||
|
let searchValue = value.replace(/lbry:\/\//g, '').replace(/-/g, ' ');
|
||||||
|
if (searchValue.includes('#')) {
|
||||||
|
// This should probably be more robust, but I think it's fine for now
|
||||||
|
// Remove everything after # to get rid of the claim id
|
||||||
|
searchValue = searchValue.substring(0, searchValue.indexOf('#'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`https://lighthouse.lbry.io/autocomplete?s=${searchValue}`)
|
||||||
|
.then(handleFetchResponse)
|
||||||
|
.then(apiSuggestions => {
|
||||||
|
const formattedSuggestions = apiSuggestions.slice(0, 6).map(suggestion => {
|
||||||
|
// This will need to be more robust when the api starts returning lbry uris
|
||||||
|
const isChannel = suggestion.startsWith('@');
|
||||||
|
const suggestionObj = {
|
||||||
|
value: isChannel ? `lbry://${suggestion}` : suggestion,
|
||||||
|
shorthand: isChannel ? suggestion.slice(1) : '',
|
||||||
|
type: isChannel ? 'channel' : 'search',
|
||||||
|
};
|
||||||
|
|
||||||
|
return suggestionObj;
|
||||||
|
});
|
||||||
|
|
||||||
|
suggestions = suggestions.concat(formattedSuggestions);
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
|
||||||
|
data: { suggestions },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// If the fetch fails, do nothing
|
||||||
|
// Basic search suggestions are already populated at this point
|
||||||
|
});
|
||||||
|
};
|
209
src/redux/actions/wallet.js
Normal file
209
src/redux/actions/wallet.js
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import Lbry from 'lbry';
|
||||||
|
import { doNotify } from 'redux/actions/notifications';
|
||||||
|
import {
|
||||||
|
selectBalance,
|
||||||
|
selectDraftTransaction,
|
||||||
|
selectDraftTransactionAmount,
|
||||||
|
} from 'redux/selectors/wallet';
|
||||||
|
|
||||||
|
export function doUpdateBalance() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { wallet: { balance: balanceInStore } } = getState();
|
||||||
|
Lbry.wallet_balance().then(balance => {
|
||||||
|
if (balanceInStore !== balance) {
|
||||||
|
return dispatch({
|
||||||
|
type: ACTIONS.UPDATE_BALANCE,
|
||||||
|
data: {
|
||||||
|
balance,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doBalanceSubscribe() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch(doUpdateBalance());
|
||||||
|
setInterval(() => dispatch(doUpdateBalance()), 5000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchTransactions() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_TRANSACTIONS_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.transaction_list().then(results => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_TRANSACTIONS_COMPLETED,
|
||||||
|
data: {
|
||||||
|
transactions: results,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchBlock(height) {
|
||||||
|
return dispatch => {
|
||||||
|
Lbry.block_show({ height }).then(block => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.FETCH_BLOCK_SUCCESS,
|
||||||
|
data: { block },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doGetNewAddress() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GET_NEW_ADDRESS_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Removed localStorage use, since address is expected to be stored in redux store
|
||||||
|
Lbry.wallet_new_address().then(address => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GET_NEW_ADDRESS_COMPLETED,
|
||||||
|
data: { address },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doCheckAddressIsMine(address) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.CHECK_ADDRESS_IS_MINE_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.wallet_is_address_mine({ address }).then(isMine => {
|
||||||
|
if (!isMine) dispatch(doGetNewAddress());
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.CHECK_ADDRESS_IS_MINE_COMPLETED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSendDraftTransaction() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const draftTx = selectDraftTransaction(state);
|
||||||
|
const balance = selectBalance(state);
|
||||||
|
const amount = selectDraftTransactionAmount(state);
|
||||||
|
|
||||||
|
if (balance - amount <= 0) {
|
||||||
|
dispatch(
|
||||||
|
doNotify({
|
||||||
|
title: 'Insufficient credits',
|
||||||
|
message: 'Insufficient credits',
|
||||||
|
type: 'error',
|
||||||
|
displayType: ['modal', 'toast'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SEND_TRANSACTION_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
const successCallback = results => {
|
||||||
|
if (results === true) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SEND_TRANSACTION_COMPLETED,
|
||||||
|
});
|
||||||
|
dispatch(
|
||||||
|
doNotify({
|
||||||
|
title: 'Credits sent',
|
||||||
|
message: `You sent ${amount} LBC`,
|
||||||
|
type: 'error',
|
||||||
|
displayType: ['snackbar', 'toast'],
|
||||||
|
linkText: 'History',
|
||||||
|
linkTarget: '/wallet',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SEND_TRANSACTION_FAILED,
|
||||||
|
data: { error: results },
|
||||||
|
});
|
||||||
|
dispatch(
|
||||||
|
doNotify({
|
||||||
|
title: 'Transaction failed',
|
||||||
|
message: 'Transaction failed',
|
||||||
|
type: 'error',
|
||||||
|
displayType: ['modal', 'toast'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorCallback = error => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SEND_TRANSACTION_FAILED,
|
||||||
|
data: { error: error.message },
|
||||||
|
});
|
||||||
|
dispatch(
|
||||||
|
doNotify({
|
||||||
|
title: 'Transaction failed',
|
||||||
|
message: 'Transaction failed',
|
||||||
|
type: 'error',
|
||||||
|
displayType: ['modal', 'toast'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Lbry.wallet_send({
|
||||||
|
amount: draftTx.amount,
|
||||||
|
address: draftTx.address,
|
||||||
|
}).then(successCallback, errorCallback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSetDraftTransactionAmount(amount) {
|
||||||
|
return {
|
||||||
|
type: ACTIONS.SET_DRAFT_TRANSACTION_AMOUNT,
|
||||||
|
data: { amount },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSetDraftTransactionAddress(address) {
|
||||||
|
return {
|
||||||
|
type: ACTIONS.SET_DRAFT_TRANSACTION_ADDRESS,
|
||||||
|
data: { address },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSendSupport(amount, claimId, uri, successCallback, errorCallback) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const balance = selectBalance(state);
|
||||||
|
|
||||||
|
if (balance - amount <= 0) {
|
||||||
|
dispatch(
|
||||||
|
doNotify({
|
||||||
|
title: 'Insufficient credits',
|
||||||
|
message: 'Insufficient credits',
|
||||||
|
type: 'error',
|
||||||
|
displayType: ['modal', 'toast'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SUPPORT_TRANSACTION_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbry.wallet_send({
|
||||||
|
claim_id: claimId,
|
||||||
|
amount,
|
||||||
|
}).then(successCallback, errorCallback);
|
||||||
|
};
|
||||||
|
}
|
246
src/redux/reducers/claims.js
Normal file
246
src/redux/reducers/claims.js
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
|
const reducers = {};
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
rewardedContentClaimIds: [],
|
||||||
|
channelClaimCounts: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.RESOLVE_URIS_COMPLETED] = (state, action) => {
|
||||||
|
const { resolveInfo } = action.data;
|
||||||
|
const byUri = Object.assign({}, state.claimsByUri);
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
|
||||||
|
|
||||||
|
Object.entries(resolveInfo).forEach(([uri, { certificate, claimsInChannel }]) => {
|
||||||
|
if (certificate && !Number.isNaN(claimsInChannel)) {
|
||||||
|
channelClaimCounts[uri] = claimsInChannel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(resolveInfo).forEach(([uri, { certificate, claim }]) => {
|
||||||
|
if (claim) {
|
||||||
|
byId[claim.claim_id] = claim;
|
||||||
|
byUri[uri] = claim.claim_id;
|
||||||
|
} else if (claim === undefined && certificate !== undefined) {
|
||||||
|
byId[certificate.claim_id] = certificate;
|
||||||
|
// Don't point URI at the channel certificate unless it actually is
|
||||||
|
// a channel URI. This is brittle.
|
||||||
|
if (!uri.split(certificate.name)[1].match(/\//)) {
|
||||||
|
byUri[uri] = certificate.claim_id;
|
||||||
|
} else {
|
||||||
|
byUri[uri] = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
byUri[uri] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byId,
|
||||||
|
claimsByUri: byUri,
|
||||||
|
channelClaimCounts,
|
||||||
|
resolvingUris: (state.resolvingUris || []).filter(uri => !resolveInfo[uri]),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_CLAIM_LIST_MINE_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
isFetchingClaimListMine: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED] = (state, action) => {
|
||||||
|
const { claims } = action.data;
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const pendingById = Object.assign({}, state.pendingById);
|
||||||
|
|
||||||
|
claims.filter(claim => claim.category && claim.category.match(/claim/)).forEach(claim => {
|
||||||
|
byId[claim.claim_id] = claim;
|
||||||
|
|
||||||
|
const pending = Object.values(pendingById).find(
|
||||||
|
pendingClaim =>
|
||||||
|
pendingClaim.name === claim.name && pendingClaim.channel_name === claim.channel_name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
delete pendingById[pending.claim_id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove old timed out pending publishes
|
||||||
|
Object.values(pendingById)
|
||||||
|
.filter(pendingClaim => Date.now() - pendingClaim.time >= 20 * 60 * 1000)
|
||||||
|
.forEach(pendingClaim => {
|
||||||
|
delete pendingById[pendingClaim.claim_id];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
isFetchingClaimListMine: false,
|
||||||
|
myClaims: claims,
|
||||||
|
byId,
|
||||||
|
pendingById,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_CHANNEL_LIST_MINE_STARTED] = state =>
|
||||||
|
Object.assign({}, state, { fetchingMyChannels: true });
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_CHANNEL_LIST_MINE_COMPLETED] = (state, action) => {
|
||||||
|
const { claims } = action.data;
|
||||||
|
const myChannelClaims = new Set(state.myChannelClaims);
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
|
||||||
|
claims.forEach(claim => {
|
||||||
|
myChannelClaims.add(claim.claim_id);
|
||||||
|
byId[claims.claim_id] = claim;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byId,
|
||||||
|
fetchingMyChannels: false,
|
||||||
|
myChannelClaims,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED] = (state, action) => {
|
||||||
|
const { uri, page } = action.data;
|
||||||
|
const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims);
|
||||||
|
|
||||||
|
fetchingChannelClaims[uri] = page;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetchingChannelClaims,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED] = (state, action) => {
|
||||||
|
const { uri, claims, page } = action.data;
|
||||||
|
|
||||||
|
const claimsByChannel = Object.assign({}, state.claimsByChannel);
|
||||||
|
const byChannel = Object.assign({}, claimsByChannel[uri]);
|
||||||
|
const allClaimIds = new Set(byChannel.all);
|
||||||
|
const currentPageClaimIds = [];
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims);
|
||||||
|
|
||||||
|
if (claims !== undefined) {
|
||||||
|
claims.forEach(claim => {
|
||||||
|
allClaimIds.add(claim.claim_id);
|
||||||
|
currentPageClaimIds.push(claim.claim_id);
|
||||||
|
byId[claim.claim_id] = claim;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
byChannel.all = allClaimIds;
|
||||||
|
byChannel[page] = currentPageClaimIds;
|
||||||
|
claimsByChannel[uri] = byChannel;
|
||||||
|
delete fetchingChannelClaims[uri];
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
claimsByChannel,
|
||||||
|
byId,
|
||||||
|
fetchingChannelClaims,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.ABANDON_CLAIM_STARTED] = (state, action) => {
|
||||||
|
const { claimId } = action.data;
|
||||||
|
const abandoningById = Object.assign({}, state.abandoningById);
|
||||||
|
|
||||||
|
abandoningById[claimId] = true;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
abandoningById,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.ABANDON_CLAIM_SUCCEEDED] = (state, action) => {
|
||||||
|
const { claimId } = action.data;
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const claimsByUri = Object.assign({}, state.claimsByUri);
|
||||||
|
|
||||||
|
Object.keys(claimsByUri).forEach(uri => {
|
||||||
|
if (claimsByUri[uri] === claimId) {
|
||||||
|
delete claimsByUri[uri];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delete byId[claimId];
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byId,
|
||||||
|
claimsByUri,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.CREATE_CHANNEL_COMPLETED] = (state, action) => {
|
||||||
|
const { channelClaim } = action.data;
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const myChannelClaims = new Set(state.myChannelClaims);
|
||||||
|
|
||||||
|
byId[channelClaim.claim_id] = channelClaim;
|
||||||
|
myChannelClaims.add(channelClaim.claim_id);
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byId,
|
||||||
|
myChannelClaims,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_FEATURED_CONTENT_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
fetchingFeaturedContent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED] = (state, action) => {
|
||||||
|
const { uris, success } = action.data;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetchingFeaturedContent: false,
|
||||||
|
fetchingFeaturedContentFailed: !success,
|
||||||
|
featuredUris: uris,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_REWARD_CONTENT_COMPLETED] = (state, action) => {
|
||||||
|
const { claimIds } = action.data;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
rewardedContentClaimIds: claimIds,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.RESOLVE_URIS_STARTED] = (state, action) => {
|
||||||
|
const { uris } = action.data;
|
||||||
|
|
||||||
|
const oldResolving = state.resolvingUris || [];
|
||||||
|
const newResolving = Object.assign([], oldResolving);
|
||||||
|
|
||||||
|
uris.forEach(uri => {
|
||||||
|
if (!newResolving.includes(uri)) {
|
||||||
|
newResolving.push(uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
resolvingUris: newResolving,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_CHANNEL_CLAIM_COUNT_COMPLETED] = (state, action) => {
|
||||||
|
const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
|
||||||
|
const { uri, totalClaims } = action.data;
|
||||||
|
|
||||||
|
channelClaimCounts[uri] = totalClaims;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
channelClaimCounts,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function claimsReducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ reducers[ACTIONS.FETCH_COST_INFO_COMPLETED] = (state, action) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function reducer(state = defaultState, action) {
|
export function costInfoReducer(state = defaultState, action) {
|
||||||
const handler = reducers[action.type];
|
const handler = reducers[action.type];
|
||||||
if (handler) return handler(state, action);
|
if (handler) return handler(state, action);
|
||||||
return state;
|
return state;
|
||||||
|
|
156
src/redux/reducers/file_info.js
Normal file
156
src/redux/reducers/file_info.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
|
const reducers = {};
|
||||||
|
const defaultState = {};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FILE_LIST_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
isFetchingFileList: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.FILE_LIST_SUCCEEDED] = (state, action) => {
|
||||||
|
const { fileInfos } = action.data;
|
||||||
|
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||||
|
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
|
||||||
|
|
||||||
|
fileInfos.forEach(fileInfo => {
|
||||||
|
const { outpoint } = fileInfo;
|
||||||
|
|
||||||
|
if (outpoint) newByOutpoint[fileInfo.outpoint] = fileInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
isFetchingFileList: false,
|
||||||
|
byOutpoint: newByOutpoint,
|
||||||
|
pendingByOutpoint,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_FILE_INFO_STARTED] = (state, action) => {
|
||||||
|
const { outpoint } = action.data;
|
||||||
|
const newFetching = Object.assign({}, state.fetching);
|
||||||
|
|
||||||
|
newFetching[outpoint] = true;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fetching: newFetching,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_FILE_INFO_COMPLETED] = (state, action) => {
|
||||||
|
const { fileInfo, outpoint } = action.data;
|
||||||
|
|
||||||
|
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||||
|
const newFetching = Object.assign({}, state.fetching);
|
||||||
|
|
||||||
|
newByOutpoint[outpoint] = fileInfo;
|
||||||
|
delete newFetching[outpoint];
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byOutpoint: newByOutpoint,
|
||||||
|
fetching: newFetching,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.DOWNLOADING_STARTED] = (state, action) => {
|
||||||
|
const { uri, outpoint, fileInfo } = action.data;
|
||||||
|
|
||||||
|
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||||
|
const newDownloading = Object.assign({}, state.downloadingByOutpoint);
|
||||||
|
const newLoading = Object.assign({}, state.urisLoading);
|
||||||
|
|
||||||
|
newDownloading[outpoint] = true;
|
||||||
|
newByOutpoint[outpoint] = fileInfo;
|
||||||
|
delete newLoading[uri];
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
downloadingByOutpoint: newDownloading,
|
||||||
|
urisLoading: newLoading,
|
||||||
|
byOutpoint: newByOutpoint,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.DOWNLOADING_PROGRESSED] = (state, action) => {
|
||||||
|
const { outpoint, fileInfo } = action.data;
|
||||||
|
|
||||||
|
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||||
|
const newDownloading = Object.assign({}, state.downloadingByOutpoint);
|
||||||
|
|
||||||
|
newByOutpoint[outpoint] = fileInfo;
|
||||||
|
newDownloading[outpoint] = true;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byOutpoint: newByOutpoint,
|
||||||
|
downloadingByOutpoint: newDownloading,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.DOWNLOADING_COMPLETED] = (state, action) => {
|
||||||
|
const { outpoint, fileInfo } = action.data;
|
||||||
|
|
||||||
|
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||||
|
const newDownloading = Object.assign({}, state.downloadingByOutpoint);
|
||||||
|
|
||||||
|
newByOutpoint[outpoint] = fileInfo;
|
||||||
|
delete newDownloading[outpoint];
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byOutpoint: newByOutpoint,
|
||||||
|
downloadingByOutpoint: newDownloading,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FILE_DELETE] = (state, action) => {
|
||||||
|
const { outpoint } = action.data;
|
||||||
|
|
||||||
|
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||||
|
const downloadingByOutpoint = Object.assign({}, state.downloadingByOutpoint);
|
||||||
|
|
||||||
|
delete newByOutpoint[outpoint];
|
||||||
|
delete downloadingByOutpoint[outpoint];
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
byOutpoint: newByOutpoint,
|
||||||
|
downloadingByOutpoint,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.LOADING_VIDEO_STARTED] = (state, action) => {
|
||||||
|
const { uri } = action.data;
|
||||||
|
|
||||||
|
const newLoading = Object.assign({}, state.urisLoading);
|
||||||
|
|
||||||
|
newLoading[uri] = true;
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
urisLoading: newLoading,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.LOADING_VIDEO_FAILED] = (state, action) => {
|
||||||
|
const { uri } = action.data;
|
||||||
|
|
||||||
|
const newLoading = Object.assign({}, state.urisLoading);
|
||||||
|
|
||||||
|
delete newLoading[uri];
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
urisLoading: newLoading,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_DATE] = (state, action) => {
|
||||||
|
const { time } = action.data;
|
||||||
|
if (time) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
publishedDate: time,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fileInfoReducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
39
src/redux/reducers/notifications.js
Normal file
39
src/redux/reducers/notifications.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
|
const reducers = {};
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
// First-in, first-out
|
||||||
|
queue: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.NOTIFICATION_CREATED] = (state, action) => {
|
||||||
|
const { title, message, type, errorCode, displayType } = action.data;
|
||||||
|
const queue = Object.assign([], state.queue);
|
||||||
|
queue.push({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
errorCode,
|
||||||
|
displayType,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
queue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.NOTIFICATION_DISPLAYED] = state => {
|
||||||
|
const queue = Object.assign([], state.queue);
|
||||||
|
queue.shift();
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
queue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function notificationsReducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
103
src/redux/reducers/search.js
Normal file
103
src/redux/reducers/search.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// @flow
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { handleActions } from 'util/redux-utils';
|
||||||
|
|
||||||
|
type SearchSuccess = {
|
||||||
|
type: ACTIONS.SEARCH_SUCCESS,
|
||||||
|
data: {
|
||||||
|
query: string,
|
||||||
|
uris: Array<string>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateSearchQuery = {
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_QUERY,
|
||||||
|
data: {
|
||||||
|
query: string,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchSuggestion = {
|
||||||
|
value: string,
|
||||||
|
shorthand: string,
|
||||||
|
type: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateSearchSuggestions = {
|
||||||
|
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
|
||||||
|
data: {
|
||||||
|
suggestions: Array<SearchSuggestion>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchState = {
|
||||||
|
isActive: boolean,
|
||||||
|
searchQuery: string,
|
||||||
|
suggestions: Array<SearchSuggestion>,
|
||||||
|
urisByQuery: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
isActive: false,
|
||||||
|
searchQuery: '', // needs to be an empty string for input focusing
|
||||||
|
suggestions: [],
|
||||||
|
urisByQuery: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchReducer = handleActions(
|
||||||
|
{
|
||||||
|
[ACTIONS.SEARCH_START]: (state: SearchState): SearchState => ({
|
||||||
|
...state,
|
||||||
|
searching: true,
|
||||||
|
}),
|
||||||
|
[ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => {
|
||||||
|
const { query, uris } = action.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
searching: false,
|
||||||
|
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[ACTIONS.SEARCH_FAIL]: (state: SearchState): SearchState => ({
|
||||||
|
...state,
|
||||||
|
searching: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.UPDATE_SEARCH_QUERY]: (
|
||||||
|
state: SearchState,
|
||||||
|
action: UpdateSearchQuery
|
||||||
|
): SearchState => ({
|
||||||
|
...state,
|
||||||
|
searchQuery: action.data.query,
|
||||||
|
isActive: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.UPDATE_SEARCH_SUGGESTIONS]: (
|
||||||
|
state: SearchState,
|
||||||
|
action: UpdateSearchSuggestions
|
||||||
|
): SearchState => ({
|
||||||
|
...state,
|
||||||
|
suggestions: action.data.suggestions,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// clear the searchQuery on back/forward
|
||||||
|
// it may be populated by the page title for search/file pages
|
||||||
|
// if going home, it should be blank
|
||||||
|
[ACTIONS.HISTORY_NAVIGATE]: (state: SearchState): SearchState => ({
|
||||||
|
...state,
|
||||||
|
searchQuery: '',
|
||||||
|
suggestions: [],
|
||||||
|
isActive: false,
|
||||||
|
}),
|
||||||
|
// sets isActive to false so the uri will be populated correctly if the
|
||||||
|
// user is on a file page. The search query will still be present on any
|
||||||
|
// other page
|
||||||
|
[ACTIONS.CLOSE_MODAL]: (state: SearchState): SearchState => ({
|
||||||
|
...state,
|
||||||
|
isActive: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
defaultState
|
||||||
|
);
|
146
src/redux/reducers/wallet.js
Normal file
146
src/redux/reducers/wallet.js
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
|
const reducers = {};
|
||||||
|
const buildDraftTransaction = () => ({
|
||||||
|
amount: undefined,
|
||||||
|
address: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
balance: undefined,
|
||||||
|
blocks: {},
|
||||||
|
transactions: {},
|
||||||
|
fetchingTransactions: false,
|
||||||
|
gettingNewAddress: false,
|
||||||
|
draftTransaction: buildDraftTransaction(),
|
||||||
|
sendingSupport: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_TRANSACTIONS_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
fetchingTransactions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_TRANSACTIONS_COMPLETED] = (state, action) => {
|
||||||
|
const byId = Object.assign({}, state.transactions);
|
||||||
|
|
||||||
|
const { transactions } = action.data;
|
||||||
|
|
||||||
|
transactions.forEach(transaction => {
|
||||||
|
byId[transaction.txid] = transaction;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
transactions: byId,
|
||||||
|
fetchingTransactions: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.GET_NEW_ADDRESS_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
gettingNewAddress: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.GET_NEW_ADDRESS_COMPLETED] = (state, action) => {
|
||||||
|
const { address } = action.data;
|
||||||
|
|
||||||
|
// Say no to localStorage!
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
gettingNewAddress: false,
|
||||||
|
receiveAddress: address,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.UPDATE_BALANCE] = (state, action) =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
balance: action.data.balance,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.CHECK_ADDRESS_IS_MINE_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
checkingAddressOwnership: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.CHECK_ADDRESS_IS_MINE_COMPLETED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
checkingAddressOwnership: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.SET_DRAFT_TRANSACTION_AMOUNT] = (state, action) => {
|
||||||
|
const oldDraft = state.draftTransaction;
|
||||||
|
const newDraft = Object.assign({}, oldDraft, {
|
||||||
|
amount: parseFloat(action.data.amount),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraft,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.SET_DRAFT_TRANSACTION_ADDRESS] = (state, action) => {
|
||||||
|
const oldDraft = state.draftTransaction;
|
||||||
|
const newDraft = Object.assign({}, oldDraft, {
|
||||||
|
address: action.data.address,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraft,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.SEND_TRANSACTION_STARTED] = state => {
|
||||||
|
const newDraftTransaction = Object.assign({}, state.draftTransaction, {
|
||||||
|
sending: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraftTransaction,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.SEND_TRANSACTION_COMPLETED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
draftTransaction: buildDraftTransaction(),
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.SEND_TRANSACTION_FAILED] = (state, action) => {
|
||||||
|
const newDraftTransaction = Object.assign({}, state.draftTransaction, {
|
||||||
|
sending: false,
|
||||||
|
error: action.data.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
draftTransaction: newDraftTransaction,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.SUPPORT_TRANSACTION_STARTED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
sendingSupport: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.SUPPORT_TRANSACTION_COMPLETED] = state =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
sendingSupport: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.SUPPORT_TRANSACTION_FAILED] = (state, action) =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
error: action.data.error,
|
||||||
|
sendingSupport: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.FETCH_BLOCK_SUCCESS] = (state, action) => {
|
||||||
|
const { block, block: { height } } = action.data;
|
||||||
|
const blocks = Object.assign({}, state.blocks);
|
||||||
|
|
||||||
|
blocks[height] = block;
|
||||||
|
|
||||||
|
return Object.assign({}, state, { blocks });
|
||||||
|
};
|
||||||
|
|
||||||
|
export function walletReducer(state = defaultState, action) {
|
||||||
|
const handler = reducers[action.type];
|
||||||
|
if (handler) return handler(state, action);
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { normalizeURI } from 'lbryURI';
|
||||||
|
import { makeSelectCurrentParam } from 'redux/selectors/navigation';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
const selectState = state => state.claims || {};
|
const selectState = state => state.claims || {};
|
||||||
|
@ -23,3 +25,185 @@ export const selectClaimsByUri = createSelector(selectState, selectClaimsById, (
|
||||||
|
|
||||||
return claims;
|
return claims;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectAllClaimsByChannel = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.claimsByChannel || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const makeSelectClaimForUri = uri =>
|
||||||
|
createSelector(selectClaimsByUri, claims => claims && claims[normalizeURI(uri)]);
|
||||||
|
|
||||||
|
export const selectMyClaimsRaw = createSelector(selectState, state => state.myClaims);
|
||||||
|
|
||||||
|
export const selectAbandoningIds = createSelector(selectState, state =>
|
||||||
|
Object.keys(state.abandoningById || {})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectMyActiveClaims = createSelector(
|
||||||
|
selectMyClaimsRaw,
|
||||||
|
selectAbandoningIds,
|
||||||
|
(claims, abandoningIds) =>
|
||||||
|
new Set(
|
||||||
|
claims &&
|
||||||
|
claims
|
||||||
|
.map(claim => claim.claim_id)
|
||||||
|
.filter(claimId => Object.keys(abandoningIds).indexOf(claimId) === -1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const makeSelectClaimIsMine = rawUri => {
|
||||||
|
const uri = normalizeURI(rawUri);
|
||||||
|
return createSelector(
|
||||||
|
selectClaimsByUri,
|
||||||
|
selectMyActiveClaims,
|
||||||
|
(claims, myClaims) =>
|
||||||
|
claims && claims[uri] && claims[uri].claim_id && myClaims.has(claims[uri].claim_id)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectAllFetchingChannelClaims = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.fetchingChannelClaims || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const makeSelectFetchingChannelClaims = uri =>
|
||||||
|
createSelector(selectAllFetchingChannelClaims, fetching => fetching && fetching[uri]);
|
||||||
|
|
||||||
|
export const makeSelectClaimsInChannelForCurrentPage = uri => {
|
||||||
|
const pageSelector = makeSelectCurrentParam('page');
|
||||||
|
|
||||||
|
return createSelector(
|
||||||
|
selectClaimsById,
|
||||||
|
selectAllClaimsByChannel,
|
||||||
|
pageSelector,
|
||||||
|
(byId, allClaims, page) => {
|
||||||
|
const byChannel = allClaims[uri] || {};
|
||||||
|
const claimIds = byChannel[page || 1];
|
||||||
|
|
||||||
|
if (!claimIds) return claimIds;
|
||||||
|
|
||||||
|
return claimIds.map(claimId => byId[claimId]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeSelectMetadataForUri = uri =>
|
||||||
|
createSelector(makeSelectClaimForUri(uri), claim => {
|
||||||
|
const metadata = claim && claim.value && claim.value.stream && claim.value.stream.metadata;
|
||||||
|
|
||||||
|
return metadata || (claim === undefined ? undefined : null);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeSelectTitleForUri = uri =>
|
||||||
|
createSelector(makeSelectMetadataForUri(uri), metadata => metadata && metadata.title);
|
||||||
|
|
||||||
|
export const makeSelectContentTypeForUri = uri =>
|
||||||
|
createSelector(makeSelectClaimForUri(uri), claim => {
|
||||||
|
const source = claim && claim.value && claim.value.stream && claim.value.stream.source;
|
||||||
|
return source ? source.contentType : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectIsFetchingClaimListMine = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.isFetchingClaimListMine
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectPendingClaims = createSelector(selectState, state =>
|
||||||
|
Object.values(state.pendingById || {})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectMyClaims = createSelector(
|
||||||
|
selectMyActiveClaims,
|
||||||
|
selectClaimsById,
|
||||||
|
selectAbandoningIds,
|
||||||
|
selectPendingClaims,
|
||||||
|
(myClaimIds, byId, abandoningIds, pendingClaims) => {
|
||||||
|
const claims = [];
|
||||||
|
|
||||||
|
myClaimIds.forEach(id => {
|
||||||
|
const claim = byId[id];
|
||||||
|
|
||||||
|
if (claim && abandoningIds.indexOf(id) === -1) claims.push(claim);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...claims, ...pendingClaims];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectMyClaimsWithoutChannels = createSelector(selectMyClaims, myClaims =>
|
||||||
|
myClaims.filter(claim => !claim.name.match(/^@/))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectAllMyClaimsByOutpoint = createSelector(
|
||||||
|
selectMyClaimsRaw,
|
||||||
|
claims =>
|
||||||
|
new Set(claims && claims.length ? claims.map(claim => `${claim.txid}:${claim.nout}`) : null)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectMyClaimsOutpoints = createSelector(selectMyClaims, myClaims => {
|
||||||
|
const outpoints = [];
|
||||||
|
|
||||||
|
myClaims.forEach(claim => outpoints.push(`${claim.txid}:${claim.nout}`));
|
||||||
|
|
||||||
|
return outpoints;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectFetchingMyChannels = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.fetchingMyChannels
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectMyChannelClaims = createSelector(
|
||||||
|
selectState,
|
||||||
|
selectClaimsById,
|
||||||
|
(state, byId) => {
|
||||||
|
const ids = state.myChannelClaims || [];
|
||||||
|
const claims = [];
|
||||||
|
|
||||||
|
ids.forEach(id => {
|
||||||
|
if (byId[id]) {
|
||||||
|
// I'm not sure why this check is necessary, but it ought to be a quick fix for https://github.com/lbryio/lbry-app/issues/544
|
||||||
|
claims.push(byId[id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectResolvingUris = createSelector(selectState, state => state.resolvingUris || []);
|
||||||
|
|
||||||
|
export const makeSelectIsUriResolving = uri =>
|
||||||
|
createSelector(
|
||||||
|
selectResolvingUris,
|
||||||
|
resolvingUris => resolvingUris && resolvingUris.indexOf(uri) !== -1
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectFeaturedUris = createSelector(selectState, state => state.featuredUris);
|
||||||
|
|
||||||
|
export const selectFetchingFeaturedUris = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.fetchingFeaturedContent
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
||||||
|
|
||||||
|
export const selectChannelClaimCounts = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.channelClaimCounts || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const makeSelectTotalItemsForChannel = uri =>
|
||||||
|
createSelector(selectChannelClaimCounts, byUri => byUri && byUri[uri]);
|
||||||
|
|
||||||
|
export const makeSelectTotalPagesForChannel = uri =>
|
||||||
|
createSelector(
|
||||||
|
selectChannelClaimCounts,
|
||||||
|
byUri => byUri && byUri[uri] && Math.ceil(byUri[uri] / 10)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectRewardContentClaimIds = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.rewardedContentClaimIds
|
||||||
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
//import { selectCurrentParams } from 'redux/selectors/navigation';
|
import { selectCurrentParams } from 'redux/selectors/navigation';
|
||||||
|
|
||||||
export const selectState = state => state.costInfo || {};
|
export const selectState = state => state.costInfo || {};
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export const makeSelectCostInfoForUri = uri =>
|
||||||
|
|
||||||
export const selectCostForCurrentPageUri = createSelector(
|
export const selectCostForCurrentPageUri = createSelector(
|
||||||
selectAllCostInfoByUri,
|
selectAllCostInfoByUri,
|
||||||
{}/*selectCurrentParams*/,
|
selectCurrentParams,
|
||||||
(costInfo, params) => (params.uri && costInfo[params.uri] ? costInfo[params.uri].cost : undefined)
|
(costInfo, params) => (params.uri && costInfo[params.uri] ? costInfo[params.uri].cost : undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
197
src/redux/selectors/file_info.js
Normal file
197
src/redux/selectors/file_info.js
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import {
|
||||||
|
selectClaimsByUri,
|
||||||
|
selectIsFetchingClaimListMine,
|
||||||
|
selectMyClaims,
|
||||||
|
selectClaimsById,
|
||||||
|
} from 'redux/selectors/claims';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { buildURI } from 'lbryURI';
|
||||||
|
|
||||||
|
export const selectState = state => state.fileInfo || {};
|
||||||
|
|
||||||
|
export const selectFileInfosByOutpoint = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.byOutpoint || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectIsFetchingFileList = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.isFetchingFileList
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
|
||||||
|
selectIsFetchingFileList,
|
||||||
|
selectIsFetchingClaimListMine,
|
||||||
|
(isFetchingFileList, isFetchingClaimListMine) => isFetchingFileList || isFetchingClaimListMine
|
||||||
|
);
|
||||||
|
|
||||||
|
export const makeSelectFileInfoForUri = uri =>
|
||||||
|
createSelector(selectClaimsByUri, selectFileInfosByOutpoint, (claims, byOutpoint) => {
|
||||||
|
const claim = claims[uri];
|
||||||
|
const outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined;
|
||||||
|
return outpoint ? byOutpoint[outpoint] : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectDownloadingByOutpoint = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.downloadingByOutpoint || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const makeSelectDownloadingForUri = uri =>
|
||||||
|
createSelector(
|
||||||
|
selectDownloadingByOutpoint,
|
||||||
|
makeSelectFileInfoForUri(uri),
|
||||||
|
(byOutpoint, fileInfo) => {
|
||||||
|
if (!fileInfo) return false;
|
||||||
|
return byOutpoint[fileInfo.outpoint];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectUrisLoading = createSelector(selectState, state => state.urisLoading || {});
|
||||||
|
|
||||||
|
export const makeSelectLoadingForUri = uri =>
|
||||||
|
createSelector(selectUrisLoading, byUri => byUri && byUri[uri]);
|
||||||
|
|
||||||
|
export const selectFileInfosDownloaded = createSelector(
|
||||||
|
selectFileInfosByOutpoint,
|
||||||
|
selectMyClaims,
|
||||||
|
(byOutpoint, myClaims) =>
|
||||||
|
Object.values(byOutpoint).filter(fileInfo => {
|
||||||
|
const myClaimIds = myClaims.map(claim => claim.claim_id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
fileInfo &&
|
||||||
|
myClaimIds.indexOf(fileInfo.claim_id) === -1 &&
|
||||||
|
(fileInfo.completed || fileInfo.written_bytes)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// export const selectFileInfoForUri = (state, props) => {
|
||||||
|
// const claims = selectClaimsByUri(state),
|
||||||
|
// claim = claims[props.uri],
|
||||||
|
// fileInfos = selectAllFileInfos(state),
|
||||||
|
// outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined;
|
||||||
|
|
||||||
|
// return outpoint && fileInfos ? fileInfos[outpoint] : undefined;
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const selectDownloadingFileInfos = createSelector(
|
||||||
|
selectDownloadingByOutpoint,
|
||||||
|
selectFileInfosByOutpoint,
|
||||||
|
(downloadingByOutpoint, fileInfosByOutpoint) => {
|
||||||
|
const outpoints = Object.keys(downloadingByOutpoint);
|
||||||
|
const fileInfos = [];
|
||||||
|
|
||||||
|
outpoints.forEach(outpoint => {
|
||||||
|
const fileInfo = fileInfosByOutpoint[outpoint];
|
||||||
|
|
||||||
|
if (fileInfo) fileInfos.push(fileInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fileInfos;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectTotalDownloadProgress = createSelector(selectDownloadingFileInfos, fileInfos => {
|
||||||
|
const progress = [];
|
||||||
|
|
||||||
|
fileInfos.forEach(fileInfo => {
|
||||||
|
progress.push(fileInfo.written_bytes / fileInfo.total_bytes * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalProgress = progress.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
if (fileInfos.length > 0) return totalProgress / fileInfos.length / 100.0;
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectSearchDownloadUris = query =>
|
||||||
|
createSelector(selectFileInfosDownloaded, selectClaimsById, (fileInfos, claimsById) => {
|
||||||
|
if (!query || !fileInfos.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParts = query.toLowerCase().split(' ');
|
||||||
|
const searchQueryDictionary = {};
|
||||||
|
queryParts.forEach(subQuery => {
|
||||||
|
searchQueryDictionary[subQuery] = subQuery;
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrayContainsQueryPart = array => {
|
||||||
|
for (let i = 0; i < array.length; i += 1) {
|
||||||
|
const subQuery = array[i];
|
||||||
|
if (searchQueryDictionary[subQuery]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadResultsFromQuery = [];
|
||||||
|
fileInfos.forEach(fileInfo => {
|
||||||
|
const { channel_name: channelName, claim_name: claimName, metadata } = fileInfo;
|
||||||
|
const { author, description, title } = metadata;
|
||||||
|
|
||||||
|
if (channelName) {
|
||||||
|
const lowerCaseChannel = channelName.toLowerCase();
|
||||||
|
const strippedOutChannelName = lowerCaseChannel.slice(1); // trim off the @
|
||||||
|
if (searchQueryDictionary[channelName] || searchQueryDictionary[strippedOutChannelName]) {
|
||||||
|
downloadResultsFromQuery.push(fileInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameParts = claimName.toLowerCase().split('-');
|
||||||
|
if (arrayContainsQueryPart(nameParts)) {
|
||||||
|
downloadResultsFromQuery.push(fileInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleParts = title.toLowerCase().split(' ');
|
||||||
|
if (arrayContainsQueryPart(titleParts)) {
|
||||||
|
downloadResultsFromQuery.push(fileInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (author) {
|
||||||
|
const authorParts = author.toLowerCase().split(' ');
|
||||||
|
if (arrayContainsQueryPart(authorParts)) {
|
||||||
|
downloadResultsFromQuery.push(fileInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
const descriptionParts = description.toLowerCase().split(' ');
|
||||||
|
if (arrayContainsQueryPart(descriptionParts)) {
|
||||||
|
downloadResultsFromQuery.push(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return downloadResultsFromQuery.length
|
||||||
|
? downloadResultsFromQuery.map(fileInfo => {
|
||||||
|
const { channel_name: channelName, claim_id: claimId, claim_name: claimName } = fileInfo;
|
||||||
|
|
||||||
|
const uriParams = {};
|
||||||
|
|
||||||
|
if (channelName) {
|
||||||
|
const claim = claimsById[claimId];
|
||||||
|
if (claim.value) {
|
||||||
|
uriParams.claimId = claim.value.publisherSignature.certificateId;
|
||||||
|
} else {
|
||||||
|
uriParams.claimId = claimId;
|
||||||
|
}
|
||||||
|
uriParams.channelName = channelName;
|
||||||
|
uriParams.contentName = claimName;
|
||||||
|
} else {
|
||||||
|
uriParams.claimId = claimId;
|
||||||
|
uriParams.claimName = claimName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = buildURI(uriParams);
|
||||||
|
return uri;
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
});
|
202
src/redux/selectors/navigation.js
Normal file
202
src/redux/selectors/navigation.js
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { parseQueryParams } from 'util/query_params';
|
||||||
|
|
||||||
|
export const selectState = state => state.navigation || {};
|
||||||
|
|
||||||
|
export const selectCurrentPath = createSelector(selectState, state => state.currentPath);
|
||||||
|
|
||||||
|
export const computePageFromPath = path => path.replace(/^\//, '').split('?')[0];
|
||||||
|
|
||||||
|
export const selectCurrentPage = createSelector(selectCurrentPath, path =>
|
||||||
|
computePageFromPath(path)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectCurrentParams = createSelector(selectCurrentPath, path => {
|
||||||
|
if (path === undefined) return {};
|
||||||
|
if (!path.match(/\?/)) return {};
|
||||||
|
|
||||||
|
return parseQueryParams(path.split('?')[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeSelectCurrentParam = param =>
|
||||||
|
createSelector(selectCurrentParams, params => (params ? params[param] : undefined));
|
||||||
|
|
||||||
|
export const selectPathAfterAuth = createSelector(selectState, state => state.pathAfterAuth);
|
||||||
|
|
||||||
|
export const selectIsBackDisabled = createSelector(selectState, state => state.index === 0);
|
||||||
|
|
||||||
|
export const selectIsForwardDisabled = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.index === state.stack.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectIsHome = createSelector(selectCurrentPage, page => page === 'discover');
|
||||||
|
|
||||||
|
export const selectHistoryIndex = createSelector(selectState, state => state.index);
|
||||||
|
|
||||||
|
export const selectHistoryStack = createSelector(selectState, state => state.stack);
|
||||||
|
|
||||||
|
// returns current page attributes (scrollY, path)
|
||||||
|
export const selectActiveHistoryEntry = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.stack[state.index]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectPageTitle = createSelector(selectCurrentPage, page => {
|
||||||
|
switch (page) {
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectNavLinks = createSelector(
|
||||||
|
selectCurrentPage,
|
||||||
|
selectHistoryStack,
|
||||||
|
(currentPage, historyStack) => {
|
||||||
|
const isWalletPage = page =>
|
||||||
|
page === 'wallet' ||
|
||||||
|
page === 'send' ||
|
||||||
|
page === 'getcredits' ||
|
||||||
|
page === 'rewards' ||
|
||||||
|
page === 'history' ||
|
||||||
|
page === 'invite';
|
||||||
|
|
||||||
|
const isMyLbryPage = page =>
|
||||||
|
page === 'downloaded' || page === 'published' || page === 'settings';
|
||||||
|
|
||||||
|
const previousStack = historyStack.slice().reverse();
|
||||||
|
|
||||||
|
const getPreviousSubLinkPath = checkIfValidPage => {
|
||||||
|
for (let i = 0; i < previousStack.length; i += 1) {
|
||||||
|
const currentStackItem = previousStack[i];
|
||||||
|
|
||||||
|
// Trim off the "/" from the path
|
||||||
|
const pageInStack = currentStackItem.path.slice(1);
|
||||||
|
if (checkIfValidPage(pageInStack)) {
|
||||||
|
return currentStackItem.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gets the last active sublink in a section
|
||||||
|
const getActiveSublink = category => {
|
||||||
|
if (category === 'wallet') {
|
||||||
|
const previousPath = getPreviousSubLinkPath(isWalletPage);
|
||||||
|
return previousPath || '/wallet';
|
||||||
|
} else if (category === 'myLbry') {
|
||||||
|
const previousPath = getPreviousSubLinkPath(isMyLbryPage);
|
||||||
|
return previousPath || '/downloaded';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentlyWalletPage = isWalletPage(currentPage);
|
||||||
|
const isCurrentlyMyLbryPage = isMyLbryPage(currentPage);
|
||||||
|
|
||||||
|
const walletSubLinks = [
|
||||||
|
{
|
||||||
|
label: 'Overview',
|
||||||
|
path: '/wallet',
|
||||||
|
active: currentPage === 'wallet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Send & Recieve',
|
||||||
|
path: '/send',
|
||||||
|
active: currentPage === 'send',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Get Credits',
|
||||||
|
path: '/getcredits',
|
||||||
|
active: currentPage === 'getcredits',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rewards',
|
||||||
|
path: '/rewards',
|
||||||
|
active: currentPage === 'rewards',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Invites',
|
||||||
|
path: '/invite',
|
||||||
|
active: currentPage === 'invite',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Transactions',
|
||||||
|
path: '/history',
|
||||||
|
active: currentPage === 'history',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const myLbrySubLinks = [
|
||||||
|
{
|
||||||
|
label: 'Downloads',
|
||||||
|
path: '/downloaded',
|
||||||
|
active: currentPage === 'downloaded',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Publishes',
|
||||||
|
path: '/published',
|
||||||
|
active: currentPage === 'published',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
path: '/settings',
|
||||||
|
active: currentPage === 'settings',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Backup',
|
||||||
|
path: '/backup',
|
||||||
|
active: currentPage === 'backup',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const navLinks = {
|
||||||
|
primary: [
|
||||||
|
{
|
||||||
|
label: 'Explore',
|
||||||
|
path: '/discover',
|
||||||
|
active: currentPage === 'discover',
|
||||||
|
icon: 'Compass',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscriptions',
|
||||||
|
path: '/subscriptions',
|
||||||
|
active: currentPage === 'subscriptions',
|
||||||
|
icon: 'AtSign',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
label: 'Wallet',
|
||||||
|
icon: 'CreditCard',
|
||||||
|
subLinks: walletSubLinks,
|
||||||
|
path: isCurrentlyWalletPage ? '/wallet' : getActiveSublink('wallet'),
|
||||||
|
active: isWalletPage(currentPage),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'My LBRY',
|
||||||
|
icon: 'Settings',
|
||||||
|
subLinks: myLbrySubLinks,
|
||||||
|
path: isCurrentlyMyLbryPage ? '/downloaded' : getActiveSublink('myLbry'),
|
||||||
|
active: isMyLbryPage(currentPage),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Publish',
|
||||||
|
icon: 'UploadCloud',
|
||||||
|
path: '/publish',
|
||||||
|
active: currentPage === 'publish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Help',
|
||||||
|
path: '/help',
|
||||||
|
active: currentPage === 'help',
|
||||||
|
icon: 'HelpCircle',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return navLinks;
|
||||||
|
}
|
||||||
|
);
|
8
src/redux/selectors/notifications.js
Normal file
8
src/redux/selectors/notifications.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
export const selectState = state => state.notifications || {};
|
||||||
|
|
||||||
|
export const selectNotification = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => (state.queue.length > 0 ? state.queue[0] : {})
|
||||||
|
);
|
37
src/redux/selectors/search.js
Normal file
37
src/redux/selectors/search.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { selectCurrentPage, selectCurrentParams } from 'redux/selectors/navigation';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
export const selectState = state => state.search || {};
|
||||||
|
|
||||||
|
export const selectSearchValue = createSelector(selectState, state => state.searchQuery);
|
||||||
|
|
||||||
|
export const selectSearchQuery = createSelector(
|
||||||
|
selectCurrentPage,
|
||||||
|
selectCurrentParams,
|
||||||
|
(page, params) => (page === 'search' ? params && params.query : null)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectIsSearching = createSelector(selectState, state => state.searching);
|
||||||
|
|
||||||
|
export const selectSearchUrisByQuery = createSelector(selectState, state => state.urisByQuery);
|
||||||
|
|
||||||
|
export const makeSelectSearchUris = query =>
|
||||||
|
// replace statement below is kind of ugly, and repeated in doSearch action
|
||||||
|
createSelector(
|
||||||
|
selectSearchUrisByQuery,
|
||||||
|
byQuery => byQuery[query ? query.replace(/^lbry:\/\//i, '') : query]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectWunderBarAddress = createSelector(
|
||||||
|
selectCurrentPage,
|
||||||
|
selectSearchQuery,
|
||||||
|
selectCurrentParams,
|
||||||
|
(page, query, params) => {
|
||||||
|
// only populate the wunderbar address if we are on the file/channel pages
|
||||||
|
// or show the search query
|
||||||
|
if (page === 'show') {
|
||||||
|
return params.uri;
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
);
|
125
src/redux/selectors/wallet.js
Normal file
125
src/redux/selectors/wallet.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
export const selectState = state => state.wallet || {};
|
||||||
|
|
||||||
|
export const selectBalance = createSelector(selectState, state => state.balance);
|
||||||
|
|
||||||
|
export const selectTransactionsById = createSelector(selectState, state => state.transactions);
|
||||||
|
|
||||||
|
export const selectTransactionItems = createSelector(selectTransactionsById, byId => {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
Object.keys(byId).forEach(txid => {
|
||||||
|
const tx = byId[txid];
|
||||||
|
|
||||||
|
// ignore dust/fees
|
||||||
|
// it is fee only txn if all infos are also empty
|
||||||
|
if (
|
||||||
|
Math.abs(tx.value) === Math.abs(tx.fee) &&
|
||||||
|
tx.claim_info.length === 0 &&
|
||||||
|
tx.support_info.length === 0 &&
|
||||||
|
tx.update_info.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const append = [];
|
||||||
|
|
||||||
|
append.push(
|
||||||
|
...tx.claim_info.map(item =>
|
||||||
|
Object.assign({}, tx, item, {
|
||||||
|
type: item.claim_name[0] === '@' ? 'channel' : 'publish',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
append.push(
|
||||||
|
...tx.support_info.map(item =>
|
||||||
|
Object.assign({}, tx, item, {
|
||||||
|
type: !item.is_tip ? 'support' : 'tip',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
append.push(...tx.update_info.map(item => Object.assign({}, tx, item, { type: 'update' })));
|
||||||
|
|
||||||
|
if (!append.length) {
|
||||||
|
append.push(
|
||||||
|
Object.assign({}, tx, {
|
||||||
|
type: tx.value < 0 ? 'spend' : 'receive',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
...append.map(item => {
|
||||||
|
// value on transaction, amount on outpoint
|
||||||
|
// amount is always positive, but should match sign of value
|
||||||
|
const amount = parseFloat(item.balance_delta ? item.balance_delta : item.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
txid,
|
||||||
|
date: tx.timestamp ? new Date(Number(tx.timestamp) * 1000) : null,
|
||||||
|
amount,
|
||||||
|
fee: amount < 0 ? -1 * tx.fee / append.length : 0,
|
||||||
|
claim_id: item.claim_id,
|
||||||
|
claim_name: item.claim_name,
|
||||||
|
type: item.type || 'send',
|
||||||
|
nout: item.nout,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return items.reverse();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectRecentTransactions = createSelector(selectTransactionItems, transactions => {
|
||||||
|
const threshold = new Date();
|
||||||
|
threshold.setDate(threshold.getDate() - 7);
|
||||||
|
return transactions.filter(transaction => transaction.date > threshold);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectHasTransactions = createSelector(
|
||||||
|
selectTransactionItems,
|
||||||
|
transactions => transactions && transactions.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectIsFetchingTransactions = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.fetchingTransactions
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectIsSendingSupport = createSelector(selectState, state => state.sendingSupport);
|
||||||
|
|
||||||
|
export const selectReceiveAddress = createSelector(selectState, state => state.receiveAddress);
|
||||||
|
|
||||||
|
export const selectGettingNewAddress = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.gettingNewAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectDraftTransaction = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.draftTransaction || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectDraftTransactionAmount = createSelector(
|
||||||
|
selectDraftTransaction,
|
||||||
|
draft => draft.amount
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectDraftTransactionAddress = createSelector(
|
||||||
|
selectDraftTransaction,
|
||||||
|
draft => draft.address
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectDraftTransactionError = createSelector(
|
||||||
|
selectDraftTransaction,
|
||||||
|
draft => draft.error
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectBlocks = createSelector(selectState, state => state.blocks);
|
||||||
|
|
||||||
|
export const makeSelectBlockDate = block =>
|
||||||
|
createSelector(
|
||||||
|
selectBlocks,
|
||||||
|
blocks => (blocks && blocks[block] ? new Date(blocks[block].time * 1000) : undefined)
|
||||||
|
);
|
12
src/types/Notification.js
Normal file
12
src/types/Notification.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// @flow
|
||||||
|
export type Notification = {
|
||||||
|
title: ?string,
|
||||||
|
message: string,
|
||||||
|
type: string,
|
||||||
|
errorCode: ?number,
|
||||||
|
displayType: mixed,
|
||||||
|
|
||||||
|
// additional properties for SnackBar
|
||||||
|
linkText: ?string,
|
||||||
|
linkTarget: ?string,
|
||||||
|
};
|
7
src/util/batchActions.js
Normal file
7
src/util/batchActions.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// https://github.com/reactjs/redux/issues/911
|
||||||
|
export function batchActions(...actions) {
|
||||||
|
return {
|
||||||
|
type: 'BATCH_ACTIONS',
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
}
|
21
src/util/formatCredits.js
Normal file
21
src/util/formatCredits.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export function formatCredits(amount, precision) {
|
||||||
|
return amount.toFixed(precision || 1).replace(/\.?0+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFullPrice(amount, precision = 1) {
|
||||||
|
let formated = '';
|
||||||
|
|
||||||
|
const quantity = amount.toString().split('.');
|
||||||
|
const fraction = quantity[1];
|
||||||
|
|
||||||
|
if (fraction) {
|
||||||
|
const decimals = fraction.split('');
|
||||||
|
const first = decimals.filter(number => number !== '0')[0];
|
||||||
|
const index = decimals.indexOf(first);
|
||||||
|
|
||||||
|
// Set format fraction
|
||||||
|
formated = `.${fraction.substring(0, index + precision)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFloat(quantity[0] + formated);
|
||||||
|
}
|
5
src/util/handle-fetch.js
Normal file
5
src/util/handle-fetch.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export default function handleFetchResponse(response) {
|
||||||
|
return response.status === 200
|
||||||
|
? Promise.resolve(response.json())
|
||||||
|
: Promise.reject(new Error(response.statusText));
|
||||||
|
}
|
28
src/util/query_params.js
Normal file
28
src/util/query_params.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
export function parseQueryParams(queryString) {
|
||||||
|
if (queryString === '') return {};
|
||||||
|
const parts = queryString
|
||||||
|
.split('?')
|
||||||
|
.pop()
|
||||||
|
.split('&')
|
||||||
|
.map(p => p.split('='));
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
parts.forEach(array => {
|
||||||
|
const [first, second] = array;
|
||||||
|
params[first] = second;
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toQueryString(params) {
|
||||||
|
if (!params) return '';
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
Object.keys(params).forEach(key => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(params, key) && params[key]) {
|
||||||
|
parts.push(`${key}=${params[key]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return parts.join('&');
|
||||||
|
}
|
17
src/util/redux-utils.js
Normal file
17
src/util/redux-utils.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// util for creating reducers
|
||||||
|
// based off of redux-actions
|
||||||
|
// https://redux-actions.js.org/docs/api/handleAction.html#handleactions
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const handleActions = (actionMap, defaultState) => (state = defaultState, action) => {
|
||||||
|
const handler = actionMap[action.type];
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
const newState = handler(state, action);
|
||||||
|
return Object.assign({}, state, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// just return the original state if no handler
|
||||||
|
// returning a copy here breaks redux-persist
|
||||||
|
return state;
|
||||||
|
};
|
|
@ -4,7 +4,7 @@ module.exports = {
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'build'),
|
path: path.resolve(__dirname, 'build'),
|
||||||
filename: 'index.js',
|
filename: 'index.js',
|
||||||
libraryTarget: 'commonjs2'
|
libraryTarget: 'umd'
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|
Loading…
Reference in a new issue