2021-10-17 10:36:14 +02:00
|
|
|
// @flow
|
|
|
|
import { normalizeURI, parseURI, isURIValid } from 'util/lbryURI';
|
|
|
|
import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
|
|
|
|
import { createSelector } from 'reselect';
|
2021-10-08 03:57:48 +02:00
|
|
|
import { createCachedSelector } from 're-reselect';
|
2021-10-17 10:36:14 +02:00
|
|
|
import { isClaimNsfw, filterClaims } from 'util/claim';
|
|
|
|
import * as CLAIM from 'constants/claim';
|
|
|
|
|
2021-10-08 03:57:48 +02:00
|
|
|
type State = { claims: any };
|
2021-10-17 10:36:14 +02:00
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
const selectState = (state: State) => state.claims || {};
|
2021-10-17 10:36:14 +02:00
|
|
|
|
2021-10-08 03:57:48 +02:00
|
|
|
export const selectById = (state: State) => selectState(state).byId || {};
|
|
|
|
export const selectPendingClaimsById = (state: State) => selectState(state).pendingById || {};
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const selectClaimsById = createSelector(selectById, selectPendingClaimsById, (byId, pendingById) => {
|
|
|
|
return Object.assign(byId, pendingById); // do I need merged to keep metadata?
|
|
|
|
});
|
|
|
|
|
2021-10-08 03:57:48 +02:00
|
|
|
export const selectClaimIdsByUri = (state: State) => selectState(state).claimsByUri || {};
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectCurrentChannelPage = (state: State) => selectState(state).currentChannelPage || 1;
|
|
|
|
export const selectCreatingChannel = (state: State) => selectState(state).creatingChannel;
|
|
|
|
export const selectCreateChannelError = (state: State) => selectState(state).createChannelError;
|
|
|
|
export const selectRepostLoading = (state: State) => selectState(state).repostLoading;
|
|
|
|
export const selectRepostError = (state: State) => selectState(state).repostError;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const selectClaimsByUri = createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => {
|
|
|
|
const claims = {};
|
|
|
|
|
|
|
|
Object.keys(byUri).forEach((uri) => {
|
|
|
|
const claimId = byUri[uri];
|
|
|
|
|
|
|
|
// NOTE returning a null claim allows us to differentiate between an
|
|
|
|
// undefined (never fetched claim) and one which just doesn't exist. Not
|
|
|
|
// the cleanest solution but couldn't think of anything better right now
|
|
|
|
if (claimId === null) {
|
|
|
|
claims[uri] = null;
|
|
|
|
} else {
|
|
|
|
claims[uri] = byId[claimId];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return claims;
|
|
|
|
});
|
|
|
|
|
`byId[]` fixes to reduce invalidation (#239)
* Don't update 'pendingById' if no changes.
'pendingById' isn't frequently updated, but using it as a proof-of-concept to fix how reducers should be written to avoid unnecessary updates.
ImmutableJS apparently does all of this for us, but there are cons to using it as well, so using own wrappers for now.
* Don't update 'byId' if no changes + add 'selectClaimWithId'
## Ticket
116 Claim store optimization ideas (reducing unnecessary renders)
## Changes
- Ignore things like `confirmations` so that already-fetched claims aren't invalidated and causes re-rendering. The `stringify` might look expensive, but the amount of avoided re-renders outweighs it. There might be faster ways to compare, though.
- With `byId[claimId]` references more stable now, memoized selectors can now use 'selectClaimWithId' to pick a specific claim to depend on, instead of 'byId' which changes on every update.
* Fix memo: selectMyChannelClaims, selectActiveChannelClaim
## Issue
These should never recalculate after `channel_list` has been fetched, but they do because of poor selector dependency.
## Change
With the `byId` changes from the previous commit, we are now able to memoize these selectors correctly.
2021-11-08 18:25:29 +01:00
|
|
|
/**
|
|
|
|
* Returns the claim with the specified ID. The claim could be undefined if does
|
|
|
|
* not exist or have not fetched. Take note of the second parameter, which means
|
|
|
|
* an inline function or helper would be required when used as an input to
|
|
|
|
* 'createSelector'.
|
|
|
|
*
|
|
|
|
* @param state
|
|
|
|
* @param claimId
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
|
|
|
export const selectClaimWithId = (state: State, claimId: string) => {
|
|
|
|
const byId = selectClaimsById(state);
|
|
|
|
return byId[claimId];
|
|
|
|
};
|
|
|
|
|
2021-10-17 10:36:14 +02:00
|
|
|
export const selectAllClaimsByChannel = createSelector(selectState, (state) => state.paginatedClaimsByChannel || {});
|
|
|
|
|
|
|
|
export const selectPendingIds = createSelector(selectState, (state) => Object.keys(state.pendingById) || []);
|
|
|
|
|
|
|
|
export const selectPendingClaims = createSelector(selectPendingClaimsById, (pendingById) => Object.values(pendingById));
|
|
|
|
|
|
|
|
export const makeSelectClaimIsPending = (uri: string) =>
|
|
|
|
createSelector(selectClaimIdsByUri, selectPendingClaimsById, (idsByUri, pendingById) => {
|
|
|
|
const claimId = idsByUri[normalizeURI(uri)];
|
|
|
|
|
|
|
|
if (claimId) {
|
|
|
|
return Boolean(pendingById[claimId]);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectClaimIdIsPending = (claimId: string) =>
|
|
|
|
createSelector(selectPendingClaimsById, (pendingById) => {
|
|
|
|
return Boolean(pendingById[claimId]);
|
|
|
|
});
|
|
|
|
|
2021-11-09 13:23:09 +01:00
|
|
|
export const selectClaimIdForUri = (state: State, uri: string) => selectClaimIdsByUri(state)[uri];
|
2021-10-17 10:36:14 +02:00
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectReflectingById = (state: State) => selectState(state).reflectingById;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]);
|
|
|
|
|
2021-10-08 03:57:48 +02:00
|
|
|
export const selectClaimForUri = createCachedSelector(
|
|
|
|
selectClaimIdsByUri,
|
|
|
|
selectClaimsById,
|
|
|
|
(state, uri) => uri,
|
|
|
|
(state, uri, returnRepost = true) => returnRepost,
|
|
|
|
(byUri, byId, uri, returnRepost) => {
|
2021-11-02 17:37:53 +01:00
|
|
|
const validUri = isURIValid(uri, false);
|
2021-10-08 03:57:48 +02:00
|
|
|
|
|
|
|
if (validUri && byUri) {
|
2021-11-02 17:37:53 +01:00
|
|
|
const normalizedUri = normalizeURI(uri);
|
|
|
|
const claimId = uri && byUri[normalizedUri];
|
2021-10-08 03:57:48 +02:00
|
|
|
const claim = byId[claimId];
|
|
|
|
|
|
|
|
// Make sure to return the claim as is so apps can check if it's been resolved before (null) or still needs to be resolved (undefined)
|
|
|
|
if (claimId === null) {
|
|
|
|
return null;
|
|
|
|
} else if (claimId === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const repostedClaim = claim && claim.reposted_claim;
|
|
|
|
if (repostedClaim && returnRepost) {
|
|
|
|
const channelUrl =
|
|
|
|
claim.signing_channel && (claim.signing_channel.canonical_url || claim.signing_channel.permanent_url);
|
|
|
|
|
|
|
|
return {
|
|
|
|
...repostedClaim,
|
2021-11-02 17:37:53 +01:00
|
|
|
repost_url: normalizedUri,
|
2021-10-08 03:57:48 +02:00
|
|
|
repost_channel_url: channelUrl,
|
|
|
|
repost_bid_amount: claim && claim.meta && claim.meta.effective_amount,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return claim;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-10-25 06:58:34 +02:00
|
|
|
)((state, uri, returnRepost = true) => `${String(uri)}:${returnRepost ? '1' : '0'}`);
|
2021-10-08 03:57:48 +02:00
|
|
|
|
2021-11-02 17:37:53 +01:00
|
|
|
// Note: this is deprecated. Use "selectClaimForUri(state, uri)" instead.
|
2021-10-17 10:36:14 +02:00
|
|
|
export const makeSelectClaimForUri = (uri: string, returnRepost: boolean = true) =>
|
|
|
|
createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => {
|
2021-11-02 17:37:53 +01:00
|
|
|
const validUri = isURIValid(uri, false);
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
if (validUri && byUri) {
|
2021-11-02 17:37:53 +01:00
|
|
|
const normalizedUri = normalizeURI(uri);
|
|
|
|
const claimId = uri && byUri[normalizedUri];
|
2021-10-17 10:36:14 +02:00
|
|
|
const claim = byId[claimId];
|
|
|
|
|
|
|
|
// Make sure to return the claim as is so apps can check if it's been resolved before (null) or still needs to be resolved (undefined)
|
|
|
|
if (claimId === null) {
|
|
|
|
return null;
|
|
|
|
} else if (claimId === undefined) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const repostedClaim = claim && claim.reposted_claim;
|
|
|
|
if (repostedClaim && returnRepost) {
|
|
|
|
const channelUrl =
|
|
|
|
claim.signing_channel && (claim.signing_channel.canonical_url || claim.signing_channel.permanent_url);
|
|
|
|
|
|
|
|
return {
|
|
|
|
...repostedClaim,
|
2021-11-02 17:37:53 +01:00
|
|
|
repost_url: normalizedUri,
|
2021-10-17 10:36:14 +02:00
|
|
|
repost_channel_url: channelUrl,
|
|
|
|
repost_bid_amount: claim && claim.meta && claim.meta.effective_amount,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return claim;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-11-04 03:10:40 +01:00
|
|
|
// Returns your claim IDs without handling pending and abandoned claims.
|
|
|
|
export const selectMyClaimIdsRaw = (state: State) => selectState(state).myClaims;
|
|
|
|
|
2021-10-17 10:36:14 +02:00
|
|
|
export const selectMyClaimsRaw = createSelector(selectState, selectClaimsById, (state, byId) => {
|
|
|
|
const ids = state.myClaims;
|
|
|
|
if (!ids) {
|
|
|
|
return ids;
|
|
|
|
}
|
|
|
|
|
|
|
|
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-desktop/issues/544
|
|
|
|
claims.push(byId[id]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return claims;
|
|
|
|
});
|
|
|
|
|
2021-11-10 02:32:29 +01:00
|
|
|
export const selectAbandoningById = (state: State) => selectState(state).abandoningById || {};
|
|
|
|
export const selectAbandoningIds = createSelector(selectAbandoningById, (abandoningById) =>
|
|
|
|
Object.keys(abandoningById)
|
|
|
|
);
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const makeSelectAbandoningClaimById = (claimId: string) =>
|
|
|
|
createSelector(selectAbandoningIds, (ids) => ids.includes(claimId));
|
|
|
|
|
|
|
|
export const makeSelectIsAbandoningClaimForUri = (uri: string) =>
|
|
|
|
createSelector(selectClaimIdsByUri, selectAbandoningIds, (claimIdsByUri, abandoningById) => {
|
|
|
|
const claimId = claimIdsByUri[normalizeURI(uri)];
|
|
|
|
return abandoningById.indexOf(claimId) >= 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const selectMyActiveClaims = createSelector(
|
2021-11-10 02:32:29 +01:00
|
|
|
selectMyClaimIdsRaw,
|
2021-10-17 10:36:14 +02:00
|
|
|
selectAbandoningIds,
|
2021-11-10 02:32:29 +01:00
|
|
|
(myClaimIds, abandoningIds) => {
|
|
|
|
return new Set(myClaimIds && myClaimIds.filter((claimId) => !abandoningIds.includes(claimId)));
|
|
|
|
}
|
2021-10-17 10:36:14 +02:00
|
|
|
);
|
|
|
|
|
2021-11-10 03:30:03 +01:00
|
|
|
// Helper for 'selectClaimIsMineForUri'.
|
|
|
|
// Returns undefined string if unable to normalize or is not valid.
|
|
|
|
const selectNormalizedAndVerifiedUri = createCachedSelector(
|
|
|
|
(state, rawUri) => rawUri,
|
|
|
|
(rawUri) => {
|
|
|
|
try {
|
|
|
|
const uri = normalizeURI(rawUri);
|
|
|
|
if (isURIValid(uri, false)) {
|
|
|
|
return uri;
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
)((state, rawUri) => String(rawUri));
|
|
|
|
|
2021-11-11 05:48:10 +01:00
|
|
|
export const selectClaimIsMine = (state: State, claim: ?Claim) => {
|
|
|
|
if (claim) {
|
|
|
|
// The original code seems to imply that 'is_my_output' could be false even
|
|
|
|
// when it is yours and there is a need to double-check with 'myActiveClaims'.
|
|
|
|
// I'm retaining that logic. Otherwise, we could have just return
|
|
|
|
// is_my_output directly when it is defined and skip the fallback.
|
|
|
|
if (claim.is_my_output) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
// 'is_my_output' is false or undefined.
|
|
|
|
const myActiveClaims = selectMyActiveClaims(state);
|
|
|
|
return claim.claim_id && myActiveClaims.has(claim.claim_id);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-11-10 03:30:03 +01:00
|
|
|
export const selectClaimIsMineForUri = (state: State, rawUri: string) => {
|
|
|
|
// Not memoizing this selector because:
|
|
|
|
// (1) The workload is somewhat lightweight.
|
|
|
|
// (2) Since it depends on 'selectClaimsByUri', memoization won't work anyway
|
|
|
|
// because the array is constantly invalidated.
|
|
|
|
|
|
|
|
const uri = selectNormalizedAndVerifiedUri(state, rawUri);
|
|
|
|
if (!uri) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const claimsByUri = selectClaimsByUri(state);
|
2021-11-11 05:48:10 +01:00
|
|
|
return selectClaimIsMine(state, claimsByUri && claimsByUri[uri]);
|
2021-11-10 03:30:03 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// DEPRECATED - use selectClaimIsMineForUri instead.
|
2021-10-17 10:36:14 +02:00
|
|
|
export const makeSelectClaimIsMine = (rawUri: string) => {
|
|
|
|
let uri;
|
|
|
|
try {
|
|
|
|
uri = normalizeURI(rawUri);
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
|
2021-11-02 17:37:53 +01:00
|
|
|
if (!isURIValid(uri, false)) {
|
2021-10-17 10:36:14 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
claims &&
|
|
|
|
claims[uri] &&
|
|
|
|
(claims[uri].is_my_output || (claims[uri].claim_id && myClaims.has(claims[uri].claim_id)))
|
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectMyPurchases = (state: State) => selectState(state).myPurchases;
|
|
|
|
export const selectPurchaseUriSuccess = (state: State) => selectState(state).purchaseUriSuccess;
|
|
|
|
export const selectMyPurchasesCount = (state: State) => selectState(state).myPurchasesPageTotalResults;
|
|
|
|
export const selectIsFetchingMyPurchases = (state: State) => selectState(state).fetchingMyPurchases;
|
|
|
|
export const selectFetchingMyPurchasesError = (state: State) => selectState(state).fetchingMyPurchasesError;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const makeSelectMyPurchasesForPage = (query: ?string, page: number = 1) =>
|
|
|
|
createSelector(
|
|
|
|
selectMyPurchases,
|
|
|
|
selectClaimsByUri,
|
|
|
|
(myPurchases: Array<string>, claimsByUri: { [string]: Claim }) => {
|
|
|
|
if (!myPurchases) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!query) {
|
|
|
|
// ensure no duplicates from double purchase bugs
|
|
|
|
// return [...new Set(myPurchases)];
|
|
|
|
return Array.from(new Set(myPurchases));
|
|
|
|
}
|
|
|
|
|
|
|
|
const fileInfos = myPurchases.map((uri) => claimsByUri[uri]);
|
|
|
|
const matchingFileInfos = filterClaims(fileInfos, query);
|
|
|
|
const start = (Number(page) - 1) * Number(CLAIM.PAGE_SIZE);
|
|
|
|
const end = Number(page) * Number(CLAIM.PAGE_SIZE);
|
|
|
|
return matchingFileInfos && matchingFileInfos.length
|
|
|
|
? matchingFileInfos.slice(start, end).map((fileInfo) => fileInfo.canonical_url || fileInfo.permanent_url)
|
|
|
|
: [];
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const makeSelectClaimWasPurchased = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
return claim && claim.purchase_receipt !== undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const selectAllFetchingChannelClaims = createSelector(selectState, (state) => state.fetchingChannelClaims || {});
|
|
|
|
|
|
|
|
export const makeSelectFetchingChannelClaims = (uri: string) =>
|
|
|
|
createSelector(selectAllFetchingChannelClaims, (fetching) => fetching && fetching[uri]);
|
|
|
|
|
|
|
|
export const makeSelectClaimsInChannelForPage = (uri: string, page?: number) =>
|
|
|
|
createSelector(selectClaimsById, selectAllClaimsByChannel, (byId, allClaims) => {
|
|
|
|
const byChannel = allClaims[uri] || {};
|
|
|
|
const claimIds = byChannel[page || 1];
|
|
|
|
|
|
|
|
if (!claimIds) return claimIds;
|
|
|
|
|
|
|
|
return claimIds.map((claimId) => byId[claimId]);
|
|
|
|
});
|
|
|
|
|
|
|
|
// THIS IS LEFT OVER FROM ONE TAB CHANNEL_CONTENT
|
|
|
|
export const makeSelectTotalClaimsInChannelSearch = (uri: string) =>
|
|
|
|
createSelector(selectClaimsById, selectAllClaimsByChannel, (byId, allClaims) => {
|
|
|
|
const byChannel = allClaims[uri] || {};
|
|
|
|
return byChannel['itemCount'];
|
|
|
|
});
|
|
|
|
|
|
|
|
// THIS IS LEFT OVER FROM ONE_TAB CHANNEL CONTENT
|
|
|
|
export const makeSelectTotalPagesInChannelSearch = (uri: string) =>
|
|
|
|
createSelector(selectClaimsById, selectAllClaimsByChannel, (byId, allClaims) => {
|
|
|
|
const byChannel = allClaims[uri] || {};
|
|
|
|
return byChannel['pageCount'];
|
|
|
|
});
|
|
|
|
|
2021-10-08 07:52:30 +02:00
|
|
|
export const selectMetadataForUri = createCachedSelector(selectClaimForUri, (claim, uri) => {
|
|
|
|
const metadata = claim && claim.value;
|
|
|
|
return metadata || (claim === undefined ? undefined : null);
|
2021-10-25 06:58:34 +02:00
|
|
|
})((state, uri) => String(uri));
|
2021-10-08 07:52:30 +02:00
|
|
|
|
2021-10-17 10:36:14 +02:00
|
|
|
export const makeSelectMetadataForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
const metadata = claim && claim.value;
|
|
|
|
return metadata || (claim === undefined ? undefined : null);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectMetadataItemForUri = (uri: string, key: string) =>
|
|
|
|
createSelector(makeSelectMetadataForUri(uri), (metadata: ChannelMetadata | StreamMetadata) => {
|
|
|
|
return metadata ? metadata[key] : undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectTitleForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectMetadataForUri(uri), (metadata) => metadata && metadata.title);
|
|
|
|
|
2021-10-08 03:57:48 +02:00
|
|
|
export const selectDateForUri = createCachedSelector(
|
|
|
|
selectClaimForUri, // input: (state, uri, ?returnRepost)
|
|
|
|
(claim) => {
|
2021-10-17 10:36:14 +02:00
|
|
|
const timestamp =
|
|
|
|
claim &&
|
|
|
|
claim.value &&
|
|
|
|
(claim.value.release_time
|
|
|
|
? claim.value.release_time * 1000
|
|
|
|
: claim.meta && claim.meta.creation_timestamp
|
|
|
|
? claim.meta.creation_timestamp * 1000
|
|
|
|
: null);
|
|
|
|
if (!timestamp) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const dateObj = new Date(timestamp);
|
|
|
|
return dateObj;
|
2021-10-08 03:57:48 +02:00
|
|
|
}
|
2021-10-25 06:58:34 +02:00
|
|
|
)((state, uri) => String(uri));
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const makeSelectAmountForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
return claim && claim.amount;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectEffectiveAmountForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri, false), (claim) => {
|
|
|
|
return (
|
|
|
|
claim && claim.meta && typeof claim.meta.effective_amount === 'string' && Number(claim.meta.effective_amount)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectContentTypeForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
const source = claim && claim.value && claim.value.source;
|
|
|
|
return source ? source.media_type : undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectThumbnailForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
const thumbnail = claim && claim.value && claim.value.thumbnail;
|
|
|
|
return thumbnail && thumbnail.url ? thumbnail.url.trim().replace(/^http:\/\//i, 'https://') : undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectCoverForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
const cover = claim && claim.value && claim.value.cover;
|
|
|
|
return cover && cover.url ? cover.url.trim().replace(/^http:\/\//i, 'https://') : undefined;
|
|
|
|
});
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectIsFetchingClaimListMine = (state: State) => selectState(state).isFetchingClaimListMine;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const selectMyClaimsPage = createSelector(selectState, (state) => state.myClaimsPageResults || []);
|
|
|
|
|
|
|
|
export const selectMyClaimsPageNumber = createSelector(
|
|
|
|
selectState,
|
|
|
|
(state) => (state.claimListMinePage && state.claimListMinePage.items) || [],
|
|
|
|
|
|
|
|
(state) => (state.txoPage && state.txoPage.page) || 1
|
|
|
|
);
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectMyClaimsPageItemCount = (state: State) => selectState(state).myClaimsPageTotalResults || 1;
|
|
|
|
export const selectFetchingMyClaimsPageError = (state: State) => selectState(state).fetchingClaimListMinePageError;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const selectMyClaims = createSelector(
|
|
|
|
selectMyActiveClaims,
|
|
|
|
selectClaimsById,
|
|
|
|
selectAbandoningIds,
|
|
|
|
(myClaimIds, byId, abandoningIds) => {
|
|
|
|
const claims = [];
|
|
|
|
|
|
|
|
myClaimIds.forEach((id) => {
|
|
|
|
const claim = byId[id];
|
|
|
|
|
|
|
|
if (claim && abandoningIds.indexOf(id) === -1) claims.push(claim);
|
|
|
|
});
|
|
|
|
|
|
|
|
return [...claims];
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const selectMyClaimsWithoutChannels = createSelector(selectMyClaims, (myClaims) =>
|
|
|
|
myClaims.filter((claim) => claim && !claim.name.match(/^@/)).sort((a, b) => a.timestamp - b.timestamp)
|
|
|
|
);
|
|
|
|
|
|
|
|
export const selectMyClaimUrisWithoutChannels = createSelector(selectMyClaimsWithoutChannels, (myClaims) => {
|
|
|
|
return myClaims
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (a.height < 1) {
|
|
|
|
return -1;
|
|
|
|
} else if (b.height < 1) {
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
return b.timestamp - a.timestamp;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.map((claim) => {
|
|
|
|
return claim.canonical_url || claim.permanent_url;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectFetchingMyChannels = (state: State) => selectState(state).fetchingMyChannels;
|
|
|
|
export const selectFetchingMyCollections = (state: State) => selectState(state).fetchingMyCollections;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
2021-11-08 07:27:14 +01:00
|
|
|
export const selectMyChannelClaimIds = (state: State) => selectState(state).myChannelClaims;
|
|
|
|
|
`byId[]` fixes to reduce invalidation (#239)
* Don't update 'pendingById' if no changes.
'pendingById' isn't frequently updated, but using it as a proof-of-concept to fix how reducers should be written to avoid unnecessary updates.
ImmutableJS apparently does all of this for us, but there are cons to using it as well, so using own wrappers for now.
* Don't update 'byId' if no changes + add 'selectClaimWithId'
## Ticket
116 Claim store optimization ideas (reducing unnecessary renders)
## Changes
- Ignore things like `confirmations` so that already-fetched claims aren't invalidated and causes re-rendering. The `stringify` might look expensive, but the amount of avoided re-renders outweighs it. There might be faster ways to compare, though.
- With `byId[claimId]` references more stable now, memoized selectors can now use 'selectClaimWithId' to pick a specific claim to depend on, instead of 'byId' which changes on every update.
* Fix memo: selectMyChannelClaims, selectActiveChannelClaim
## Issue
These should never recalculate after `channel_list` has been fetched, but they do because of poor selector dependency.
## Change
With the `byId` changes from the previous commit, we are now able to memoize these selectors correctly.
2021-11-08 18:25:29 +01:00
|
|
|
export const selectMyChannelClaims = createSelector(selectMyChannelClaimIds, (myChannelClaimIds) => {
|
|
|
|
if (!myChannelClaimIds) {
|
|
|
|
return myChannelClaimIds;
|
2021-10-17 10:36:14 +02:00
|
|
|
}
|
|
|
|
|
`byId[]` fixes to reduce invalidation (#239)
* Don't update 'pendingById' if no changes.
'pendingById' isn't frequently updated, but using it as a proof-of-concept to fix how reducers should be written to avoid unnecessary updates.
ImmutableJS apparently does all of this for us, but there are cons to using it as well, so using own wrappers for now.
* Don't update 'byId' if no changes + add 'selectClaimWithId'
## Ticket
116 Claim store optimization ideas (reducing unnecessary renders)
## Changes
- Ignore things like `confirmations` so that already-fetched claims aren't invalidated and causes re-rendering. The `stringify` might look expensive, but the amount of avoided re-renders outweighs it. There might be faster ways to compare, though.
- With `byId[claimId]` references more stable now, memoized selectors can now use 'selectClaimWithId' to pick a specific claim to depend on, instead of 'byId' which changes on every update.
* Fix memo: selectMyChannelClaims, selectActiveChannelClaim
## Issue
These should never recalculate after `channel_list` has been fetched, but they do because of poor selector dependency.
## Change
With the `byId` changes from the previous commit, we are now able to memoize these selectors correctly.
2021-11-08 18:25:29 +01:00
|
|
|
if (!window || !window.store) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: Grabbing the store and running the selector this way is anti-pattern,
|
|
|
|
// but it is _needed_ and works only because we know for sure that 'byId[]'
|
|
|
|
// will be populated with the same claims as when 'myChannelClaimIds' is populated.
|
|
|
|
// If we put 'state' or 'byId' as the input selector, it essentially
|
|
|
|
// recalculates every time. Putting 'state' as input to createSelector() is
|
|
|
|
// always wrong from a memoization standpoint.
|
|
|
|
const state = window.store.getState();
|
|
|
|
const byId = selectClaimsById(state);
|
|
|
|
|
2021-10-17 10:36:14 +02:00
|
|
|
const claims = [];
|
`byId[]` fixes to reduce invalidation (#239)
* Don't update 'pendingById' if no changes.
'pendingById' isn't frequently updated, but using it as a proof-of-concept to fix how reducers should be written to avoid unnecessary updates.
ImmutableJS apparently does all of this for us, but there are cons to using it as well, so using own wrappers for now.
* Don't update 'byId' if no changes + add 'selectClaimWithId'
## Ticket
116 Claim store optimization ideas (reducing unnecessary renders)
## Changes
- Ignore things like `confirmations` so that already-fetched claims aren't invalidated and causes re-rendering. The `stringify` might look expensive, but the amount of avoided re-renders outweighs it. There might be faster ways to compare, though.
- With `byId[claimId]` references more stable now, memoized selectors can now use 'selectClaimWithId' to pick a specific claim to depend on, instead of 'byId' which changes on every update.
* Fix memo: selectMyChannelClaims, selectActiveChannelClaim
## Issue
These should never recalculate after `channel_list` has been fetched, but they do because of poor selector dependency.
## Change
With the `byId` changes from the previous commit, we are now able to memoize these selectors correctly.
2021-11-08 18:25:29 +01:00
|
|
|
myChannelClaimIds.forEach((id) => {
|
2021-10-17 10:36:14 +02:00
|
|
|
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-desktop/issues/544
|
|
|
|
claims.push(byId[id]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return claims;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const selectMyChannelUrls = createSelector(selectMyChannelClaims, (claims) =>
|
|
|
|
claims ? claims.map((claim) => claim.canonical_url || claim.permanent_url) : undefined
|
|
|
|
);
|
|
|
|
|
2021-11-08 07:27:14 +01:00
|
|
|
export const selectHasChannels = (state: State) => {
|
|
|
|
const myChannelClaimIds = selectMyChannelClaimIds(state);
|
|
|
|
return myChannelClaimIds ? myChannelClaimIds.length > 0 : false;
|
|
|
|
};
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectMyCollectionIds = (state: State) => selectState(state).myCollectionClaims;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const selectResolvingUris = createSelector(selectState, (state) => state.resolvingUris || []);
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectChannelImportPending = (state: State) => selectState(state).pendingChannelImport;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const makeSelectIsUriResolving = (uri: string) =>
|
|
|
|
createSelector(selectResolvingUris, (resolvingUris) => resolvingUris && resolvingUris.indexOf(uri) !== -1);
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectPlayingUri = (state: State) => selectState(state).playingUri;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const selectChannelClaimCounts = createSelector(selectState, (state) => state.channelClaimCounts || {});
|
|
|
|
|
|
|
|
export const makeSelectPendingClaimForUri = (uri: string) =>
|
|
|
|
createSelector(selectPendingClaimsById, (pendingById) => {
|
|
|
|
let uriStreamName;
|
|
|
|
let uriChannelName;
|
|
|
|
try {
|
|
|
|
({ streamName: uriStreamName, channelName: uriChannelName } = parseURI(uri));
|
|
|
|
} catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const pendingClaims = (Object.values(pendingById): any);
|
|
|
|
const matchingClaim = pendingClaims.find((claim: GenericClaim) => {
|
|
|
|
return claim.normalized_name === uriChannelName || claim.normalized_name === uriStreamName;
|
|
|
|
});
|
|
|
|
return matchingClaim || null;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectTotalItemsForChannel = (uri: string) =>
|
|
|
|
createSelector(selectChannelClaimCounts, (byUri) => byUri && byUri[normalizeURI(uri)]);
|
|
|
|
|
|
|
|
export const makeSelectTotalPagesForChannel = (uri: string, pageSize: number = 10) =>
|
|
|
|
createSelector(
|
|
|
|
selectChannelClaimCounts,
|
|
|
|
(byUri) => byUri && byUri[uri] && Math.ceil(byUri[normalizeURI(uri)] / pageSize)
|
|
|
|
);
|
|
|
|
|
|
|
|
export const makeSelectNsfwCountFromUris = (uris: Array<string>) =>
|
|
|
|
createSelector(selectClaimsByUri, (claims) =>
|
|
|
|
uris.reduce((acc, uri) => {
|
|
|
|
const claim = claims[uri];
|
|
|
|
if (claim && isClaimNsfw(claim)) {
|
|
|
|
return acc + 1;
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, 0)
|
|
|
|
);
|
|
|
|
|
|
|
|
export const makeSelectOmittedCountForChannel = (uri: string) =>
|
|
|
|
createSelector(
|
|
|
|
makeSelectTotalItemsForChannel(uri),
|
|
|
|
makeSelectTotalClaimsInChannelSearch(uri),
|
|
|
|
(claimsInChannel, claimsInSearch) => {
|
|
|
|
if (claimsInChannel && typeof claimsInSearch === 'number' && claimsInSearch >= 0) {
|
|
|
|
return claimsInChannel - claimsInSearch;
|
|
|
|
} else return 0;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const makeSelectClaimIsNsfw = (uri: string) =>
|
|
|
|
createSelector(
|
|
|
|
makeSelectClaimForUri(uri),
|
|
|
|
// Eventually these will come from some list of tags that are considered adult
|
|
|
|
// Or possibly come from users settings of what tags they want to hide
|
|
|
|
// For now, there is just a hard coded list of tags inside `isClaimNsfw`
|
|
|
|
// selectNaughtyTags(),
|
|
|
|
(claim: Claim) => {
|
|
|
|
if (!claim) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return isClaimNsfw(claim);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// Returns the associated channel uri for a given claim uri
|
|
|
|
// accepts a regular claim uri lbry://something
|
|
|
|
// returns the channel uri that created this claim lbry://@channel
|
|
|
|
export const makeSelectChannelForClaimUri = (uri: string, includePrefix: boolean = false) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim: ?Claim) => {
|
|
|
|
if (!claim || !claim.signing_channel || !claim.is_channel_signature_valid) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { canonical_url: canonicalUrl, permanent_url: permanentUrl } = claim.signing_channel;
|
|
|
|
|
|
|
|
if (canonicalUrl) {
|
|
|
|
return includePrefix ? canonicalUrl : canonicalUrl.slice('lbry://'.length);
|
|
|
|
} else {
|
|
|
|
return includePrefix ? permanentUrl : permanentUrl.slice('lbry://'.length);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectChannelPermUrlForClaimUri = (uri: string, includePrefix: boolean = false) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim: ?Claim) => {
|
|
|
|
if (claim && claim.value_type === 'channel') {
|
|
|
|
return claim.permanent_url;
|
|
|
|
}
|
|
|
|
if (!claim || !claim.signing_channel || !claim.is_channel_signature_valid) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return claim.signing_channel.permanent_url;
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectMyChannelPermUrlForName = (name: string) =>
|
|
|
|
createSelector(selectMyChannelClaims, (claims) => {
|
|
|
|
const matchingClaim = claims && claims.find((claim) => claim.name === name);
|
|
|
|
return matchingClaim ? matchingClaim.permanent_url : null;
|
|
|
|
});
|
|
|
|
|
2021-10-08 07:52:30 +02:00
|
|
|
export const selectTagsForUri = createCachedSelector(selectMetadataForUri, (metadata: ?GenericMetadata) => {
|
|
|
|
return (metadata && metadata.tags) || [];
|
2021-10-25 06:58:34 +02:00
|
|
|
})((state, uri) => String(uri));
|
2021-10-08 07:52:30 +02:00
|
|
|
|
2021-10-17 10:36:14 +02:00
|
|
|
export const makeSelectTagsForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectMetadataForUri(uri), (metadata: ?GenericMetadata) => {
|
|
|
|
return (metadata && metadata.tags) || [];
|
|
|
|
});
|
|
|
|
|
|
|
|
export const selectFetchingClaimSearchByQuery = createSelector(
|
|
|
|
selectState,
|
|
|
|
(state) => state.fetchingClaimSearchByQuery || {}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const selectFetchingClaimSearch = createSelector(
|
|
|
|
selectFetchingClaimSearchByQuery,
|
|
|
|
(fetchingClaimSearchByQuery) => Boolean(Object.keys(fetchingClaimSearchByQuery).length)
|
|
|
|
);
|
|
|
|
|
|
|
|
export const selectClaimSearchByQuery = createSelector(selectState, (state) => state.claimSearchByQuery || {});
|
|
|
|
|
|
|
|
export const selectClaimSearchByQueryLastPageReached = createSelector(
|
|
|
|
selectState,
|
|
|
|
(state) => state.claimSearchByQueryLastPageReached || {}
|
|
|
|
);
|
|
|
|
|
|
|
|
export const makeSelectShortUrlForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => claim && claim.short_url);
|
|
|
|
|
|
|
|
export const makeSelectCanonicalUrlForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => claim && claim.canonical_url);
|
|
|
|
|
|
|
|
export const makeSelectPermanentUrlForUri = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => claim && claim.permanent_url);
|
|
|
|
|
|
|
|
export const makeSelectSupportsForUri = (uri: string) =>
|
|
|
|
createSelector(selectSupportsByOutpoint, makeSelectClaimForUri(uri), (byOutpoint, claim: ?StreamClaim) => {
|
|
|
|
if (!claim || !claim.is_my_output) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { claim_id: claimId } = claim;
|
|
|
|
let total = 0;
|
|
|
|
|
|
|
|
Object.values(byOutpoint).forEach((support) => {
|
|
|
|
// $FlowFixMe
|
|
|
|
const { claim_id, amount } = support;
|
|
|
|
total = claim_id === claimId && amount ? total + parseFloat(amount) : total;
|
|
|
|
});
|
|
|
|
|
|
|
|
return total;
|
|
|
|
});
|
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectUpdatingChannel = (state: State) => selectState(state).updatingChannel;
|
|
|
|
export const selectUpdateChannelError = (state: State) => selectState(state).updateChannelError;
|
2021-10-17 10:36:14 +02:00
|
|
|
|
|
|
|
export const makeSelectReflectingClaimForUri = (uri: string) =>
|
|
|
|
createSelector(selectClaimIdsByUri, selectReflectingById, (claimIdsByUri, reflectingById) => {
|
|
|
|
const claimId = claimIdsByUri[normalizeURI(uri)];
|
|
|
|
return reflectingById[claimId];
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectMyStreamUrlsForPage = (page: number = 1) =>
|
|
|
|
createSelector(selectMyClaimUrisWithoutChannels, (urls) => {
|
|
|
|
const start = (Number(page) - 1) * Number(CLAIM.PAGE_SIZE);
|
|
|
|
const end = Number(page) * Number(CLAIM.PAGE_SIZE);
|
|
|
|
|
|
|
|
return urls && urls.length ? urls.slice(start, end) : [];
|
|
|
|
});
|
|
|
|
|
|
|
|
export const selectMyStreamUrlsCount = createSelector(selectMyClaimUrisWithoutChannels, (channels) => channels.length);
|
|
|
|
|
|
|
|
export const makeSelectTagInClaimOrChannelForUri = (uri: string, tag: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
const claimTags = (claim && claim.value && claim.value.tags) || [];
|
|
|
|
const channelTags =
|
|
|
|
(claim && claim.signing_channel && claim.signing_channel.value && claim.signing_channel.value.tags) || [];
|
|
|
|
return claimTags.includes(tag) || channelTags.includes(tag);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectClaimHasSource = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
if (!claim) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Boolean(claim.value.source);
|
|
|
|
});
|
|
|
|
|
|
|
|
export const makeSelectClaimIsStreamPlaceholder = (uri: string) =>
|
|
|
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
|
|
|
if (!claim) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Boolean(claim.value_type === 'stream' && !claim.value.source);
|
|
|
|
});
|
|
|
|
|
2021-11-10 16:30:58 +01:00
|
|
|
export const selectTotalStakedAmountForChannelUri = createCachedSelector(selectClaimForUri, (claim) => {
|
|
|
|
if (!claim || !claim.amount || !claim.meta || !claim.meta.support_amount) {
|
|
|
|
return 0;
|
|
|
|
}
|
2021-10-17 10:36:14 +02:00
|
|
|
|
2021-11-10 16:30:58 +01:00
|
|
|
return parseFloat(claim.amount) + parseFloat(claim.meta.support_amount) || 0;
|
|
|
|
})((state, uri) => String(uri));
|
2021-10-17 10:36:14 +02:00
|
|
|
|
2021-11-10 16:30:58 +01:00
|
|
|
export const selectStakedLevelForChannelUri = createCachedSelector(selectTotalStakedAmountForChannelUri, (amount) => {
|
|
|
|
let level = 1;
|
|
|
|
switch (true) {
|
|
|
|
case amount >= CLAIM.LEVEL_2_STAKED_AMOUNT && amount < CLAIM.LEVEL_3_STAKED_AMOUNT:
|
|
|
|
level = 2;
|
|
|
|
break;
|
|
|
|
case amount >= CLAIM.LEVEL_3_STAKED_AMOUNT && amount < CLAIM.LEVEL_4_STAKED_AMOUNT:
|
|
|
|
level = 3;
|
|
|
|
break;
|
|
|
|
case amount >= CLAIM.LEVEL_4_STAKED_AMOUNT && amount < CLAIM.LEVEL_5_STAKED_AMOUNT:
|
|
|
|
level = 4;
|
|
|
|
break;
|
|
|
|
case amount >= CLAIM.LEVEL_5_STAKED_AMOUNT:
|
|
|
|
level = 5;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return level;
|
|
|
|
})((state, uri) => String(uri));
|
2021-10-17 10:36:14 +02:00
|
|
|
|
2021-10-23 04:41:43 +02:00
|
|
|
export const selectUpdatingCollection = (state: State) => selectState(state).updatingCollection;
|
|
|
|
export const selectUpdateCollectionError = (state: State) => selectState(state).updateCollectionError;
|
|
|
|
export const selectCreatingCollection = (state: State) => selectState(state).creatingCollection;
|
|
|
|
export const selectCreateCollectionError = (state: State) => selectState(state).createCollectionError;
|