diff --git a/dist/bundle.es.js b/dist/bundle.es.js index 9512952..4bbe81d 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -116,6 +116,12 @@ const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED'; const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'; const FILE_DELETE = 'FILE_DELETE'; const SET_FILE_LIST_SORT = 'SET_FILE_LIST_SORT'; +const PURCHASE_URI_STARTED = 'PURCHASE_URI_STARTED'; +const PURCHASE_URI_COMPLETED = 'PURCHASE_URI_COMPLETED'; +const PURCHASE_URI_FAILED = 'PURCHASE_URI_FAILED'; +const LOADING_FILE_STARTED = 'LOADING_FILE_STARTED'; +const LOADING_FILE_COMPLETED = 'LOADING_FILE_COMPLETED'; +const LOADING_FILE_FAILED = 'LOADING_FILE_FAILED'; // Search const SEARCH_START = 'SEARCH_START'; @@ -228,6 +234,11 @@ const DISMISS_ERROR = 'DISMISS_ERROR'; const FETCH_DATE = 'FETCH_DATE'; +// Cost info +const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED'; +const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED'; +const FETCH_COST_INFO_FAILED = 'FETCH_COST_INFO_FAILED'; + var action_types = /*#__PURE__*/Object.freeze({ WINDOW_FOCUSED: WINDOW_FOCUSED, DAEMON_READY: DAEMON_READY, @@ -327,6 +338,12 @@ var action_types = /*#__PURE__*/Object.freeze({ FETCH_AVAILABILITY_COMPLETED: FETCH_AVAILABILITY_COMPLETED, FILE_DELETE: FILE_DELETE, SET_FILE_LIST_SORT: SET_FILE_LIST_SORT, + PURCHASE_URI_STARTED: PURCHASE_URI_STARTED, + PURCHASE_URI_COMPLETED: PURCHASE_URI_COMPLETED, + PURCHASE_URI_FAILED: PURCHASE_URI_FAILED, + LOADING_FILE_STARTED: LOADING_FILE_STARTED, + LOADING_FILE_COMPLETED: LOADING_FILE_COMPLETED, + LOADING_FILE_FAILED: LOADING_FILE_FAILED, SEARCH_START: SEARCH_START, SEARCH_SUCCESS: SEARCH_SUCCESS, SEARCH_FAIL: SEARCH_FAIL, @@ -418,7 +435,10 @@ var action_types = /*#__PURE__*/Object.freeze({ DISMISS_TOAST: DISMISS_TOAST, CREATE_ERROR: CREATE_ERROR, DISMISS_ERROR: DISMISS_ERROR, - FETCH_DATE: FETCH_DATE + FETCH_DATE: FETCH_DATE, + FETCH_COST_INFO_STARTED: FETCH_COST_INFO_STARTED, + FETCH_COST_INFO_COMPLETED: FETCH_COST_INFO_COMPLETED, + FETCH_COST_INFO_FAILED: FETCH_COST_INFO_FAILED }); const API_DOWN = 'apiDown'; @@ -2295,6 +2315,108 @@ const selectFileListPublishedSort = reselect.createSelector(selectState$3, state const selectFileListDownloadedSort = reselect.createSelector(selectState$3, state => state.fileListDownloadedSort); +// + +const selectState$4 = state => state.file || {}; + +const selectFailedPurchaseUris = reselect.createSelector(selectState$4, state => state.failedPurchaseUris); + +const selectPurchasedUris = reselect.createSelector(selectState$4, state => state.purchasedUris); + +const selectPurchasedStreamingUrls = reselect.createSelector(selectState$4, state => state.purchasedStreamingUrls); + +const selectLastPurchasedUri = reselect.createSelector(selectState$4, state => state.purchasedUris.length > 0 ? state.purchasedUris[state.purchasedUris.length - 1] : null); + +const makeSelectStreamingUrlForUri = uri => reselect.createSelector(selectPurchasedStreamingUrls, streamingUrls => streamingUrls && streamingUrls[uri]); + +// + +function doFileGet(uri, saveFile = true) { + return dispatch => { + dispatch({ + type: LOADING_FILE_STARTED, + data: { + uri + } + }); + + // set save_file argument to True to save the file (old behaviour) + lbryProxy.get({ uri, save_file: saveFile }).then(streamInfo => { + const timeout = streamInfo === null || typeof streamInfo !== 'object'; + + if (timeout) { + dispatch({ + type: LOADING_FILE_FAILED, + data: { uri } + }); + dispatch({ + type: PURCHASE_URI_FAILED, + data: { uri } + }); + + dispatch(doToast({ message: `File timeout for uri ${uri}`, isError: true })); + } else { + // purchase was completed successfully + const { streaming_url: streamingUrl } = streamInfo; + dispatch({ + type: PURCHASE_URI_COMPLETED, + data: { uri, streamingUrl: !saveFile && streamingUrl ? streamingUrl : null } + }); + } + }).catch(() => { + dispatch({ + type: LOADING_FILE_FAILED, + data: { uri } + }); + dispatch({ + type: PURCHASE_URI_FAILED, + data: { uri } + }); + + dispatch(doToast({ + message: `Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.com/faq/support for support.`, + isError: true + })); + }); + }; +} + +function doPurchaseUri(uri, costInfo, saveFile = true) { + return (dispatch, getState) => { + dispatch({ + type: PURCHASE_URI_STARTED, + data: { uri } + }); + + const state = getState(); + const balance = selectBalance(state); + const fileInfo = makeSelectFileInfoForUri(uri)(state); + const downloadingByOutpoint = selectDownloadingByOutpoint(state); + const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; + const alreadyStreaming = makeSelectStreamingUrlForUri(uri)(state); + + if (alreadyDownloading || alreadyStreaming) { + dispatch({ + type: PURCHASE_URI_FAILED, + data: { uri, error: `Already fetching uri: ${uri}` } + }); + return; + } + + const { cost } = costInfo; + + if (cost > balance) { + dispatch({ + type: PURCHASE_URI_FAILED, + data: { uri, error: 'Insufficient credits' } + }); + return; + } + + dispatch(doFileGet(uri, saveFile)); + }; +} + function doFetchFileInfo(uri) { return (dispatch, getState) => { const state = getState(); @@ -2790,15 +2912,65 @@ var _extends$4 = Object.assign || function (target) { for (var i = 1; i < argume const reducers$1 = {}; const defaultState$1 = { + failedPurchaseUris: [], + purchasedUris: [], + purchasedStreamingUrls: {} +}; + +reducers$1[PURCHASE_URI_COMPLETED] = (state, action) => { + const { uri, streamingUrl } = action.data; + const newPurchasedUris = state.purchasedUris.slice(); + const newFailedPurchaseUris = state.failedPurchaseUris.slice(); + const newPurchasedStreamingUrls = Object.assign({}, state.purchasedStreamingUrls); + + if (!newPurchasedUris.includes(uri)) { + newPurchasedUris.push(uri); + } + if (newFailedPurchaseUris.includes(uri)) { + newFailedPurchaseUris.splice(newFailedPurchaseUris.indexOf(uri), 1); + } + if (streamingUrl) { + newPurchasedStreamingUrls[uri] = streamingUrl; + } + + return _extends$4({}, state, { + failedPurchaseUris: newFailedPurchaseUris, + purchasedUris: newPurchasedUris, + purchasedStreamingUrls: newPurchasedStreamingUrls + }); +}; + +reducers$1[PURCHASE_URI_FAILED] = (state, action) => { + const { uri } = action.data; + const newFailedPurchaseUris = state.failedPurchaseUris.slice(); + if (!newFailedPurchaseUris.includes(uri)) { + newFailedPurchaseUris.push(uri); + } + + return _extends$4({}, state, { + failedPurchaseUris: newFailedPurchaseUris + }); +}; + +function fileReducer(state = defaultState$1, action) { + const handler = reducers$1[action.type]; + if (handler) return handler(state, action); + return state; +} + +var _extends$5 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +const reducers$2 = {}; +const defaultState$2 = { fileListPublishedSort: DATE_NEW, fileListDownloadedSort: DATE_NEW }; -reducers$1[FILE_LIST_STARTED] = state => Object.assign({}, state, { +reducers$2[FILE_LIST_STARTED] = state => Object.assign({}, state, { isFetchingFileList: true }); -reducers$1[FILE_LIST_SUCCEEDED] = (state, action) => { +reducers$2[FILE_LIST_SUCCEEDED] = (state, action) => { const { fileInfos } = action.data; const newByOutpoint = Object.assign({}, state.byOutpoint); const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint); @@ -2816,7 +2988,7 @@ reducers$1[FILE_LIST_SUCCEEDED] = (state, action) => { }); }; -reducers$1[FETCH_FILE_INFO_STARTED] = (state, action) => { +reducers$2[FETCH_FILE_INFO_STARTED] = (state, action) => { const { outpoint } = action.data; const newFetching = Object.assign({}, state.fetching); @@ -2827,7 +2999,7 @@ reducers$1[FETCH_FILE_INFO_STARTED] = (state, action) => { }); }; -reducers$1[FETCH_FILE_INFO_COMPLETED] = (state, action) => { +reducers$2[FETCH_FILE_INFO_COMPLETED] = (state, action) => { const { fileInfo, outpoint } = action.data; const newByOutpoint = Object.assign({}, state.byOutpoint); @@ -2842,7 +3014,7 @@ reducers$1[FETCH_FILE_INFO_COMPLETED] = (state, action) => { }); }; -reducers$1[DOWNLOADING_STARTED] = (state, action) => { +reducers$2[DOWNLOADING_STARTED] = (state, action) => { const { uri, outpoint, fileInfo } = action.data; const newByOutpoint = Object.assign({}, state.byOutpoint); @@ -2860,7 +3032,7 @@ reducers$1[DOWNLOADING_STARTED] = (state, action) => { }); }; -reducers$1[DOWNLOADING_PROGRESSED] = (state, action) => { +reducers$2[DOWNLOADING_PROGRESSED] = (state, action) => { const { outpoint, fileInfo } = action.data; const newByOutpoint = Object.assign({}, state.byOutpoint); @@ -2875,7 +3047,7 @@ reducers$1[DOWNLOADING_PROGRESSED] = (state, action) => { }); }; -reducers$1[DOWNLOADING_CANCELED] = (state, action) => { +reducers$2[DOWNLOADING_CANCELED] = (state, action) => { const { outpoint } = action.data; const newDownloading = Object.assign({}, state.downloadingByOutpoint); @@ -2886,7 +3058,7 @@ reducers$1[DOWNLOADING_CANCELED] = (state, action) => { }); }; -reducers$1[DOWNLOADING_COMPLETED] = (state, action) => { +reducers$2[DOWNLOADING_COMPLETED] = (state, action) => { const { outpoint, fileInfo } = action.data; const newByOutpoint = Object.assign({}, state.byOutpoint); @@ -2901,7 +3073,7 @@ reducers$1[DOWNLOADING_COMPLETED] = (state, action) => { }); }; -reducers$1[FILE_DELETE] = (state, action) => { +reducers$2[FILE_DELETE] = (state, action) => { const { outpoint } = action.data; const newByOutpoint = Object.assign({}, state.byOutpoint); @@ -2916,37 +3088,37 @@ reducers$1[FILE_DELETE] = (state, action) => { }); }; -reducers$1[LOADING_VIDEO_STARTED] = (state, action) => { +reducers$2[LOADING_VIDEO_STARTED] = (state, action) => { const { uri } = action.data; const newLoading = Object.assign({}, state.urisLoading); newLoading[uri] = true; - const newErrors = _extends$4({}, state.errors); + const newErrors = _extends$5({}, state.errors); if (uri in newErrors) delete newErrors[uri]; return Object.assign({}, state, { urisLoading: newLoading, - errors: _extends$4({}, newErrors) + errors: _extends$5({}, newErrors) }); }; -reducers$1[LOADING_VIDEO_FAILED] = (state, action) => { +reducers$2[LOADING_VIDEO_FAILED] = (state, action) => { const { uri } = action.data; const newLoading = Object.assign({}, state.urisLoading); delete newLoading[uri]; - const newErrors = _extends$4({}, state.errors); + const newErrors = _extends$5({}, state.errors); newErrors[uri] = true; return Object.assign({}, state, { urisLoading: newLoading, - errors: _extends$4({}, newErrors) + errors: _extends$5({}, newErrors) }); }; -reducers$1[FETCH_DATE] = (state, action) => { +reducers$2[FETCH_DATE] = (state, action) => { const { time } = action.data; if (time) { return Object.assign({}, state, { @@ -2956,7 +3128,7 @@ reducers$1[FETCH_DATE] = (state, action) => { return null; }; -reducers$1[SET_FILE_LIST_SORT] = (state, action) => { +reducers$2[SET_FILE_LIST_SORT] = (state, action) => { const pageSortStates = { [PUBLISHED]: 'fileListPublishedSort', [DOWNLOADED]: 'fileListDownloadedSort' @@ -2969,8 +3141,8 @@ reducers$1[SET_FILE_LIST_SORT] = (state, action) => { }); }; -function fileInfoReducer(state = defaultState$1, action) { - const handler = reducers$1[action.type]; +function fileInfoReducer(state = defaultState$2, action) { + const handler = reducers$2[action.type]; if (handler) return handler(state, action); return state; } @@ -2993,9 +3165,9 @@ const handleActions = (actionMap, defaultState) => (state = defaultState, action return state; }; -var _extends$5 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$6 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -const defaultState$2 = { +const defaultState$3 = { notifications: [], toasts: [], errors: [] @@ -3008,7 +3180,7 @@ const notificationsReducer = handleActions({ const newToasts = state.toasts.slice(); newToasts.push(toast); - return _extends$5({}, state, { + return _extends$6({}, state, { toasts: newToasts }); }, @@ -3016,7 +3188,7 @@ const notificationsReducer = handleActions({ const newToasts = state.toasts.slice(); newToasts.shift(); - return _extends$5({}, state, { + return _extends$6({}, state, { toasts: newToasts }); }, @@ -3027,7 +3199,7 @@ const notificationsReducer = handleActions({ const newNotifications = state.notifications.slice(); newNotifications.push(notification); - return _extends$5({}, state, { + return _extends$6({}, state, { notifications: newNotifications }); }, @@ -3038,7 +3210,7 @@ const notificationsReducer = handleActions({ notifications = notifications.map(pastNotification => pastNotification.id === notification.id ? notification : pastNotification); - return _extends$5({}, state, { + return _extends$6({}, state, { notifications }); }, @@ -3047,7 +3219,7 @@ const notificationsReducer = handleActions({ let newNotifications = state.notifications.slice(); newNotifications = newNotifications.filter(notification => notification.id !== id); - return _extends$5({}, state, { + return _extends$6({}, state, { notifications: newNotifications }); }, @@ -3058,7 +3230,7 @@ const notificationsReducer = handleActions({ const newErrors = state.errors.slice(); newErrors.push(error); - return _extends$5({}, state, { + return _extends$6({}, state, { errors: newErrors }); }, @@ -3066,15 +3238,15 @@ const notificationsReducer = handleActions({ const newErrors = state.errors.slice(); newErrors.shift(); - return _extends$5({}, state, { + return _extends$6({}, state, { errors: newErrors }); } -}, defaultState$2); +}, defaultState$3); -var _extends$6 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$7 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -const defaultState$3 = { +const defaultState$4 = { isActive: false, // does the user have any typed text in the search input focused: false, // is the search input focused searchQuery: '', // needs to be an empty string for input focusing @@ -3092,29 +3264,29 @@ const defaultState$3 = { }; const searchReducer = handleActions({ - [SEARCH_START]: state => _extends$6({}, state, { + [SEARCH_START]: state => _extends$7({}, state, { searching: true }), [SEARCH_SUCCESS]: (state, action) => { const { query, uris } = action.data; - return _extends$6({}, state, { + return _extends$7({}, state, { searching: false, urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }) }); }, - [SEARCH_FAIL]: state => _extends$6({}, state, { + [SEARCH_FAIL]: state => _extends$7({}, state, { searching: false }), - [UPDATE_SEARCH_QUERY]: (state, action) => _extends$6({}, state, { + [UPDATE_SEARCH_QUERY]: (state, action) => _extends$7({}, state, { searchQuery: action.data.query, isActive: true }), - [UPDATE_SEARCH_SUGGESTIONS]: (state, action) => _extends$6({}, state, { - suggestions: _extends$6({}, state.suggestions, { + [UPDATE_SEARCH_SUGGESTIONS]: (state, action) => _extends$7({}, state, { + suggestions: _extends$7({}, state.suggestions, { [action.data.query]: action.data.suggestions }) }), @@ -3122,27 +3294,27 @@ const searchReducer = handleActions({ // 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 - [DISMISS_NOTIFICATION]: state => _extends$6({}, state, { + [DISMISS_NOTIFICATION]: state => _extends$7({}, state, { isActive: false }), - [SEARCH_FOCUS]: state => _extends$6({}, state, { + [SEARCH_FOCUS]: state => _extends$7({}, state, { focused: true }), - [SEARCH_BLUR]: state => _extends$6({}, state, { + [SEARCH_BLUR]: state => _extends$7({}, state, { focused: false }), [UPDATE_SEARCH_OPTIONS]: (state, action) => { const { options: oldOptions } = state; const newOptions = action.data; - const options = _extends$6({}, oldOptions, newOptions); - return _extends$6({}, state, { + const options = _extends$7({}, oldOptions, newOptions); + return _extends$7({}, state, { options }); } -}, defaultState$3); +}, defaultState$4); -var _extends$7 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$8 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const buildDraftTransaction = () => ({ amount: undefined, @@ -3153,7 +3325,7 @@ const buildDraftTransaction = () => ({ // See details in https://github.com/lbryio/lbry/issues/1307 -const defaultState$4 = { +const defaultState$5 = { balance: undefined, totalBalance: undefined, latestBlock: undefined, @@ -3182,25 +3354,25 @@ const defaultState$4 = { }; const walletReducer = handleActions({ - [FETCH_TRANSACTIONS_STARTED]: state => _extends$7({}, state, { + [FETCH_TRANSACTIONS_STARTED]: state => _extends$8({}, state, { fetchingTransactions: true }), [FETCH_TRANSACTIONS_COMPLETED]: (state, action) => { - const byId = _extends$7({}, state.transactions); + const byId = _extends$8({}, state.transactions); const { transactions } = action.data; transactions.forEach(transaction => { byId[transaction.txid] = transaction; }); - return _extends$7({}, state, { + return _extends$8({}, state, { transactions: byId, fetchingTransactions: false }); }, - [FETCH_SUPPORTS_STARTED]: state => _extends$7({}, state, { + [FETCH_SUPPORTS_STARTED]: state => _extends$8({}, state, { fetchingSupports: true }), @@ -3213,7 +3385,7 @@ const walletReducer = handleActions({ byOutpoint[`${txid}:${nout}`] = transaction; }); - return _extends$7({}, state, { supports: byOutpoint, fetchingSupports: false }); + return _extends$8({}, state, { supports: byOutpoint, fetchingSupports: false }); }, [ABANDON_SUPPORT_STARTED]: (state, action) => { @@ -3222,7 +3394,7 @@ const walletReducer = handleActions({ currentlyAbandoning[outpoint] = true; - return _extends$7({}, state, { + return _extends$8({}, state, { abandoningSupportsByOutpoint: currentlyAbandoning }); }, @@ -3235,56 +3407,56 @@ const walletReducer = handleActions({ delete currentlyAbandoning[outpoint]; delete byOutpoint[outpoint]; - return _extends$7({}, state, { + return _extends$8({}, state, { supports: byOutpoint, abandoningSupportsById: currentlyAbandoning }); }, - [GET_NEW_ADDRESS_STARTED]: state => _extends$7({}, state, { + [GET_NEW_ADDRESS_STARTED]: state => _extends$8({}, state, { gettingNewAddress: true }), [GET_NEW_ADDRESS_COMPLETED]: (state, action) => { const { address } = action.data; - return _extends$7({}, state, { gettingNewAddress: false, receiveAddress: address }); + return _extends$8({}, state, { gettingNewAddress: false, receiveAddress: address }); }, - [UPDATE_BALANCE]: (state, action) => _extends$7({}, state, { + [UPDATE_BALANCE]: (state, action) => _extends$8({}, state, { balance: action.data.balance }), - [UPDATE_TOTAL_BALANCE]: (state, action) => _extends$7({}, state, { + [UPDATE_TOTAL_BALANCE]: (state, action) => _extends$8({}, state, { totalBalance: action.data.totalBalance }), - [CHECK_ADDRESS_IS_MINE_STARTED]: state => _extends$7({}, state, { + [CHECK_ADDRESS_IS_MINE_STARTED]: state => _extends$8({}, state, { checkingAddressOwnership: true }), - [CHECK_ADDRESS_IS_MINE_COMPLETED]: state => _extends$7({}, state, { + [CHECK_ADDRESS_IS_MINE_COMPLETED]: state => _extends$8({}, state, { checkingAddressOwnership: false }), [SET_DRAFT_TRANSACTION_AMOUNT]: (state, action) => { const oldDraft = state.draftTransaction; - const newDraft = _extends$7({}, oldDraft, { amount: parseFloat(action.data.amount) }); + const newDraft = _extends$8({}, oldDraft, { amount: parseFloat(action.data.amount) }); - return _extends$7({}, state, { draftTransaction: newDraft }); + return _extends$8({}, state, { draftTransaction: newDraft }); }, [SET_DRAFT_TRANSACTION_ADDRESS]: (state, action) => { const oldDraft = state.draftTransaction; - const newDraft = _extends$7({}, oldDraft, { address: action.data.address }); + const newDraft = _extends$8({}, oldDraft, { address: action.data.address }); - return _extends$7({}, state, { draftTransaction: newDraft }); + return _extends$8({}, state, { draftTransaction: newDraft }); }, [SEND_TRANSACTION_STARTED]: state => { - const newDraftTransaction = _extends$7({}, state.draftTransaction, { sending: true }); + const newDraftTransaction = _extends$8({}, state.draftTransaction, { sending: true }); - return _extends$7({}, state, { draftTransaction: newDraftTransaction }); + return _extends$8({}, state, { draftTransaction: newDraftTransaction }); }, [SEND_TRANSACTION_COMPLETED]: state => Object.assign({}, state, { @@ -3297,134 +3469,134 @@ const walletReducer = handleActions({ error: action.data.error }); - return _extends$7({}, state, { draftTransaction: newDraftTransaction }); + return _extends$8({}, state, { draftTransaction: newDraftTransaction }); }, - [SUPPORT_TRANSACTION_STARTED]: state => _extends$7({}, state, { + [SUPPORT_TRANSACTION_STARTED]: state => _extends$8({}, state, { sendingSupport: true }), - [SUPPORT_TRANSACTION_COMPLETED]: state => _extends$7({}, state, { + [SUPPORT_TRANSACTION_COMPLETED]: state => _extends$8({}, state, { sendingSupport: false }), - [SUPPORT_TRANSACTION_FAILED]: (state, action) => _extends$7({}, state, { + [SUPPORT_TRANSACTION_FAILED]: (state, action) => _extends$8({}, state, { error: action.data.error, sendingSupport: false }), - [WALLET_STATUS_COMPLETED]: (state, action) => _extends$7({}, state, { + [WALLET_STATUS_COMPLETED]: (state, action) => _extends$8({}, state, { walletIsEncrypted: action.result }), - [WALLET_ENCRYPT_START]: state => _extends$7({}, state, { + [WALLET_ENCRYPT_START]: state => _extends$8({}, state, { walletEncryptPending: true, walletEncryptSucceded: null, walletEncryptResult: null }), - [WALLET_ENCRYPT_COMPLETED]: (state, action) => _extends$7({}, state, { + [WALLET_ENCRYPT_COMPLETED]: (state, action) => _extends$8({}, state, { walletEncryptPending: false, walletEncryptSucceded: true, walletEncryptResult: action.result }), - [WALLET_ENCRYPT_FAILED]: (state, action) => _extends$7({}, state, { + [WALLET_ENCRYPT_FAILED]: (state, action) => _extends$8({}, state, { walletEncryptPending: false, walletEncryptSucceded: false, walletEncryptResult: action.result }), - [WALLET_DECRYPT_START]: state => _extends$7({}, state, { + [WALLET_DECRYPT_START]: state => _extends$8({}, state, { walletDecryptPending: true, walletDecryptSucceded: null, walletDecryptResult: null }), - [WALLET_DECRYPT_COMPLETED]: (state, action) => _extends$7({}, state, { + [WALLET_DECRYPT_COMPLETED]: (state, action) => _extends$8({}, state, { walletDecryptPending: false, walletDecryptSucceded: true, walletDecryptResult: action.result }), - [WALLET_DECRYPT_FAILED]: (state, action) => _extends$7({}, state, { + [WALLET_DECRYPT_FAILED]: (state, action) => _extends$8({}, state, { walletDecryptPending: false, walletDecryptSucceded: false, walletDecryptResult: action.result }), - [WALLET_UNLOCK_START]: state => _extends$7({}, state, { + [WALLET_UNLOCK_START]: state => _extends$8({}, state, { walletUnlockPending: true, walletUnlockSucceded: null, walletUnlockResult: null }), - [WALLET_UNLOCK_COMPLETED]: (state, action) => _extends$7({}, state, { + [WALLET_UNLOCK_COMPLETED]: (state, action) => _extends$8({}, state, { walletUnlockPending: false, walletUnlockSucceded: true, walletUnlockResult: action.result }), - [WALLET_UNLOCK_FAILED]: (state, action) => _extends$7({}, state, { + [WALLET_UNLOCK_FAILED]: (state, action) => _extends$8({}, state, { walletUnlockPending: false, walletUnlockSucceded: false, walletUnlockResult: action.result }), - [WALLET_LOCK_START]: state => _extends$7({}, state, { + [WALLET_LOCK_START]: state => _extends$8({}, state, { walletLockPending: false, walletLockSucceded: null, walletLockResult: null }), - [WALLET_LOCK_COMPLETED]: (state, action) => _extends$7({}, state, { + [WALLET_LOCK_COMPLETED]: (state, action) => _extends$8({}, state, { walletLockPending: false, walletLockSucceded: true, walletLockResult: action.result }), - [WALLET_LOCK_FAILED]: (state, action) => _extends$7({}, state, { + [WALLET_LOCK_FAILED]: (state, action) => _extends$8({}, state, { walletLockPending: false, walletLockSucceded: false, walletLockResult: action.result }), - [SET_TRANSACTION_LIST_FILTER]: (state, action) => _extends$7({}, state, { + [SET_TRANSACTION_LIST_FILTER]: (state, action) => _extends$8({}, state, { transactionListFilter: action.data }), - [UPDATE_CURRENT_HEIGHT]: (state, action) => _extends$7({}, state, { + [UPDATE_CURRENT_HEIGHT]: (state, action) => _extends$8({}, state, { latestBlock: action.data }) -}, defaultState$4); +}, defaultState$5); -var _extends$8 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$9 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -const reducers$2 = {}; -const defaultState$5 = { +const reducers$3 = {}; +const defaultState$6 = { positions: {} }; -reducers$2[SET_CONTENT_POSITION] = (state, action) => { +reducers$3[SET_CONTENT_POSITION] = (state, action) => { const { claimId, outpoint, position } = action.data; - return _extends$8({}, state, { - positions: _extends$8({}, state.positions, { - [claimId]: _extends$8({}, state.positions[claimId], { + return _extends$9({}, state, { + positions: _extends$9({}, state.positions, { + [claimId]: _extends$9({}, state.positions[claimId], { [outpoint]: position }) }) }); }; -function contentReducer(state = defaultState$5, action) { - const handler = reducers$2[action.type]; +function contentReducer(state = defaultState$6, action) { + const handler = reducers$3[action.type]; if (handler) return handler(state, action); return state; } -const selectState$4 = state => state.content || {}; +const selectState$5 = state => state.content || {}; -const makeSelectContentPositionForUri = uri => reselect.createSelector(selectState$4, makeSelectClaimForUri(uri), (state, claim) => { +const makeSelectContentPositionForUri = uri => reselect.createSelector(selectState$5, makeSelectClaimForUri(uri), (state, claim) => { if (!claim) { return null; } @@ -3433,14 +3605,14 @@ const makeSelectContentPositionForUri = uri => reselect.createSelector(selectSta return state.positions[id] ? state.positions[id][outpoint] : null; }); -var _extends$9 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$a = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -const selectState$5 = state => state.notifications || {}; +const selectState$6 = state => state.notifications || {}; -const selectToast = reselect.createSelector(selectState$5, state => { +const selectToast = reselect.createSelector(selectState$6, state => { if (state.toasts.length) { const { id, params } = state.toasts[0]; - return _extends$9({ + return _extends$a({ id }, params); } @@ -3448,7 +3620,7 @@ const selectToast = reselect.createSelector(selectState$5, state => { return null; }); -const selectError = reselect.createSelector(selectState$5, state => { +const selectError = reselect.createSelector(selectState$6, state => { if (state.errors.length) { const { error } = state.errors[0]; return { @@ -3488,9 +3660,11 @@ exports.doFetchClaimsByChannel = doFetchClaimsByChannel; exports.doFetchFileInfo = doFetchFileInfo; exports.doFetchFileInfosAndPublishedClaims = doFetchFileInfosAndPublishedClaims; exports.doFetchTransactions = doFetchTransactions; +exports.doFileGet = doFileGet; exports.doFileList = doFileList; exports.doFocusSearchInput = doFocusSearchInput; exports.doGetNewAddress = doGetNewAddress; +exports.doPurchaseUri = doPurchaseUri; exports.doResolveUri = doResolveUri; exports.doResolveUris = doResolveUris; exports.doSearch = doSearch; @@ -3512,6 +3686,7 @@ exports.doWalletEncrypt = doWalletEncrypt; exports.doWalletStatus = doWalletStatus; exports.doWalletUnlock = doWalletUnlock; exports.fileInfoReducer = fileInfoReducer; +exports.fileReducer = fileReducer; exports.formatCredits = formatCredits; exports.formatFullPrice = formatFullPrice; exports.isClaimNsfw = isClaimNsfw; @@ -3543,6 +3718,7 @@ exports.makeSelectPendingByUri = makeSelectPendingByUri; exports.makeSelectQueryWithOptions = makeSelectQueryWithOptions; exports.makeSelectRecommendedContentForUri = makeSelectRecommendedContentForUri; exports.makeSelectSearchUris = makeSelectSearchUris; +exports.makeSelectStreamingUrlForUri = makeSelectStreamingUrlForUri; exports.makeSelectThumbnailForUri = makeSelectThumbnailForUri; exports.makeSelectTitleForUri = makeSelectTitleForUri; exports.makeSelectTotalItemsForChannel = makeSelectTotalItemsForChannel; @@ -3572,6 +3748,7 @@ exports.selectDraftTransactionAddress = selectDraftTransactionAddress; exports.selectDraftTransactionAmount = selectDraftTransactionAmount; exports.selectDraftTransactionError = selectDraftTransactionError; exports.selectError = selectError; +exports.selectFailedPurchaseUris = selectFailedPurchaseUris; exports.selectFetchingMyChannels = selectFetchingMyChannels; exports.selectFileInfosByOutpoint = selectFileInfosByOutpoint; exports.selectFileInfosDownloaded = selectFileInfosDownloaded; @@ -3585,6 +3762,7 @@ exports.selectIsFetchingFileListDownloadedOrPublished = selectIsFetchingFileList exports.selectIsFetchingTransactions = selectIsFetchingTransactions; exports.selectIsSearching = selectIsSearching; exports.selectIsSendingSupport = selectIsSendingSupport; +exports.selectLastPurchasedUri = selectLastPurchasedUri; exports.selectMyActiveClaims = selectMyActiveClaims; exports.selectMyChannelClaims = selectMyChannelClaims; exports.selectMyClaims = selectMyClaims; @@ -3594,6 +3772,8 @@ exports.selectMyClaimsWithoutChannels = selectMyClaimsWithoutChannels; exports.selectPendingById = selectPendingById; exports.selectPendingClaims = selectPendingClaims; exports.selectPlayingUri = selectPlayingUri; +exports.selectPurchasedStreamingUrls = selectPurchasedStreamingUrls; +exports.selectPurchasedUris = selectPurchasedUris; exports.selectReceiveAddress = selectReceiveAddress; exports.selectRecentTransactions = selectRecentTransactions; exports.selectResolvingUris = selectResolvingUris; diff --git a/dist/flow-typed/File.js b/dist/flow-typed/File.js index a4f8a60..4384d3f 100644 --- a/dist/flow-typed/File.js +++ b/dist/flow-typed/File.js @@ -24,6 +24,7 @@ declare type FileListItem = { stopped: false, stream_hash: string, stream_name: string, + streaming_url: string, suggested_file_name: string, total_bytes: number, total_bytes_lower_bound: number, @@ -33,3 +34,24 @@ declare type FileListItem = { txid: string, written_bytes: number, }; + +declare type FileState = { + failedPurchaseUris: Array, + purchasedUris: Array, + purchasedStreamingUrls: {}, +}; + +declare type PurchaseUriCompleted = { + type: ACTIONS.PURCHASE_URI_COMPLETED, + data: { + uri: string, + streamingUrl: string, + }, +}; + +declare type PurchaseUriFailed = { + type: ACTIONS.PURCHASE_URI_FAILED, + data: { + uri: string + }, +}; diff --git a/flow-typed/File.js b/flow-typed/File.js index a4f8a60..4384d3f 100644 --- a/flow-typed/File.js +++ b/flow-typed/File.js @@ -24,6 +24,7 @@ declare type FileListItem = { stopped: false, stream_hash: string, stream_name: string, + streaming_url: string, suggested_file_name: string, total_bytes: number, total_bytes_lower_bound: number, @@ -33,3 +34,24 @@ declare type FileListItem = { txid: string, written_bytes: number, }; + +declare type FileState = { + failedPurchaseUris: Array, + purchasedUris: Array, + purchasedStreamingUrls: {}, +}; + +declare type PurchaseUriCompleted = { + type: ACTIONS.PURCHASE_URI_COMPLETED, + data: { + uri: string, + streamingUrl: string, + }, +}; + +declare type PurchaseUriFailed = { + type: ACTIONS.PURCHASE_URI_FAILED, + data: { + uri: string + }, +}; diff --git a/src/constants/action_types.js b/src/constants/action_types.js index 95d4035..9582f77 100644 --- a/src/constants/action_types.js +++ b/src/constants/action_types.js @@ -106,6 +106,12 @@ export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED'; export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'; export const FILE_DELETE = 'FILE_DELETE'; export const SET_FILE_LIST_SORT = 'SET_FILE_LIST_SORT'; +export const PURCHASE_URI_STARTED = 'PURCHASE_URI_STARTED'; +export const PURCHASE_URI_COMPLETED = 'PURCHASE_URI_COMPLETED'; +export const PURCHASE_URI_FAILED = 'PURCHASE_URI_FAILED'; +export const LOADING_FILE_STARTED = 'LOADING_FILE_STARTED'; +export const LOADING_FILE_COMPLETED = 'LOADING_FILE_COMPLETED'; +export const LOADING_FILE_FAILED = 'LOADING_FILE_FAILED'; // Search export const SEARCH_START = 'SEARCH_START'; @@ -217,3 +223,8 @@ export const CREATE_ERROR = 'CREATE_ERROR'; export const DISMISS_ERROR = 'DISMISS_ERROR'; export const FETCH_DATE = 'FETCH_DATE'; + +// Cost info +export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED'; +export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED'; +export const FETCH_COST_INFO_FAILED = 'FETCH_COST_INFO_FAILED'; diff --git a/src/index.js b/src/index.js index 9fa1fff..9265f41 100644 --- a/src/index.js +++ b/src/index.js @@ -47,6 +47,8 @@ export { doCreateChannel, } from 'redux/actions/claims'; +export { doPurchaseUri, doFileGet } from 'redux/actions/file'; + export { doFetchFileInfo, doFileList, @@ -93,6 +95,7 @@ export { isClaimNsfw } from 'util/claim'; // reducers export { claimsReducer } from 'redux/reducers/claims'; +export { fileReducer } from 'redux/reducers/file'; export { fileInfoReducer } from 'redux/reducers/file_info'; export { notificationsReducer } from 'redux/reducers/notifications'; export { searchReducer } from 'redux/reducers/search'; @@ -104,6 +107,14 @@ export { makeSelectContentPositionForUri } from 'redux/selectors/content'; export { selectToast, selectError } from 'redux/selectors/notifications'; +export { + selectFailedPurchaseUris, + selectPurchasedUris, + selectPurchasedStreamingUrls, + selectLastPurchasedUri, + makeSelectStreamingUrlForUri, +} from 'redux/selectors/file'; + export { makeSelectClaimForUri, makeSelectClaimIsMine, diff --git a/src/redux/actions/file.js b/src/redux/actions/file.js new file mode 100644 index 0000000..6d7bb37 --- /dev/null +++ b/src/redux/actions/file.js @@ -0,0 +1,104 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import Lbry from 'lbry'; +import { doToast } from 'redux/actions/notifications'; +import { selectBalance } from 'redux/selectors/wallet'; +import { makeSelectFileInfoForUri, selectDownloadingByOutpoint } from 'redux/selectors/file_info'; +import { makeSelectStreamingUrlForUri } from 'redux/selectors/file'; + +type Dispatch = (action: any) => any; +type GetState = () => { file: FileState }; + +export function doFileGet(uri: string, saveFile: boolean = true) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.LOADING_FILE_STARTED, + data: { + uri, + }, + }); + + // set save_file argument to True to save the file (old behaviour) + Lbry.get({ uri, save_file: saveFile }) + .then((streamInfo: GetResponse) => { + const timeout = streamInfo === null || typeof streamInfo !== 'object'; + + if (timeout) { + dispatch({ + type: ACTIONS.LOADING_FILE_FAILED, + data: { uri }, + }); + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri }, + }); + + dispatch(doToast({ message: `File timeout for uri ${uri}`, isError: true })); + } else { + // purchase was completed successfully + const { streaming_url: streamingUrl } = streamInfo; + dispatch({ + type: ACTIONS.PURCHASE_URI_COMPLETED, + data: { uri, streamingUrl: !saveFile && streamingUrl ? streamingUrl : null }, + }); + } + }) + .catch(() => { + dispatch({ + type: ACTIONS.LOADING_FILE_FAILED, + data: { uri }, + }); + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri }, + }); + + dispatch( + doToast({ + message: `Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.com/faq/support for support.`, + isError: true, + }) + ); + }); + }; +} + +export function doPurchaseUri(uri: string, costInfo: { cost: number }, saveFile: boolean = true) { + return (dispatch: Dispatch, getState: GetState) => { + dispatch({ + type: ACTIONS.PURCHASE_URI_STARTED, + data: { uri }, + }); + + const state = getState(); + const balance = selectBalance(state); + const fileInfo = makeSelectFileInfoForUri(uri)(state); + const downloadingByOutpoint = selectDownloadingByOutpoint(state); + const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; + const alreadyStreaming = makeSelectStreamingUrlForUri(uri)(state); + + if (alreadyDownloading || alreadyStreaming) { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error: `Already fetching uri: ${uri}` }, + }); + + Promise.resolve(); + return; + } + + const { cost } = costInfo; + + if (cost > balance) { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error: 'Insufficient credits' }, + }); + + Promise.resolve(); + return; + } + + dispatch(doFileGet(uri, saveFile)); + }; +} diff --git a/src/redux/reducers/file.js b/src/redux/reducers/file.js new file mode 100644 index 0000000..2e593fa --- /dev/null +++ b/src/redux/reducers/file.js @@ -0,0 +1,58 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; + +const reducers = {}; +const defaultState = { + failedPurchaseUris: [], + purchasedUris: [], + purchasedStreamingUrls: {}, +}; + +reducers[ACTIONS.PURCHASE_URI_COMPLETED] = ( + state: FileState, + action: PurchaseUriCompleted +): FileState => { + const { uri, streamingUrl } = action.data; + const newPurchasedUris = state.purchasedUris.slice(); + const newFailedPurchaseUris = state.failedPurchaseUris.slice(); + const newPurchasedStreamingUrls = Object.assign({}, state.purchasedStreamingUrls); + + if (!newPurchasedUris.includes(uri)) { + newPurchasedUris.push(uri); + } + if (newFailedPurchaseUris.includes(uri)) { + newFailedPurchaseUris.splice(newFailedPurchaseUris.indexOf(uri), 1); + } + if (streamingUrl) { + newPurchasedStreamingUrls[uri] = streamingUrl; + } + + return { + ...state, + failedPurchaseUris: newFailedPurchaseUris, + purchasedUris: newPurchasedUris, + purchasedStreamingUrls: newPurchasedStreamingUrls, + }; +}; + +reducers[ACTIONS.PURCHASE_URI_FAILED] = ( + state: FileState, + action: PurchaseUriFailed +): FileState => { + const { uri } = action.data; + const newFailedPurchaseUris = state.failedPurchaseUris.slice(); + if (!newFailedPurchaseUris.includes(uri)) { + newFailedPurchaseUris.push(uri); + } + + return { + ...state, + failedPurchaseUris: newFailedPurchaseUris, + }; +}; + +export function fileReducer(state: FileState = defaultState, action: any) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/src/redux/selectors/file.js b/src/redux/selectors/file.js new file mode 100644 index 0000000..11caee5 --- /dev/null +++ b/src/redux/selectors/file.js @@ -0,0 +1,33 @@ +// @flow +import { createSelector } from 'reselect'; + +type State = { file: FileState }; + +export const selectState = (state: State): FileState => state.file || {}; + +export const selectFailedPurchaseUris: (state: State) => Array = createSelector( + selectState, + state => state.failedPurchaseUris +); + +export const selectPurchasedUris: (state: State) => Array = createSelector( + selectState, + state => state.purchasedUris +); + +export const selectPurchasedStreamingUrls: (state: State) => {} = createSelector( + selectState, + state => state.purchasedStreamingUrls +); + +export const selectLastPurchasedUri: (state: State) => string = createSelector( + selectState, + state => + state.purchasedUris.length > 0 ? state.purchasedUris[state.purchasedUris.length - 1] : null +); + +export const makeSelectStreamingUrlForUri = (uri: string): ((state: State) => {}) => + createSelector( + selectPurchasedStreamingUrls, + streamingUrls => streamingUrls && streamingUrls[uri] + );