added claims, file_info and search actions, reducers and selectors
This commit is contained in:
parent
57e5b4a086
commit
2ae5556916
24 changed files with 4907 additions and 300 deletions
3170
build/index.js
3170
build/index.js
File diff suppressed because it is too large
Load diff
77
src/index.js
77
src/index.js
|
@ -1,4 +1,77 @@
|
|||
export { Lbry } from 'lbry';
|
||||
export { LbryApi } from 'lbryapi';
|
||||
// common
|
||||
import Lbry from 'lbry';
|
||||
import LbryApi from 'lbryapi';
|
||||
import Lbryuri from 'lbryuri';
|
||||
export { Lbry, LbryApi, Lbryuri };
|
||||
|
||||
// actions
|
||||
export { doFetchClaimListMine, doAbandonClaim, doResolveUris, doResolveUri } from 'redux/actions/claims';
|
||||
export { doFetchCostInfoForUri } from 'redux/actions/cost_info';
|
||||
export { doFetchFileInfo, doFileList, doFetchFileInfosAndPublishedClaims } from 'redux/actions/file_info';
|
||||
export { doSearch } from 'redux/actions/search';
|
||||
|
||||
// reducers
|
||||
export { claimsReducer } from 'redux/reducers/claims';
|
||||
export { costInfoReducer } from 'redux/reducers/cost_info';
|
||||
export { fileInfoReducer } from 'redux/reducers/file_info';
|
||||
export { searchReducer } from 'redux/reducers/search';
|
||||
export { walletReducer } from 'redux/reducers/wallet';
|
||||
|
||||
// selectors
|
||||
export {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectFetchingChannelClaims,
|
||||
makeSelectClaimsInChannelForCurrentPage,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectIsUriResolving,
|
||||
selectClaimsById,
|
||||
selectClaimsByUri,
|
||||
selectAllClaimsByChannel,
|
||||
selectMyClaimsRaw,
|
||||
selectAbandoningIds,
|
||||
selectMyActiveClaims,
|
||||
selectAllFetchingChannelClaims,
|
||||
selectIsFetchingClaimListMine,
|
||||
selectPendingClaims,
|
||||
selectMyClaims,
|
||||
selectMyClaimsWithoutChannels,
|
||||
selectAllMyClaimsByOutpoint,
|
||||
selectMyClaimsOutpoints,
|
||||
selectFetchingMyChannels,
|
||||
selectMyChannelClaims,
|
||||
selectResolvingUris
|
||||
} from 'redux/selectors/claims';
|
||||
|
||||
export {
|
||||
makeSelectFetchingCostInfoForUri,
|
||||
makeSelectCostInfoForUri,
|
||||
selectAllCostInfoByUri,
|
||||
selectCostForCurrentPageUri,
|
||||
selectFetchingCostInfo
|
||||
} from 'redux/selectors/cost_info';
|
||||
|
||||
export {
|
||||
selectFileInfosByOutpoint,
|
||||
selectIsFetchingFileList,
|
||||
selectIsFetchingFileListDownloadedOrPublished,
|
||||
makeSelectFileInfoForUri,
|
||||
selectDownloadingByOutpoint,
|
||||
makeSelectDownloadingForUri,
|
||||
selectUrisLoading,
|
||||
makeSelectLoadingForUri,
|
||||
selectFileInfosDownloaded,
|
||||
selectDownloadingFileInfos,
|
||||
selectTotalDownloadProgress
|
||||
} from 'redux/selectors/file_info';
|
||||
|
||||
export {
|
||||
makeSelectSearchUris,
|
||||
selectSearchQuery,
|
||||
selectIsSearching,
|
||||
selectSearchUrisByQuery,
|
||||
selectWunderBarAddress,
|
||||
selectWunderBarIcon
|
||||
} from 'redux/selectors/search';
|
||||
|
|
26
src/lbry.js
26
src/lbry.js
|
@ -12,19 +12,6 @@ function apiCall(method, params, resolve, reject) {
|
|||
return jsonrpc.call(Lbry.daemonConnectionString, method, params, resolve, reject, reject);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function getLocal(key, fallback = undefined) {
|
||||
const itemRaw = localStorage.getItem(key);
|
||||
return itemRaw === null ? fallback : JSON.parse(itemRaw);
|
||||
|
@ -276,4 +263,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;
|
||||
|
|
|
@ -2,6 +2,7 @@ import Lbry from 'lbry';
|
|||
import querystring from 'querystring';
|
||||
|
||||
const LbryApi = {
|
||||
enabled: true,
|
||||
exchangePromise: null,
|
||||
exchangeLastFetched: null,
|
||||
};
|
||||
|
@ -31,7 +32,7 @@ LbryApi.getExchangeRates = () => {
|
|||
};
|
||||
|
||||
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')));
|
||||
}
|
||||
|
|
238
src/lbryuri.js
Normal file
238
src/lbryuri.js
Normal file
|
@ -0,0 +1,238 @@
|
|||
const CHANNEL_NAME_MIN_LEN = 1;
|
||||
const CLAIM_ID_MAX_LEN = 40;
|
||||
|
||||
const Lbryuri = {};
|
||||
|
||||
Lbryuri.REGEXP_INVALID_URI = /[^A-Za-z0-9-]/g;
|
||||
Lbryuri.REGEXP_ADDRESS = /^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 @
|
||||
*/
|
||||
Lbryuri.parse = (uri, requireProto = false) => {
|
||||
// Break into components. Empty sub-matches are converted to null
|
||||
const componentsRegex = new RegExp(
|
||||
'^((?:lbry://)?)' + // protocol
|
||||
'([^:$#/]*)' + // name (stops at the first separator or end)
|
||||
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
|
||||
'(/?)(.*)' // path separator, path
|
||||
);
|
||||
const [proto, name, 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 (!name) {
|
||||
throw new Error(__('URI does not include name.'));
|
||||
}
|
||||
|
||||
const isChannel = name.startsWith('@');
|
||||
const channelName = isChannel ? name.slice(1) : name;
|
||||
|
||||
if (isChannel) {
|
||||
if (!channelName) {
|
||||
throw new Error(__('No channel name after @.'));
|
||||
}
|
||||
|
||||
if (channelName.length < CHANNEL_NAME_MIN_LEN) {
|
||||
throw new Error(__(`Channel names must be at least %s characters.`, CHANNEL_NAME_MIN_LEN));
|
||||
}
|
||||
|
||||
contentName = path;
|
||||
}
|
||||
|
||||
const nameBadChars = (channelName || name).match(Lbryuri.REGEXP_INVALID_URI);
|
||||
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 > CLAIM_ID_MAX_LEN || !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(Lbryuri.REGEXP_INVALID_URI);
|
||||
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 {
|
||||
name,
|
||||
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 lbryuri.parse() and builds a URI.
|
||||
*
|
||||
* The channelName key will accept names with or without the @ prefix.
|
||||
*/
|
||||
Lbryuri.build = (uriObj, includeProto = true) => {
|
||||
const { claimId, claimSequence, bidPosition, contentName, channelName } = uriObj;
|
||||
|
||||
let { name, path } = uriObj;
|
||||
|
||||
if (channelName) {
|
||||
const channelNameFormatted = channelName.startsWith('@') ? channelName : `@${channelName}`;
|
||||
if (!name) {
|
||||
name = channelNameFormatted;
|
||||
} else if (name !== channelNameFormatted) {
|
||||
throw new Error(
|
||||
__(
|
||||
'Received a channel content URI, but 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 (!name) {
|
||||
name = 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://' : '') +
|
||||
name +
|
||||
(claimId ? `#${claimId}` : '') +
|
||||
(claimSequence ? `:${claimSequence}` : '') +
|
||||
(bidPosition ? `${bidPosition}` : '') +
|
||||
(path ? `/${path}` : '')
|
||||
);
|
||||
};
|
||||
|
||||
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
|
||||
* consists of adding the lbry:// prefix if needed) */
|
||||
Lbryuri.normalize = uri => {
|
||||
if (uri.match(/pending_claim/)) return uri;
|
||||
|
||||
const { name, path, bidPosition, claimSequence, claimId } = Lbryuri.parse(uri);
|
||||
return Lbryuri.build({ name, path, claimSequence, bidPosition, claimId });
|
||||
};
|
||||
|
||||
Lbryuri.isValid = uri => {
|
||||
let parts;
|
||||
try {
|
||||
parts = Lbryuri.parse(Lbryuri.normalize(uri));
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return parts && parts.name;
|
||||
};
|
||||
|
||||
Lbryuri.isValidName = (name, checkCase = true) => {
|
||||
const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i');
|
||||
return regexp.test(name);
|
||||
};
|
||||
|
||||
Lbryuri.isClaimable = uri => {
|
||||
let parts;
|
||||
try {
|
||||
parts = Lbryuri.parse(Lbryuri.normalize(uri));
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
parts &&
|
||||
parts.name &&
|
||||
!parts.claimId &&
|
||||
!parts.bidPosition &&
|
||||
!parts.claimSequence &&
|
||||
!parts.isChannel &&
|
||||
!parts.path
|
||||
);
|
||||
};
|
||||
|
||||
if (window) {
|
||||
window.lbryuri = Lbryuri;
|
||||
}
|
||||
export default Lbryuri;
|
107
src/redux/actions/claims.js
Normal file
107
src/redux/actions/claims.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import Lbry from 'lbry';
|
||||
import Lbryuri from 'lbryuri';
|
||||
import { selectResolvingUris } from 'redux/selectors/claims';
|
||||
|
||||
export function doResolveUris(uris) {
|
||||
return (dispatch, getState) => {
|
||||
const normalizedUris = uris.map(Lbryuri.normalize);
|
||||
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(doOpenModal(MODALS.TRANSACTION_FAILED));
|
||||
};
|
||||
|
||||
const successCallback = results => {
|
||||
if (results.txid) {
|
||||
dispatch({
|
||||
type: ACTIONS.ABANDON_CLAIM_SUCCEEDED,
|
||||
data: {
|
||||
claimId,
|
||||
},
|
||||
});
|
||||
dispatch(doResolveUri(Lbryuri.build({ name, claimId })));
|
||||
dispatch(doFetchClaimListMine());
|
||||
} else {
|
||||
dispatch(doOpenModal(MODALS.TRANSACTION_FAILED));
|
||||
}
|
||||
};
|
||||
|
||||
Lbry.claim_abandon({
|
||||
txid,
|
||||
nout,
|
||||
}).then(successCallback, errorCallback);
|
||||
};
|
||||
}
|
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());
|
||||
};
|
||||
}
|
64
src/redux/actions/navigation.js
Normal file
64
src/redux/actions/navigation.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import { selectHistoryIndex, selectHistoryStack } from 'redux/selectors/navigation';
|
||||
import { toQueryString } from 'util/query_params';
|
||||
|
||||
export function doNavigate(path, params = {}, options = {}) {
|
||||
return dispatch => {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = path;
|
||||
if (params && Object.values(params).length) {
|
||||
url += `?${toQueryString(params)}`;
|
||||
}
|
||||
|
||||
const { scrollY } = options;
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.HISTORY_NAVIGATE,
|
||||
data: { url, index: options.index, scrollY },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doAuthNavigate(pathAfterAuth = null, params = {}) {
|
||||
return dispatch => {
|
||||
if (pathAfterAuth) {
|
||||
dispatch({
|
||||
type: ACTIONS.CHANGE_AFTER_AUTH_PATH,
|
||||
data: {
|
||||
path: `${pathAfterAuth}?${toQueryString(params)}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
dispatch(doNavigate('/auth'));
|
||||
};
|
||||
}
|
||||
|
||||
export function doHistoryTraverse(dispatch, state, modifier) {
|
||||
const stack = selectHistoryStack(state);
|
||||
const index = selectHistoryIndex(state) + modifier;
|
||||
|
||||
if (index >= 0 && index < stack.length) {
|
||||
const historyItem = stack[index];
|
||||
dispatch(doNavigate(historyItem.path, {}, { scrollY: historyItem.scrollY, index }));
|
||||
}
|
||||
}
|
||||
|
||||
export function doHistoryBack() {
|
||||
return (dispatch, getState) => doHistoryTraverse(dispatch, getState(), -1);
|
||||
}
|
||||
|
||||
export function doHistoryForward() {
|
||||
return (dispatch, getState) => doHistoryTraverse(dispatch, getState(), 1);
|
||||
}
|
||||
|
||||
export function doRecordScroll(scroll) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: ACTIONS.WINDOW_SCROLLED,
|
||||
data: { scrollY: scroll },
|
||||
});
|
||||
};
|
||||
}
|
67
src/redux/actions/search.js
Normal file
67
src/redux/actions/search.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import Lbryuri from 'lbryuri';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { doNavigate } from 'redux/actions/navigation';
|
||||
import { selectCurrentPage } from 'redux/selectors/navigation';
|
||||
import batchActions from 'util/batchActions';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function doSearch(rawQuery) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const page = selectCurrentPage(state);
|
||||
|
||||
const query = rawQuery.replace(/^lbry:\/\//i, '');
|
||||
|
||||
if (!query) {
|
||||
dispatch({
|
||||
type: ACTIONS.SEARCH_CANCELLED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SEARCH_STARTED,
|
||||
data: { query },
|
||||
});
|
||||
|
||||
if (page !== 'search') {
|
||||
dispatch(doNavigate('search', { query }));
|
||||
} else {
|
||||
fetch(`https://lighthouse.lbry.io/search?s=${query}`)
|
||||
.then(
|
||||
response =>
|
||||
response.status === 200
|
||||
? Promise.resolve(response.json())
|
||||
: Promise.reject(new Error(response.statusText))
|
||||
)
|
||||
.then(data => {
|
||||
const uris = [];
|
||||
const actions = [];
|
||||
|
||||
data.forEach(result => {
|
||||
const uri = Lbryuri.build({
|
||||
name: result.name,
|
||||
claimId: result.claimId,
|
||||
});
|
||||
actions.push(doResolveUri(uri));
|
||||
uris.push(uri);
|
||||
});
|
||||
|
||||
actions.push({
|
||||
type: ACTIONS.SEARCH_COMPLETED,
|
||||
data: {
|
||||
query,
|
||||
uris,
|
||||
},
|
||||
});
|
||||
dispatch(batchActions(...actions));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch({
|
||||
type: ACTIONS.SEARCH_CANCELLED,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
207
src/redux/actions/wallet.js
Normal file
207
src/redux/actions/wallet.js
Normal file
|
@ -0,0 +1,207 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import Lbry from 'lbry';
|
||||
import { doOpenModal, doShowSnackBar } from 'redux/actions/app';
|
||||
import { doNavigate } from 'redux/actions/navigation';
|
||||
import {
|
||||
selectBalance,
|
||||
selectDraftTransaction,
|
||||
selectDraftTransactionAmount,
|
||||
} from 'redux/selectors/wallet';
|
||||
|
||||
export function doUpdateBalance() {
|
||||
return dispatch => {
|
||||
Lbry.wallet_balance().then(balance =>
|
||||
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({ include_tip_info: true }).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,
|
||||
});
|
||||
|
||||
Lbry.wallet_new_address().then(address => {
|
||||
localStorage.setItem('wallet_address', 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(doOpenModal(MODALS.INSUFFICIENT_CREDITS));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SEND_TRANSACTION_STARTED,
|
||||
});
|
||||
|
||||
const successCallback = results => {
|
||||
if (results === true) {
|
||||
dispatch({
|
||||
type: ACTIONS.SEND_TRANSACTION_COMPLETED,
|
||||
});
|
||||
dispatch(
|
||||
doShowSnackBar({
|
||||
message: __(`You sent ${amount} LBC`),
|
||||
linkText: __('History'),
|
||||
linkTarget: __('/wallet'),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch({
|
||||
type: ACTIONS.SEND_TRANSACTION_FAILED,
|
||||
data: { error: results },
|
||||
});
|
||||
dispatch(doOpenModal(MODALS.TRANSACTION_FAILED));
|
||||
}
|
||||
};
|
||||
|
||||
const errorCallback = error => {
|
||||
dispatch({
|
||||
type: ACTIONS.SEND_TRANSACTION_FAILED,
|
||||
data: { error: error.message },
|
||||
});
|
||||
dispatch(doOpenModal(MODALS.TRANSACTION_FAILED));
|
||||
};
|
||||
|
||||
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) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const balance = selectBalance(state);
|
||||
|
||||
if (balance - amount <= 0) {
|
||||
dispatch(doOpenModal(MODALS.INSUFFICIENT_CREDITS));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SUPPORT_TRANSACTION_STARTED,
|
||||
});
|
||||
|
||||
const successCallback = results => {
|
||||
if (results.txid) {
|
||||
dispatch({
|
||||
type: ACTIONS.SUPPORT_TRANSACTION_COMPLETED,
|
||||
});
|
||||
dispatch(
|
||||
doShowSnackBar({
|
||||
message: __(`You sent ${amount} LBC as support, Mahalo!`),
|
||||
linkText: __('History'),
|
||||
linkTarget: __('/wallet'),
|
||||
})
|
||||
);
|
||||
dispatch(doNavigate('/show', { uri }));
|
||||
} else {
|
||||
dispatch({
|
||||
type: ACTIONS.SUPPORT_TRANSACTION_FAILED,
|
||||
data: { error: results.code },
|
||||
});
|
||||
dispatch(doOpenModal(MODALS.TRANSACTION_FAILED));
|
||||
}
|
||||
};
|
||||
|
||||
const errorCallback = error => {
|
||||
dispatch({
|
||||
type: ACTIONS.SUPPORT_TRANSACTION_FAILED,
|
||||
data: { error: error.code },
|
||||
});
|
||||
dispatch(doOpenModal(MODALS.TRANSACTION_FAILED));
|
||||
};
|
||||
|
||||
Lbry.wallet_send({
|
||||
claim_id: claimId,
|
||||
amount,
|
||||
}).then(successCallback, errorCallback);
|
||||
};
|
||||
}
|
183
src/redux/reducers/claims.js
Normal file
183
src/redux/reducers/claims.js
Normal file
|
@ -0,0 +1,183 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
|
||||
const reducers = {};
|
||||
|
||||
const defaultState = {};
|
||||
|
||||
reducers[ACTIONS.RESOLVE_URIS_COMPLETED] = (state, action) => {
|
||||
const { resolveInfo } = action.data;
|
||||
const byUri = Object.assign({}, state.claimsByUri);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
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];
|
||||
if (handler) return handler(state, action);
|
||||
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;
|
||||
}
|
32
src/redux/reducers/search.js
Normal file
32
src/redux/reducers/search.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
|
||||
const reducers = {};
|
||||
const defaultState = {
|
||||
urisByQuery: {},
|
||||
searching: false,
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SEARCH_STARTED] = state =>
|
||||
Object.assign({}, state, {
|
||||
searching: true,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.SEARCH_COMPLETED] = (state, action) => {
|
||||
const { query, uris } = action.data;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
searching: false,
|
||||
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
|
||||
});
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SEARCH_CANCELLED] = state =>
|
||||
Object.assign({}, state, {
|
||||
searching: false,
|
||||
});
|
||||
|
||||
export function searchReducer(state = defaultState, action) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
return state;
|
||||
}
|
148
src/redux/reducers/wallet.js
Normal file
148
src/redux/reducers/wallet.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
|
||||
const reducers = {};
|
||||
const receiveAddress = localStorage.getItem('receiveAddress');
|
||||
const buildDraftTransaction = () => ({
|
||||
amount: undefined,
|
||||
address: undefined,
|
||||
});
|
||||
|
||||
const defaultState = {
|
||||
balance: undefined,
|
||||
blocks: {},
|
||||
transactions: {},
|
||||
fetchingTransactions: false,
|
||||
receiveAddress,
|
||||
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;
|
||||
|
||||
localStorage.setItem('receiveAddress', address);
|
||||
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 Lbryuri from 'lbryuri';
|
||||
import { makeSelectCurrentParam } from 'redux/selectors/navigation';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const selectState = state => state.claims || {};
|
||||
|
@ -23,3 +25,158 @@ export const selectClaimsByUri = createSelector(selectState, selectClaimsById, (
|
|||
|
||||
return claims;
|
||||
});
|
||||
|
||||
export const selectAllClaimsByChannel = createSelector(
|
||||
selectState,
|
||||
state => state.claimsByChannel || {}
|
||||
);
|
||||
|
||||
export const makeSelectClaimForUri = uri =>
|
||||
createSelector(selectClaimsByUri, claims => claims && claims[Lbryuri.normalize(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 = Lbryuri.normalize(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
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createSelector } from 'reselect';
|
||||
//import { selectCurrentParams } from 'redux/selectors/navigation';
|
||||
import { selectCurrentParams } from 'redux/selectors/navigation';
|
||||
|
||||
export const selectState = state => state.costInfo || {};
|
||||
|
||||
|
@ -10,7 +10,7 @@ export const makeSelectCostInfoForUri = uri =>
|
|||
|
||||
export const selectCostForCurrentPageUri = createSelector(
|
||||
selectAllCostInfoByUri,
|
||||
{}/*selectCurrentParams*/,
|
||||
selectCurrentParams,
|
||||
(costInfo, params) => (params.uri && costInfo[params.uri] ? costInfo[params.uri].cost : undefined)
|
||||
);
|
||||
|
||||
|
|
106
src/redux/selectors/file_info.js
Normal file
106
src/redux/selectors/file_info.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
import {
|
||||
selectClaimsByUri,
|
||||
selectIsFetchingClaimListMine,
|
||||
selectMyClaims,
|
||||
} from 'redux/selectors/claims';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
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;
|
||||
});
|
139
src/redux/selectors/navigation.js
Normal file
139
src/redux/selectors/navigation.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { parseQueryParams, toQueryString } from 'util/query_params';
|
||||
import Lbryuri from 'lbryuri';
|
||||
|
||||
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 selectHeaderLinks = createSelector(selectCurrentPage, page => {
|
||||
// This contains intentional fall throughs
|
||||
switch (page) {
|
||||
case 'wallet':
|
||||
case 'history':
|
||||
case 'send':
|
||||
case 'getcredits':
|
||||
case 'invite':
|
||||
case 'rewards':
|
||||
case 'backup':
|
||||
return {
|
||||
wallet: __('Overview'),
|
||||
getcredits: __('Get Credits'),
|
||||
send: __('Send / Receive'),
|
||||
rewards: __('Rewards'),
|
||||
invite: __('Invites'),
|
||||
history: __('History'),
|
||||
};
|
||||
case 'downloaded':
|
||||
case 'published':
|
||||
return {
|
||||
downloaded: __('Downloaded'),
|
||||
published: __('Published'),
|
||||
};
|
||||
case 'settings':
|
||||
case 'help':
|
||||
return {
|
||||
settings: __('Settings'),
|
||||
help: __('Help'),
|
||||
};
|
||||
case 'discover':
|
||||
case 'subscriptions':
|
||||
return {
|
||||
discover: __('Discover'),
|
||||
subscriptions: __('Subscriptions'),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const selectPageTitle = createSelector(
|
||||
selectCurrentPage,
|
||||
selectCurrentParams,
|
||||
(page, params) => {
|
||||
switch (page) {
|
||||
case 'settings':
|
||||
return __('Settings');
|
||||
case 'report':
|
||||
return __('Report');
|
||||
case 'wallet':
|
||||
return __('Wallet');
|
||||
case 'send':
|
||||
return __('Send or Receive LBRY Credits');
|
||||
case 'getcredits':
|
||||
return __('Get LBRY Credits');
|
||||
case 'backup':
|
||||
return __('Backup Your Wallet');
|
||||
case 'rewards':
|
||||
return __('Rewards');
|
||||
case 'invite':
|
||||
return __('Invites');
|
||||
case 'start':
|
||||
return __('Start');
|
||||
case 'publish':
|
||||
return params.id ? __('Edit') : __('Publish');
|
||||
case 'help':
|
||||
return __('Help');
|
||||
case 'developer':
|
||||
return __('Developer');
|
||||
case 'show': {
|
||||
const parts = [Lbryuri.normalize(params.uri)];
|
||||
// If the params has any keys other than "uri"
|
||||
if (Object.keys(params).length > 1) {
|
||||
parts.push(toQueryString(Object.assign({}, params, { uri: null })));
|
||||
}
|
||||
return parts.join('?');
|
||||
}
|
||||
case 'downloaded':
|
||||
return __('Downloads & Purchases');
|
||||
case 'published':
|
||||
return __('Publications');
|
||||
case 'search':
|
||||
return params.query ? __('Search results for %s', params.query) : __('Search');
|
||||
case 'subscriptions':
|
||||
return __('Your Subscriptions');
|
||||
case 'discover':
|
||||
case false:
|
||||
case null:
|
||||
case '':
|
||||
return '';
|
||||
default:
|
||||
return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : '');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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 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]
|
||||
);
|
79
src/redux/selectors/search.js
Normal file
79
src/redux/selectors/search.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import {
|
||||
selectCurrentPage,
|
||||
selectCurrentParams,
|
||||
selectPageTitle,
|
||||
} from 'redux/selectors/navigation';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const selectState = state => state.search || {};
|
||||
|
||||
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,
|
||||
selectPageTitle,
|
||||
selectSearchQuery,
|
||||
(page, title, query) => (page !== 'search' ? title : query || title)
|
||||
);
|
||||
|
||||
export const selectWunderBarIcon = createSelector(
|
||||
selectCurrentPage,
|
||||
selectCurrentParams,
|
||||
(page, params) => {
|
||||
switch (page) {
|
||||
case 'auth':
|
||||
return 'icon-user';
|
||||
case 'settings':
|
||||
return 'icon-gear';
|
||||
case 'help':
|
||||
return 'icon-question';
|
||||
case 'report':
|
||||
return 'icon-file';
|
||||
case 'downloaded':
|
||||
return 'icon-folder';
|
||||
case 'published':
|
||||
return 'icon-folder';
|
||||
case 'history':
|
||||
return 'icon-history';
|
||||
case 'send':
|
||||
return 'icon-send';
|
||||
case 'rewards':
|
||||
return 'icon-rocket';
|
||||
case 'invite':
|
||||
return 'icon-envelope-open';
|
||||
case 'getcredits':
|
||||
return 'icon-shopping-cart';
|
||||
case 'wallet':
|
||||
case 'backup':
|
||||
return 'icon-bank';
|
||||
case 'show':
|
||||
return 'icon-file';
|
||||
case 'publish':
|
||||
return params.id ? __('icon-pencil') : __('icon-upload');
|
||||
case 'developer':
|
||||
return 'icon-code';
|
||||
case 'discover':
|
||||
case 'search':
|
||||
return 'icon-search';
|
||||
case 'subscriptions':
|
||||
return 'icon-th-list';
|
||||
default:
|
||||
return 'icon-file';
|
||||
}
|
||||
}
|
||||
);
|
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)
|
||||
);
|
9
src/util/batchActions.js
Normal file
9
src/util/batchActions.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
// https://github.com/reactjs/redux/issues/911
|
||||
function batchActions(...actions) {
|
||||
return {
|
||||
type: 'BATCH_ACTIONS',
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
export default batchActions;
|
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('&');
|
||||
}
|
|
@ -4,7 +4,7 @@ module.exports = {
|
|||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
filename: 'index.js',
|
||||
libraryTarget: 'commonjs2'
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
|
Loading…
Reference in a new issue