diff --git a/dist/bundle.es.js b/dist/bundle.es.js index 867e17b..1f957de 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -134,6 +134,9 @@ const ABANDON_CLAIM_SUCCEEDED = 'ABANDON_CLAIM_SUCCEEDED'; const FETCH_CHANNEL_LIST_STARTED = 'FETCH_CHANNEL_LIST_STARTED'; const FETCH_CHANNEL_LIST_COMPLETED = 'FETCH_CHANNEL_LIST_COMPLETED'; const FETCH_CHANNEL_LIST_FAILED = 'FETCH_CHANNEL_LIST_FAILED'; +const FETCH_COLLECTION_LIST_STARTED = 'FETCH_COLLECTION_LIST_STARTED'; +const FETCH_COLLECTION_LIST_COMPLETED = 'FETCH_COLLECTION_LIST_COMPLETED'; +const FETCH_COLLECTION_LIST_FAILED = 'FETCH_COLLECTION_LIST_FAILED'; const CREATE_CHANNEL_STARTED = 'CREATE_CHANNEL_STARTED'; const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED'; const CREATE_CHANNEL_FAILED = 'CREATE_CHANNEL_FAILED'; @@ -143,6 +146,7 @@ const UPDATE_CHANNEL_FAILED = 'UPDATE_CHANNEL_FAILED'; const IMPORT_CHANNEL_STARTED = 'IMPORT_CHANNEL_STARTED'; const IMPORT_CHANNEL_COMPLETED = 'IMPORT_CHANNEL_COMPLETED'; const IMPORT_CHANNEL_FAILED = 'IMPORT_CHANNEL_FAILED'; +const CLEAR_CHANNEL_ERRORS = 'CLEAR_CHANNEL_ERRORS'; const PUBLISH_STARTED = 'PUBLISH_STARTED'; const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED'; const PUBLISH_FAILED = 'PUBLISH_FAILED'; @@ -161,7 +165,6 @@ const CLAIM_REPOST_STARTED = 'CLAIM_REPOST_STARTED'; const CLAIM_REPOST_COMPLETED = 'CLAIM_REPOST_COMPLETED'; const CLAIM_REPOST_FAILED = 'CLAIM_REPOST_FAILED'; const CLEAR_REPOST_ERROR = 'CLEAR_REPOST_ERROR'; -const CLEAR_CHANNEL_ERRORS = 'CLEAR_CHANNEL_ERRORS'; const CHECK_PUBLISH_NAME_STARTED = 'CHECK_PUBLISH_NAME_STARTED'; const CHECK_PUBLISH_NAME_COMPLETED = 'CHECK_PUBLISH_NAME_COMPLETED'; const UPDATE_PENDING_CLAIMS = 'UPDATE_PENDING_CLAIMS'; @@ -174,6 +177,27 @@ const PURCHASE_LIST_STARTED = 'PURCHASE_LIST_STARTED'; const PURCHASE_LIST_COMPLETED = 'PURCHASE_LIST_COMPLETED'; const PURCHASE_LIST_FAILED = 'PURCHASE_LIST_FAILED'; +const COLLECTION_PUBLISH_STARTED = 'COLLECTION_PUBLISH_STARTED'; +const COLLECTION_PUBLISH_COMPLETED = 'COLLECTION_PUBLISH_COMPLETED'; +const COLLECTION_PUBLISH_FAILED = 'COLLECTION_PUBLISH_FAILED'; +const COLLECTION_PUBLISH_UPDATE_STARTED = 'COLLECTION_PUBLISH_UPDATE_STARTED'; +const COLLECTION_PUBLISH_UPDATE_COMPLETED = 'COLLECTION_PUBLISH_UPDATE_COMPLETED'; +const COLLECTION_PUBLISH_UPDATE_FAILED = 'COLLECTION_PUBLISH_UPDATE_FAILED'; +const COLLECTION_PUBLISH_ABANDON_STARTED = 'COLLECTION_PUBLISH_ABANDON_STARTED'; +const COLLECTION_PUBLISH_ABANDON_COMPLETED = 'COLLECTION_PUBLISH_ABANDON_COMPLETED'; +const COLLECTION_PUBLISH_ABANDON_FAILED = 'COLLECTION_PUBLISH_ABANDON_FAILED'; +const CLEAR_COLLECTION_ERRORS = 'CLEAR_COLLECTION_ERRORS'; +const COLLECTION_ITEMS_RESOLVE_STARTED = 'COLLECTION_ITEMS_RESOLVE_STARTED'; +const COLLECTION_ITEMS_RESOLVE_COMPLETED = 'COLLECTION_ITEMS_RESOLVE_COMPLETED'; +const COLLECTION_ITEMS_RESOLVE_FAILED = 'COLLECTION_ITEMS_RESOLVE_FAILED'; +const COLLECTION_NEW = 'COLLECTION_NEW'; +const COLLECTION_DELETE = 'COLLECTION_DELETE'; +const COLLECTION_PENDING = 'COLLECTION_PENDING'; +const COLLECTION_EDIT = 'COLLECTION_EDIT'; +const COLLECTION_COPY = 'COLLECTION_COPY'; +const COLLECTION_SAVE = 'COLLECTION_SAVE'; +const COLLECTION_ERROR = 'COLLECTION_ERROR'; + // Comments const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED'; const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED'; @@ -409,6 +433,9 @@ var action_types = /*#__PURE__*/Object.freeze({ FETCH_CHANNEL_LIST_STARTED: FETCH_CHANNEL_LIST_STARTED, FETCH_CHANNEL_LIST_COMPLETED: FETCH_CHANNEL_LIST_COMPLETED, FETCH_CHANNEL_LIST_FAILED: FETCH_CHANNEL_LIST_FAILED, + FETCH_COLLECTION_LIST_STARTED: FETCH_COLLECTION_LIST_STARTED, + FETCH_COLLECTION_LIST_COMPLETED: FETCH_COLLECTION_LIST_COMPLETED, + FETCH_COLLECTION_LIST_FAILED: FETCH_COLLECTION_LIST_FAILED, CREATE_CHANNEL_STARTED: CREATE_CHANNEL_STARTED, CREATE_CHANNEL_COMPLETED: CREATE_CHANNEL_COMPLETED, CREATE_CHANNEL_FAILED: CREATE_CHANNEL_FAILED, @@ -418,6 +445,7 @@ var action_types = /*#__PURE__*/Object.freeze({ IMPORT_CHANNEL_STARTED: IMPORT_CHANNEL_STARTED, IMPORT_CHANNEL_COMPLETED: IMPORT_CHANNEL_COMPLETED, IMPORT_CHANNEL_FAILED: IMPORT_CHANNEL_FAILED, + CLEAR_CHANNEL_ERRORS: CLEAR_CHANNEL_ERRORS, PUBLISH_STARTED: PUBLISH_STARTED, PUBLISH_COMPLETED: PUBLISH_COMPLETED, PUBLISH_FAILED: PUBLISH_FAILED, @@ -436,7 +464,6 @@ var action_types = /*#__PURE__*/Object.freeze({ CLAIM_REPOST_COMPLETED: CLAIM_REPOST_COMPLETED, CLAIM_REPOST_FAILED: CLAIM_REPOST_FAILED, CLEAR_REPOST_ERROR: CLEAR_REPOST_ERROR, - CLEAR_CHANNEL_ERRORS: CLEAR_CHANNEL_ERRORS, CHECK_PUBLISH_NAME_STARTED: CHECK_PUBLISH_NAME_STARTED, CHECK_PUBLISH_NAME_COMPLETED: CHECK_PUBLISH_NAME_COMPLETED, UPDATE_PENDING_CLAIMS: UPDATE_PENDING_CLAIMS, @@ -448,6 +475,26 @@ var action_types = /*#__PURE__*/Object.freeze({ PURCHASE_LIST_STARTED: PURCHASE_LIST_STARTED, PURCHASE_LIST_COMPLETED: PURCHASE_LIST_COMPLETED, PURCHASE_LIST_FAILED: PURCHASE_LIST_FAILED, + COLLECTION_PUBLISH_STARTED: COLLECTION_PUBLISH_STARTED, + COLLECTION_PUBLISH_COMPLETED: COLLECTION_PUBLISH_COMPLETED, + COLLECTION_PUBLISH_FAILED: COLLECTION_PUBLISH_FAILED, + COLLECTION_PUBLISH_UPDATE_STARTED: COLLECTION_PUBLISH_UPDATE_STARTED, + COLLECTION_PUBLISH_UPDATE_COMPLETED: COLLECTION_PUBLISH_UPDATE_COMPLETED, + COLLECTION_PUBLISH_UPDATE_FAILED: COLLECTION_PUBLISH_UPDATE_FAILED, + COLLECTION_PUBLISH_ABANDON_STARTED: COLLECTION_PUBLISH_ABANDON_STARTED, + COLLECTION_PUBLISH_ABANDON_COMPLETED: COLLECTION_PUBLISH_ABANDON_COMPLETED, + COLLECTION_PUBLISH_ABANDON_FAILED: COLLECTION_PUBLISH_ABANDON_FAILED, + CLEAR_COLLECTION_ERRORS: CLEAR_COLLECTION_ERRORS, + COLLECTION_ITEMS_RESOLVE_STARTED: COLLECTION_ITEMS_RESOLVE_STARTED, + COLLECTION_ITEMS_RESOLVE_COMPLETED: COLLECTION_ITEMS_RESOLVE_COMPLETED, + COLLECTION_ITEMS_RESOLVE_FAILED: COLLECTION_ITEMS_RESOLVE_FAILED, + COLLECTION_NEW: COLLECTION_NEW, + COLLECTION_DELETE: COLLECTION_DELETE, + COLLECTION_PENDING: COLLECTION_PENDING, + COLLECTION_EDIT: COLLECTION_EDIT, + COLLECTION_COPY: COLLECTION_COPY, + COLLECTION_SAVE: COLLECTION_SAVE, + COLLECTION_ERROR: COLLECTION_ERROR, COMMENT_LIST_STARTED: COMMENT_LIST_STARTED, COMMENT_LIST_COMPLETED: COMMENT_LIST_COMPLETED, COMMENT_LIST_FAILED: COMMENT_LIST_FAILED, @@ -993,6 +1040,28 @@ var shared_preferences = /*#__PURE__*/Object.freeze({ CLIENT_SYNC_KEYS: CLIENT_SYNC_KEYS }); +const COLLECTION_ID = 'lid'; +const COLLECTION_INDEX = 'linx'; + +const COL_TYPE_PLAYLIST = 'playlist'; +const COL_TYPE_CHANNELS = 'channelList'; + +const WATCH_LATER_ID = 'watchlater'; +const FAVORITES_ID = 'favorites'; +const FAVORITE_CHANNELS_ID = 'favoriteChannels'; +const BUILTIN_LISTS = [WATCH_LATER_ID, FAVORITES_ID, FAVORITE_CHANNELS_ID]; + +var collections = /*#__PURE__*/Object.freeze({ + COLLECTION_ID: COLLECTION_ID, + COLLECTION_INDEX: COLLECTION_INDEX, + COL_TYPE_PLAYLIST: COL_TYPE_PLAYLIST, + COL_TYPE_CHANNELS: COL_TYPE_CHANNELS, + WATCH_LATER_ID: WATCH_LATER_ID, + FAVORITES_ID: FAVORITES_ID, + FAVORITE_CHANNELS_ID: FAVORITE_CHANNELS_ID, + BUILTIN_LISTS: BUILTIN_LISTS +}); + const DEFAULT_FOLLOWED_TAGS = ['art', 'automotive', 'blockchain', 'comedy', 'economics', 'education', 'gaming', 'music', 'news', 'science', 'sports', 'technology']; const MATURE_TAGS = ['porn', 'porno', 'nsfw', 'mature', 'xxx', 'sex', 'creampie', 'blowjob', 'handjob', 'vagina', 'boobs', 'big boobs', 'big dick', 'pussy', 'cumshot', 'anal', 'hard fucking', 'ass', 'fuck', 'hentai']; @@ -1085,6 +1154,10 @@ const Lbry = { support_create: params => daemonCallWithResult('support_create', params), support_list: params => daemonCallWithResult('support_list', params), stream_repost: params => daemonCallWithResult('stream_repost', params), + collection_resolve: params => daemonCallWithResult('collection_resolve', params), + collection_list: params => daemonCallWithResult('collection_list', params), + collection_create: params => daemonCallWithResult('collection_create', params), + collection_update: params => daemonCallWithResult('collection_update', params), // File fetching and manipulation file_list: (params = {}) => daemonCallWithResult('file_list', params), @@ -1755,10 +1828,13 @@ function extractUserState(rawObj) { coin_swap_codes, settings, app_welcome_version, - sharing_3P + sharing_3P, + unpublishedCollections, + builtinCollections, + savedCollections } = rawObj.value; - return _extends$1({}, subscriptions ? { subscriptions } : {}, following ? { following } : {}, tags ? { tags } : {}, blocked ? { blocked } : {}, coin_swap_codes ? { coin_swap_codes } : {}, settings ? { settings } : {}, app_welcome_version ? { app_welcome_version } : {}, sharing_3P ? { sharing_3P } : {}); + return _extends$1({}, subscriptions ? { subscriptions } : {}, following ? { following } : {}, tags ? { tags } : {}, blocked ? { blocked } : {}, coin_swap_codes ? { coin_swap_codes } : {}, settings ? { settings } : {}, app_welcome_version ? { app_welcome_version } : {}, sharing_3P ? { sharing_3P } : {}, unpublishedCollections ? { unpublishedCollections } : {}, builtinCollections ? { builtinCollections } : {}, savedCollections ? { savedCollections } : {}); } return {}; @@ -1774,7 +1850,10 @@ function doPopulateSharedUserState(sharedSettings) { coin_swap_codes, settings, app_welcome_version, - sharing_3P + sharing_3P, + unpublishedCollections, + builtinCollections, + savedCollections } = extractUserState(sharedSettings); dispatch({ type: USER_STATE_POPULATE, @@ -1786,7 +1865,10 @@ function doPopulateSharedUserState(sharedSettings) { coinSwapCodes: coin_swap_codes, settings, welcomeVersion: app_welcome_version, - allowAnalytics: sharing_3P + allowAnalytics: sharing_3P, + unpublishedCollections, + builtinCollections, + savedCollections } }); }; @@ -1867,7 +1949,6 @@ const buildSharedStateMiddleware = (actions, sharedStateFilters, sharedStateCb) clearTimeout(timeout); const actionResult = next(action); // Call `getState` after calling `next` to ensure the state has updated in response to the action - function runPreferences() { const nextState = getState(); const syncEnabled = nextState.settings && nextState.settings.clientSettings && nextState.settings.clientSettings.enable_sync; @@ -2328,6 +2409,8 @@ const makeSelectClaimIsPending = uri => reselect.createSelector(selectClaimIdsBy return false; }); +const makeSelectClaimIdForUri = uri => reselect.createSelector(selectClaimIdsByUri, claimIds => claimIds[uri]); + const selectReflectingById = reselect.createSelector(selectState$1, state => state.reflectingById); const makeSelectClaimForClaimId = claimId => reselect.createSelector(selectClaimsById, byId => byId[claimId]); @@ -2568,6 +2651,8 @@ const selectMyClaimsOutpoints = reselect.createSelector(selectMyClaims, myClaims const selectFetchingMyChannels = reselect.createSelector(selectState$1, state => state.fetchingMyChannels); +const selectFetchingMyCollections = reselect.createSelector(selectState$1, state => state.fetchingMyCollections); + const selectMyChannelClaims = reselect.createSelector(selectState$1, selectClaimsById, (state, byId) => { const ids = state.myChannelClaims; if (!ids) { @@ -2587,6 +2672,8 @@ const selectMyChannelClaims = reselect.createSelector(selectState$1, selectClaim const selectMyChannelUrls = reselect.createSelector(selectMyChannelClaims, claims => claims ? claims.map(claim => claim.canonical_url || claim.permanent_url) : undefined); +const selectMyCollectionIds = reselect.createSelector(selectState$1, state => state.myCollectionClaims); + const selectResolvingUris = reselect.createSelector(selectState$1, state => state.resolvingUris || []); const selectChannelImportPending = reselect.createSelector(selectState$1, state => state.pendingChannelImport); @@ -2802,6 +2889,14 @@ const makeSelectStakedLevelForChannelUri = uri => reselect.createSelector(makeSe return level; }); +const selectUpdatingCollection = reselect.createSelector(selectState$1, state => state.updatingCollection); + +const selectUpdateCollectionError = reselect.createSelector(selectState$1, state => state.updateCollectionError); + +const selectCreatingCollection = reselect.createSelector(selectState$1, state => state.creatingCollection); + +const selectCreateCollectionError = reselect.createSelector(selectState$1, state => state.createCollectionError); + function numberWithCommas(x) { var parts = x.toString().split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); @@ -3505,6 +3600,132 @@ function batchActions(...actions) { }; } +// + +const selectState$2 = state => state.collections; + +const selectSavedCollectionIds = reselect.createSelector(selectState$2, collectionState => collectionState.saved); + +const selectBuiltinCollections = reselect.createSelector(selectState$2, state => state.builtin); +const selectResolvedCollections = reselect.createSelector(selectState$2, state => state.resolved); + +const selectMyUnpublishedCollections = reselect.createSelector(selectState$2, state => state.unpublished); + +const selectMyEditedCollections = reselect.createSelector(selectState$2, state => state.edited); + +const selectPendingCollections = reselect.createSelector(selectState$2, state => state.pending); + +const makeSelectEditedCollectionForId = id => reselect.createSelector(selectMyEditedCollections, eLists => eLists[id]); + +const makeSelectPendingCollectionForId = id => reselect.createSelector(selectPendingCollections, pending => pending[id]); + +const makeSelectPublishedCollectionForId = id => reselect.createSelector(selectResolvedCollections, rLists => rLists[id]); + +const makeSelectUnpublishedCollectionForId = id => reselect.createSelector(selectMyUnpublishedCollections, rLists => rLists[id]); + +const makeSelectCollectionIsMine = id => reselect.createSelector(selectMyCollectionIds, selectMyUnpublishedCollections, selectBuiltinCollections, (publicIds, privateIds, builtinIds) => { + return Boolean(publicIds.includes(id) || privateIds[id] || builtinIds[id]); +}); + +const selectMyPublishedCollections = reselect.createSelector(selectResolvedCollections, selectPendingCollections, selectMyEditedCollections, selectMyCollectionIds, (resolved, pending, edited, myIds) => { + // all resolved in myIds, plus those in pending and edited + const myPublishedCollections = Object.fromEntries(Object.entries(pending).concat(Object.entries(resolved).filter(([key, val]) => myIds.includes(key) && + // $FlowFixMe + !pending[key]))); + // now add in edited: + Object.entries(edited).forEach(([id, item]) => { + myPublishedCollections[id] = item; + }); + return myPublishedCollections; +}); + +const selectMyPublishedMixedCollections = reselect.createSelector(selectMyPublishedCollections, published => { + const myCollections = Object.fromEntries( + // $FlowFixMe + Object.entries(published).filter(([key, collection]) => { + // $FlowFixMe + return collection.type === 'collection'; + })); + return myCollections; +}); + +const selectMyPublishedPlaylistCollections = reselect.createSelector(selectMyPublishedCollections, published => { + const myCollections = Object.fromEntries( + // $FlowFixMe + Object.entries(published).filter(([key, collection]) => { + // $FlowFixMe + return collection.type === 'playlist'; + })); + return myCollections; +}); + +const makeSelectMyPublishedCollectionForId = id => reselect.createSelector(selectMyPublishedCollections, myPublishedCollections => myPublishedCollections[id]); + +// export const selectSavedCollections = createSelector( +// selectResolvedCollections, +// selectSavedCollectionIds, +// (resolved, myIds) => { +// const mySavedCollections = Object.fromEntries( +// Object.entries(resolved).filter(([key, val]) => myIds.includes(key)) +// ); +// return mySavedCollections; +// } +// ); + +const makeSelectIsResolvingCollectionForId = id => reselect.createSelector(selectState$2, state => { + return state.isResolvingCollectionById[id]; +}); + +const makeSelectCollectionForId = id => reselect.createSelector(selectBuiltinCollections, selectResolvedCollections, selectMyUnpublishedCollections, selectMyEditedCollections, selectPendingCollections, (bLists, rLists, uLists, eLists, pLists) => { + const collection = bLists[id] || uLists[id] || eLists[id] || rLists[id] || pLists[id]; + return collection; +}); + +const makeSelectCollectionForIdHasClaimUrl = (id, url) => reselect.createSelector(makeSelectCollectionForId(id), collection => collection && collection.items.includes(url)); + +const makeSelectUrlsForCollectionId = id => reselect.createSelector(makeSelectCollectionForId(id), collection => collection && collection.items); + +const makeSelectClaimIdsForCollectionId = id => reselect.createSelector(makeSelectCollectionForId(id), collection => { + const items = collection && collection.items || []; + const ids = items.map(item => { + const { claimId } = parseURI(item); + return claimId; + }); + return ids; +}); + +const makeSelectIndexForUrlInCollection = (url, id) => reselect.createSelector(makeSelectUrlsForCollectionId(id), urls => { + const index = urls && urls.findIndex(u => u === url); + if (index > -1) { + return index; + } + return null; +}); + +const makeSelectNextUrlForCollectionAndUrl = (id, url) => reselect.createSelector(makeSelectIndexForUrlInCollection(url, id), makeSelectUrlsForCollectionId(id), (index, urls) => { + if (urls && index >= -1) { + const url = urls[index + 1]; + if (url) { + return url; + } + } + return null; +}); + +const makeSelectNameForCollectionId = id => reselect.createSelector(makeSelectCollectionForId(id), collection => { + return collection && collection.name || ''; +}); + +const makeSelectCountForCollectionId = id => reselect.createSelector(makeSelectCollectionForId(id), collection => { + if (collection) { + if (collection.itemCount !== undefined) { + return collection.itemCount; + } + return collection.items.length; + } + return null; +}); + var _extends$5 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; function _asyncToGenerator$1(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } @@ -3542,6 +3763,8 @@ function doResolveUris(uris, returnCachedClaims = false, resolveReposts = true) const resolveInfo = {}; + const collectionIds = []; + return lbryProxy.resolve(_extends$5({ urls: urisToResolve }, options)).then((() => { var _ref = _asyncToGenerator$1(function* (result) { let repostedResults = {}; @@ -3558,6 +3781,7 @@ function doResolveUris(uris, returnCachedClaims = false, resolveReposts = true) // https://github.com/facebook/flow/issues/2221 if (uriResolveInfo) { if (uriResolveInfo.error) { + // $FlowFixMe resolveInfo[uri] = _extends$5({}, fallbackResolveInfo); } else { if (checkReposts) { @@ -3574,6 +3798,10 @@ function doResolveUris(uris, returnCachedClaims = false, resolveReposts = true) result.channel = uriResolveInfo; // $FlowFixMe result.claimsInChannel = uriResolveInfo.meta.claims_in_channel; + } else if (uriResolveInfo.value_type === 'collection') { + result.collection = uriResolveInfo; + // $FlowFixMe + collectionIds.push(uriResolveInfo.claim_id); } else { result.stream = uriResolveInfo; if (uriResolveInfo.signing_channel) { @@ -3602,6 +3830,11 @@ function doResolveUris(uris, returnCachedClaims = false, resolveReposts = true) type: RESOLVE_URIS_COMPLETED, data: { resolveInfo } }); + + if (collectionIds.length) { + dispatch(doFetchItemsInCollections({ collectionIds: collectionIds, pageSize: 5 })); + } + return result; }); @@ -3930,7 +4163,6 @@ function doUpdateChannel(params, cb) { } // we'll need to remove these once we add locations/channels to channel page edit/create options - if (channelClaim && channelClaim.value && channelClaim.value.locations) { updateParams.locations = channelClaim.value.locations; } @@ -3964,7 +4196,7 @@ function doImportChannel(certificate) { type: IMPORT_CHANNEL_STARTED }); - return lbryProxy.channel_import({ channel_data: certificate }).then(result => { + return lbryProxy.channel_import({ channel_data: certificate }).then(() => { dispatch({ type: IMPORT_CHANNEL_COMPLETED }); @@ -4001,6 +4233,35 @@ function doFetchChannelListMine(page = 1, pageSize = 99999, resolve = true) { }; } +function doFetchCollectionListMine(page = 1, pageSize = 99999) { + return dispatch => { + dispatch({ + type: FETCH_COLLECTION_LIST_STARTED + }); + + const callback = response => { + const { items } = response; + dispatch({ + type: FETCH_COLLECTION_LIST_COMPLETED, + data: { claims: items } + }); + dispatch(doFetchItemsInCollections({ + collectionIds: items.map(claim => claim.claim_id), + page_size: 5 + })); + }; + + const failure = error => { + dispatch({ + type: FETCH_COLLECTION_LIST_FAILED, + data: error + }); + }; + + lbryProxy.collection_list({ page, page_size: pageSize, resolve_claims: 1, resolve: true }).then(callback, failure); + }; +} + function doClaimSearch(options = { no_totals: true, page_size: 10, @@ -4032,7 +4293,7 @@ function doClaimSearch(options = { pageSize: options.page_size } }); - return true; + return resolveInfo; }; const failure = function (err) { @@ -4057,7 +4318,6 @@ function doClaimSearch(options = { function doRepost(options) { return dispatch => { - // $FlowFixMe return new Promise(resolve => { dispatch({ type: CLAIM_REPOST_STARTED @@ -4097,6 +4357,139 @@ function doRepost(options) { }; } +function doCollectionPublish(options, localId) { + return dispatch => { + // $FlowFixMe + + const params = { + name: options.name, + bid: creditsToString(options.bid), + title: options.title, + thumbnail_url: options.thumbnail_url, + description: options.description, + tags: [], + languages: options.languages || [], + locations: [], + blocking: true, + claims: options.claims + }; + + if (options.tags) { + params['tags'] = options.tags.map(tag => tag.name); + } + + if (options.channel_id) { + params['channel_id'] = options.channel_id; + } + + return new Promise(resolve => { + dispatch({ + type: COLLECTION_PUBLISH_STARTED + }); + + function success(response) { + const collectionClaim = response.outputs[0]; + dispatch(batchActions({ + type: COLLECTION_PUBLISH_COMPLETED, + data: { claimId: collectionClaim.claim_id } + }, + // move unpublished collection to pending collection with new publish id + // recent publish won't resolve this second. handle it in checkPending + { + type: UPDATE_PENDING_CLAIMS, + data: { + claims: [collectionClaim] + } + })); + dispatch({ + type: COLLECTION_PENDING, + data: { localId: localId, claimId: collectionClaim.claim_id } + }); + dispatch(doCheckPendingClaims()); + dispatch(doFetchCollectionListMine(1, 10)); + return resolve(collectionClaim); + } + + function failure(error) { + dispatch({ + type: COLLECTION_PUBLISH_FAILED, + data: { + error: error.message + } + }); + } + + return lbryProxy.collection_create(params).then(success, failure); + }); + }; +} + +function doCollectionPublishUpdate(options) { + return dispatch => { + // TODO: implement one click update + + const updateParams = { + bid: creditsToString(options.bid), + title: options.title, + thumbnail_url: options.thumbnail_url, + description: options.description, + tags: [], + languages: options.languages || [], + locations: [], + blocking: true, + claim_id: options.claim_id, + clear_claims: true + }; + + if (options.tags) { + updateParams['tags'] = options.tags.map(tag => tag.name); + } + + if (options.claims) { + updateParams['claims'] = options.claims; + } + return new Promise(resolve => { + dispatch({ + type: COLLECTION_PUBLISH_UPDATE_STARTED + }); + + function success(response) { + const collectionClaim = response.outputs[0]; + dispatch({ + type: COLLECTION_PUBLISH_UPDATE_COMPLETED, + data: { + collectionClaim + } + }); + dispatch({ + type: COLLECTION_PENDING, + data: { claimId: collectionClaim.claim_id } + }); + dispatch({ + type: UPDATE_PENDING_CLAIMS, + data: { + claims: [collectionClaim] + } + }); + dispatch(doCheckPendingClaims()); + dispatch(doFetchCollectionListMine(1, 10)); + return resolve(collectionClaim); + } + + function failure(error) { + dispatch({ + type: COLLECTION_PUBLISH_UPDATE_FAILED, + data: { + error: error.message + } + }); + } + + return lbryProxy.collection_update(updateParams).then(success, failure); + }); + }; +} + function doCheckPublishNameAvailability(name) { return dispatch => { dispatch({ @@ -4165,6 +4558,7 @@ const doCheckPendingClaims = onConfirmed => (dispatch, getState) => { const checkClaimList = () => { const state = getState(); const pendingIdSet = new Set(selectPendingIds(state)); + const pendingCollections = selectPendingCollections(state); lbryProxy.claim_list({ page: 1, page_size: 10 }).then(result => { const claims = result.items; const claimsToConfirm = []; @@ -4172,6 +4566,10 @@ const doCheckPendingClaims = onConfirmed => (dispatch, getState) => { const { claim_id: claimId } = claim; if (claim.confirmations > 0 && pendingIdSet.has(claimId)) { pendingIdSet.delete(claimId); + if (Object.keys(pendingCollections).includes(claim.claim_id)) { + dispatch(doFetchItemsInCollection({ collectionId: claim.claim_id })); + dispatch(doCollectionDelete(claim.claim_id, 'pending')); + } claimsToConfirm.push(claim); if (onConfirmed) { onConfirmed(claim); @@ -4199,11 +4597,469 @@ const doCheckPendingClaims = onConfirmed => (dispatch, getState) => { }, 30000); }; -const selectState$2 = state => state.fileInfo || {}; +function _asyncToGenerator$2(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } -const selectFileInfosByOutpoint = reselect.createSelector(selectState$2, state => state.byOutpoint || {}); +const getTimestamp = () => { + return Math.floor(Date.now() / 1000); +}; -const selectIsFetchingFileList = reselect.createSelector(selectState$2, state => state.isFetchingFileList); +const FETCH_BATCH_SIZE = 10; + +const doLocalCollectionCreate = (name, collectionItems, type, sourceId) => dispatch => { + return dispatch({ + type: COLLECTION_NEW, + data: { + entry: { + id: uuid.v4(), // start with a uuid, this becomes a claimId after publish + name: name, + updatedAt: getTimestamp(), + items: collectionItems || [], + sourceId: sourceId, + type: type + } + } + }); +}; + +const doCollectionDelete = (id, colKey = undefined) => (dispatch, getState) => { + const state = getState(); + const claim = makeSelectClaimForClaimId(id)(state); + const collectionDelete = () => dispatch({ + type: COLLECTION_DELETE, + data: { + id: id, + collectionKey: colKey + } + }); + if (claim && !colKey) { + // could support "abandon, but keep" later + const { txid, nout } = claim; + return dispatch(doAbandonClaim(txid, nout, collectionDelete)); + } + return collectionDelete(); +}; + +// Given a collection, save its collectionId to be resolved and displayed in Library +// export const doCollectionSave = ( +// id: string, +// ) => (dispatch: Dispatch) => { +// return dispatch({ +// type: ACTIONS.COLLECTION_SAVE, +// data: { +// id: id, +// }, +// }); +// }; + +// Given a collection and name, copy it to a local private collection with a name +// export const doCollectionCopy = ( +// id: string, +// ) => (dispatch: Dispatch) => { +// return dispatch({ +// type: ACTIONS.COLLECTION_COPY, +// data: { +// id: id, +// }, +// }); +// }; + +const doFetchItemsInCollections = (resolveItemsOptions, resolveStartedCallback) => (() => { + var _ref = _asyncToGenerator$2(function* (dispatch, getState) { + let fetchItemsForCollectionClaim = (() => { + var _ref2 = _asyncToGenerator$2(function* (claim, pageSize) { + const totalItems = claim.value.claims && claim.value.claims.length; + const claimId = claim.claim_id; + const itemOrder = claim.value.claims; + + const sortResults = function (items, claimList) { + const newItems = []; + claimList.forEach(function (id) { + const index = items.findIndex(function (i) { + return i.claim_id === id; + }); + if (index >= 0) { + newItems.push(items[index]); + } + }); + /* + This will return newItems[] of length less than total_items below + if one or more of the claims has been abandoned. That's ok for now. + */ + return newItems; + }; + + const mergeBatches = function (arrayOfResults, claimList) { + const mergedResults = { + items: [], + total_items: 0 + }; + arrayOfResults.forEach(function (result) { + mergedResults.items = mergedResults.items.concat(result.items); + mergedResults.total_items = result.total_items; + }); + + mergedResults.items = sortResults(mergedResults.items, claimList); + return mergedResults; + }; + + try { + const batchSize = pageSize || FETCH_BATCH_SIZE; + const batches = []; + + for (let i = 0; i < Math.ceil(totalItems / batchSize); i++) { + batches[i] = lbryProxy.claim_search({ + claim_ids: claim.value.claims, + page: i + 1, + page_size: batchSize + }); + } + const itemsInBatches = yield Promise.all(batches); + const result = mergeBatches(itemsInBatches, itemOrder); + + // $FlowFixMe + const itemsById = { claimId: claimId }; + if (result.items) { + itemsById.items = result.items; + } else { + itemsById.items = null; + } + return itemsById; + } catch (e) { + return { + claimId: claimId, + items: null + }; + } + }); + + return function fetchItemsForCollectionClaim(_x3, _x4) { + return _ref2.apply(this, arguments); + }; + })(); + + /* + 1) make sure all the collection claims are loaded into claims reducer, search/resolve if necessary. + 2) get the item claims for each + 3) format and make sure they're in the order as in the claim + 4) Build the collection objects and update collections reducer + 5) Update redux claims reducer + */ + let state = getState(); + const { collectionIds, pageSize } = resolveItemsOptions; + + dispatch({ + type: COLLECTION_ITEMS_RESOLVE_STARTED, + data: { ids: collectionIds } + }); + + if (resolveStartedCallback) resolveStartedCallback(); + + const collectionIdsToSearch = collectionIds.filter(function (claimId) { + return !state.claims.byId[claimId]; + }); + + if (collectionIdsToSearch.length) { + yield dispatch(doClaimSearch({ claim_ids: collectionIdsToSearch, page: 1, page_size: 9999 })); + } + + const stateAfterClaimSearch = getState(); + + function formatForClaimActions(resultClaimsByUri) { + const formattedClaims = {}; + Object.entries(resultClaimsByUri).forEach(([uri, uriResolveInfo]) => { + // Flow has terrible Object.entries support + // https://github.com/facebook/flow/issues/2221 + if (uriResolveInfo) { + let result = {}; + if (uriResolveInfo.value_type === 'channel') { + result.channel = uriResolveInfo; + // $FlowFixMe + result.claimsInChannel = uriResolveInfo.meta.claims_in_channel; + // ALSO SKIP COLLECTIONS + } else if (uriResolveInfo.value_type === 'collection') { + result.collection = uriResolveInfo; + } else { + result.stream = uriResolveInfo; + if (uriResolveInfo.signing_channel) { + result.channel = uriResolveInfo.signing_channel; + result.claimsInChannel = uriResolveInfo.signing_channel.meta && uriResolveInfo.signing_channel.meta.claims_in_channel || 0; + } + } + // $FlowFixMe + formattedClaims[uri] = result; + } + }); + return formattedClaims; + } + + const invalidCollectionIds = []; + const promisedCollectionItemFetches = []; + collectionIds.forEach(function (collectionId) { + const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch); + if (!claim) { + invalidCollectionIds.push(collectionId); + } else { + promisedCollectionItemFetches.push(fetchItemsForCollectionClaim(claim, pageSize)); + } + }); + + // $FlowFixMe + const collectionItemsById = yield Promise.all(promisedCollectionItemFetches); + + const newCollectionObjectsById = {}; + const resolvedItemsByUrl = {}; + collectionItemsById.forEach(function (entry) { + // $FlowFixMe + const collectionItems = entry.items; + const collectionId = entry.claimId; + if (collectionItems) { + const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch); + + const editedCollection = makeSelectEditedCollectionForId(collectionId)(stateAfterClaimSearch); + const { name, timestamp, value } = claim || {}; + const { title } = value; + const valueTypes = new Set(); + const streamTypes = new Set(); + + let newItems = []; + let isPlaylist; + + if (collectionItems) { + collectionItems.forEach(function (collectionItem) { + newItems.push(collectionItem.permanent_url); + valueTypes.add(collectionItem.value_type); + if (collectionItem.value.stream_type) { + streamTypes.add(collectionItem.value.stream_type); + } + resolvedItemsByUrl[collectionItem.canonical_url] = collectionItem; + }); + isPlaylist = valueTypes.size === 1 && valueTypes.has('stream') && (streamTypes.size === 1 && (streamTypes.has('audio') || streamTypes.has('video')) || streamTypes.size === 2 && streamTypes.has('audio') && streamTypes.has('video')); + } + + newCollectionObjectsById[collectionId] = { + items: newItems, + id: collectionId, + name: title || name, + itemCount: claim.value.claims.length, + type: isPlaylist ? 'playlist' : 'collection', + updatedAt: timestamp + }; + + if (editedCollection && timestamp > editedCollection['updatedAt']) { + dispatch({ + type: COLLECTION_DELETE, + data: { + id: collectionId, + collectionKey: 'edited' + } + }); + } + } else { + invalidCollectionIds.push(collectionId); + } + }); + const formattedClaimsByUri = formatForClaimActions(collectionItemsById); + + dispatch({ + type: RESOLVE_URIS_COMPLETED, + data: { resolveInfo: formattedClaimsByUri } + }); + + dispatch({ + type: COLLECTION_ITEMS_RESOLVE_COMPLETED, + data: { + resolvedCollections: newCollectionObjectsById, + failedCollectionIds: invalidCollectionIds + } + }); + }); + + return function (_x, _x2) { + return _ref.apply(this, arguments); + }; +})(); + +const doFetchItemsInCollection = (options, cb) => { + const { collectionId, pageSize } = options; + const newOptions = { + collectionIds: [collectionId] + }; + if (pageSize) newOptions.pageSize = pageSize; + return doFetchItemsInCollections(newOptions, cb); +}; + +const doCollectionEdit = (collectionId, params) => (() => { + var _ref3 = _asyncToGenerator$2(function* (dispatch, getState) { + const state = getState(); + const collection = makeSelectCollectionForId(collectionId)(state); + const editedCollection = makeSelectEditedCollectionForId(collectionId)(state); + const unpublishedCollection = makeSelectUnpublishedCollectionForId(collectionId)(state); + const publishedCollection = makeSelectPublishedCollectionForId(collectionId)(state); // needs to be published only + + const generateCollectionItemsFromSearchResult = function (results) { + return Object.values(results) + // $FlowFixMe + .reduce(function (acc, cur) { + let url; + if (cur.stream) { + url = cur.stream.permanent_url; + } else if (cur.channel) { + url = cur.channel.permanent_url; + } else if (cur.collection) { + url = cur.collection.permanent_url; + } else { + return acc; + } + acc.push(url); + return acc; + }, []); + }; + + if (!collection) { + return dispatch({ + type: COLLECTION_ERROR, + data: { + message: 'collection does not exist' + } + }); + } + + let currentItems = collection.items ? collection.items.concat() : []; + const { claims: passedClaims, order, claimIds, replace, remove, type } = params; + + const collectionType = type || collection.type; + let newItems = currentItems; + + if (passedClaims) { + if (remove) { + const passedUrls = passedClaims.map(function (claim) { + return claim.permanent_url; + }); + // $FlowFixMe // need this? + newItems = currentItems.filter(function (item) { + return !passedUrls.includes(item); + }); + } else { + passedClaims.forEach(function (claim) { + return newItems.push(claim.permanent_url); + }); + } + } + + if (claimIds) { + const batches = []; + if (claimIds.length > 50) { + for (let i = 0; i < Math.ceil(claimIds.length / 50); i++) { + batches[i] = claimIds.slice(i * 50, (i + 1) * 50); + } + } else { + batches[0] = claimIds; + } + const resultArray = yield Promise.all(batches.map(function (batch) { + let options = { claim_ids: batch, page: 1, page_size: 50 }; + return dispatch(doClaimSearch(options)); + })); + + const searchResults = Object.assign({}, ...resultArray); + + if (replace) { + newItems = generateCollectionItemsFromSearchResult(searchResults); + } else { + newItems = currentItems.concat(generateCollectionItemsFromSearchResult(searchResults)); + } + } + + if (order) { + const [movedItem] = currentItems.splice(order.from, 1); + currentItems.splice(order.to, 0, movedItem); + } + + // console.log('p&e', publishedCollection.items, newItems, publishedCollection.items.join(','), newItems.join(',')) + if (editedCollection) { + // delete edited if newItems are the same as publishedItems + if (publishedCollection.items.join(',') === newItems.join(',')) { + dispatch({ + type: COLLECTION_DELETE, + data: { + id: collectionId, + collectionKey: 'edited' + } + }); + } else { + dispatch({ + type: COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'edited', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType + } + } + }); + } + } else if (publishedCollection) { + dispatch({ + type: COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'edited', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType + } + } + }); + } else if (BUILTIN_LISTS.includes(collectionId)) { + dispatch({ + type: COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'builtin', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType + } + } + }); + } else if (unpublishedCollection) { + dispatch({ + type: COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'unpublished', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType + } + } + }); + } + return true; + }); + + return function (_x5, _x6) { + return _ref3.apply(this, arguments); + }; +})(); + +const selectState$3 = state => state.fileInfo || {}; + +const selectFileInfosByOutpoint = reselect.createSelector(selectState$3, state => state.byOutpoint || {}); + +const selectIsFetchingFileList = reselect.createSelector(selectState$3, state => state.isFetchingFileList); const selectIsFetchingFileListDownloadedOrPublished = reselect.createSelector(selectIsFetchingFileList, selectIsFetchingClaimListMine, (isFetchingFileList, isFetchingClaimListMine) => isFetchingFileList || isFetchingClaimListMine); @@ -4213,14 +5069,14 @@ const makeSelectFileInfoForUri = uri => reselect.createSelector(selectClaimsByUr return outpoint ? byOutpoint[outpoint] : undefined; }); -const selectDownloadingByOutpoint = reselect.createSelector(selectState$2, state => state.downloadingByOutpoint || {}); +const selectDownloadingByOutpoint = reselect.createSelector(selectState$3, state => state.downloadingByOutpoint || {}); const makeSelectDownloadingForUri = uri => reselect.createSelector(selectDownloadingByOutpoint, makeSelectFileInfoForUri(uri), (byOutpoint, fileInfo) => { if (!fileInfo) return false; return byOutpoint[fileInfo.outpoint]; }); -const selectUrisLoading = reselect.createSelector(selectState$2, state => state.fetching || {}); +const selectUrisLoading = reselect.createSelector(selectState$3, state => state.fetching || {}); const makeSelectLoadingForUri = uri => reselect.createSelector(selectUrisLoading, makeSelectClaimForUri(uri), (fetchingByOutpoint, claim) => { if (!claim) { @@ -4274,9 +5130,9 @@ const selectTotalDownloadProgress = reselect.createSelector(selectDownloadingFil return -1; }); -const selectFileListPublishedSort = reselect.createSelector(selectState$2, state => state.fileListPublishedSort); +const selectFileListPublishedSort = reselect.createSelector(selectState$3, state => state.fileListPublishedSort); -const selectFileListDownloadedSort = reselect.createSelector(selectState$2, state => state.fileListDownloadedSort); +const selectFileListDownloadedSort = reselect.createSelector(selectState$3, state => state.fileListDownloadedSort); const selectDownloadedUris = reselect.createSelector(selectFileInfosDownloaded, // We should use permament_url but it doesn't exist in file_list @@ -4529,10 +5385,10 @@ var _extends$6 = Object.assign || function (target) { for (var i = 1; i < argume function _objectWithoutProperties$2(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } -const selectState$3 = state => state.publish || {}; +const selectState$4 = state => state.publish || {}; // Is the current uri the same as the uri they clicked "edit" on -const selectIsStillEditing = reselect.createSelector(selectState$3, publishState => { +const selectIsStillEditing = reselect.createSelector(selectState$4, publishState => { const { editingURI, uri } = publishState; if (!editingURI || !uri) { @@ -4557,7 +5413,7 @@ const selectIsStillEditing = reselect.createSelector(selectState$3, publishState return currentName === editName; }); -const selectPublishFormValues = reselect.createSelector(selectState$3, state => state.settings, selectIsStillEditing, (publishState, settingsState, isStillEditing) => { +const selectPublishFormValues = reselect.createSelector(selectState$4, state => state.settings, selectIsStillEditing, (publishState, settingsState, isStillEditing) => { const { pendingPublish, language } = publishState, formValues = _objectWithoutProperties$2(publishState, ['pendingPublish', 'language']); const { clientSettings } = settingsState; @@ -4573,7 +5429,7 @@ const selectPublishFormValues = reselect.createSelector(selectState$3, state => return _extends$6({}, formValues, { language: actualLanguage }); }); -const makeSelectPublishFormValue = item => reselect.createSelector(selectState$3, state => state[item]); +const makeSelectPublishFormValue = item => reselect.createSelector(selectState$4, state => state[item]); const selectMyClaimForUri = reselect.createSelector(selectPublishFormValues, selectIsStillEditing, selectClaimsById, selectMyClaimsWithoutChannels, ({ editingURI, uri }, isStillEditing, claimsById, myClaims) => { const { channelName: contentName, streamName: claimName } = parseURI(uri); @@ -4586,7 +5442,7 @@ const selectMyClaimForUri = reselect.createSelector(selectPublishFormValues, sel return isStillEditing ? claimsById[editClaimId] : myClaims.find(claim => !contentName ? claim.name === claimName : claim.name === contentName || claim.name === claimName); }); -const selectIsResolvingPublishUris = reselect.createSelector(selectState$3, selectResolvingUris, ({ uri, name }, resolvingUris) => { +const selectIsResolvingPublishUris = reselect.createSelector(selectState$4, selectResolvingUris, ({ uri, name }, resolvingUris) => { if (uri) { const isResolvingUri = resolvingUris.includes(uri); const { isChannel } = parseURI(uri); @@ -4603,7 +5459,7 @@ const selectIsResolvingPublishUris = reselect.createSelector(selectState$3, sele return false; }); -const selectTakeOverAmount = reselect.createSelector(selectState$3, selectMyClaimForUri, selectClaimsByUri, ({ name }, myClaimForUri, claimsByUri) => { +const selectTakeOverAmount = reselect.createSelector(selectState$4, selectMyClaimForUri, selectClaimsByUri, ({ name }, myClaimForUri, claimsByUri) => { if (!name) { return null; } @@ -4628,14 +5484,13 @@ const selectTakeOverAmount = reselect.createSelector(selectState$3, selectMyClai var _extends$7 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -function _asyncToGenerator$2(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } +function _asyncToGenerator$3(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } const doResetThumbnailStatus = () => dispatch => { dispatch({ type: UPDATE_PUBLISH_FORM, data: { - thumbnailPath: '', - thumbnailError: undefined + thumbnailPath: '' } }); @@ -4692,13 +5547,6 @@ const doUploadThumbnail = (filePath, thumbnailBlob, fsAdapter, fs, path) => disp }, doError(error))); }; - dispatch({ - type: UPDATE_PUBLISH_FORM, - data: { - thumbnailError: undefined - } - }); - const doUpload = data => { return fetch(SPEECH_PUBLISH, { method: 'POST', @@ -4794,8 +5642,7 @@ const doPrepareEdit = (claim, uri, fileInfo, fs) => dispatch => { description, fee, languages, - releaseTime: release_time, - releaseTimeEdited: undefined, + release_time: release_time ? Number(release_time) * 1000 : undefined, thumbnail: thumbnail ? thumbnail.url : null, title, uri, @@ -4846,7 +5693,7 @@ const doPublish = (success, fail, preview) => (dispatch, getState) => { filePath, description, language, - releaseTimeEdited, + releaseTime, license, licenseUrl, useLBRYUploader, @@ -4918,8 +5765,8 @@ const doPublish = (success, fail, preview) => (dispatch, getState) => { } // Set release time to curret date. On edits, keep original release/transaction time as release_time - if (releaseTimeEdited) { - publishPayload.release_time = releaseTimeEdited; + if (releaseTime) { + publishPayload.release_time = Number(Math.round(new Date(releaseTime) / 1000)); } else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) { publishPayload.release_time = Number(myClaimForUri.value.release_time); } else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) { @@ -4982,7 +5829,7 @@ const doCheckReflectingFiles = () => (dispatch, getState) => { let reflectorCheckInterval; const checkFileList = (() => { - var _ref = _asyncToGenerator$2(function* () { + var _ref = _asyncToGenerator$3(function* () { const state = getState(); const reflectingById = selectReflectingById(state); const ids = Object.keys(reflectingById); @@ -5115,6 +5962,7 @@ const defaultState = { fetchingChannelClaims: {}, resolvingUris: [], myChannelClaims: undefined, + myCollectionClaims: [], myClaims: undefined, myPurchases: undefined, myPurchasesPageNumber: undefined, @@ -5123,6 +5971,7 @@ const defaultState = { fetchingMyPurchases: false, fetchingMyPurchasesError: undefined, fetchingMyChannels: false, + fetchingMyCollections: false, abandoningById: {}, pendingIds: [], reflectingById: {}, @@ -5131,9 +5980,13 @@ const defaultState = { claimSearchByQueryLastPageReached: {}, fetchingClaimSearchByQuery: {}, updateChannelError: '', + updateCollectionError: '', updatingChannel: false, creatingChannel: false, createChannelError: undefined, + updatingCollection: false, + creatingCollection: false, + createCollectionError: undefined, pendingChannelImport: false, repostLoading: false, repostError: undefined, @@ -5149,9 +6002,7 @@ const defaultState = { }; function handleClaimAction(state, action) { - const { - resolveInfo - } = action.data; + const { resolveInfo } = action.data; const byUri = Object.assign({}, state.claimsByUri); const byId = Object.assign({}, state.byId); @@ -5162,7 +6013,7 @@ function handleClaimAction(state, action) { Object.entries(resolveInfo).forEach(([url, resolveResponse]) => { // $FlowFixMe - const { claimsInChannel, stream, channel: channelFromResolve } = resolveResponse; + const { claimsInChannel, stream, channel: channelFromResolve, collection } = resolveResponse; const channel = channelFromResolve || stream && stream.signing_channel; if (stream) { @@ -5201,15 +6052,32 @@ function handleClaimAction(state, action) { } else { byId[channel.claim_id] = channel; } - // Also add the permanent_url here until lighthouse returns canonical_url for search results + byUri[channel.permanent_url] = channel.claim_id; byUri[channel.canonical_url] = channel.claim_id; newResolvingUrls.delete(channel.canonical_url); newResolvingUrls.delete(channel.permanent_url); } + if (collection) { + if (pendingIds.includes(collection.claim_id)) { + byId[collection.claim_id] = mergeClaims(collection, byId[collection.claim_id]); + } else { + byId[collection.claim_id] = collection; + } + byUri[url] = collection.claim_id; + byUri[collection.canonical_url] = collection.claim_id; + byUri[collection.permanent_url] = collection.claim_id; + newResolvingUrls.delete(collection.canonical_url); + newResolvingUrls.delete(collection.permanent_url); + + if (collection.is_my_output) { + myClaimIds.add(collection.claim_id); + } + } + newResolvingUrls.delete(url); - if (!stream && !channel && !pendingIds.includes(byUri[url])) { + if (!stream && !channel && !collection && !pendingIds.includes(byUri[url])) { byUri[url] = null; } }); @@ -5344,6 +6212,50 @@ reducers[FETCH_CHANNEL_LIST_FAILED] = (state, action) => { }); }; +reducers[FETCH_COLLECTION_LIST_STARTED] = state => _extends$9({}, state, { + fetchingMyCollections: true +}); + +reducers[FETCH_COLLECTION_LIST_COMPLETED] = (state, action) => { + const { claims } = action.data; + const myClaims = state.myClaims || []; + let myClaimIds = new Set(myClaims); + const pendingIds = state.pendingIds || []; + let myCollectionClaimsSet = new Set([]); + const byId = Object.assign({}, state.byId); + const byUri = Object.assign({}, state.claimsByUri); + + if (claims.length) { + myCollectionClaimsSet = new Set(state.myCollectionClaims); + claims.forEach(claim => { + const { canonical_url: canonicalUrl, permanent_url: permanentUrl, claim_id: claimId } = claim; + + byUri[canonicalUrl] = claimId; + byUri[permanentUrl] = claimId; + + // $FlowFixMe + myCollectionClaimsSet.add(claimId); + // we don't want to overwrite a pending result with a resolve + if (!pendingIds.some(c => c === claimId)) { + byId[claimId] = claim; + } + myClaimIds.add(claimId); + }); + } + + return _extends$9({}, state, { + byId, + claimsByUri: byUri, + fetchingMyCollections: false, + myCollectionClaims: Array.from(myCollectionClaimsSet), + myClaims: myClaimIds ? Array.from(myClaimIds) : null + }); +}; + +reducers[FETCH_COLLECTION_LIST_FAILED] = state => { + return _extends$9({}, state, { fetchingMyCollections: false }); +}; + reducers[FETCH_CHANNEL_CLAIMS_STARTED] = (state, action) => { const { uri, page } = action.data; const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims); @@ -5486,6 +6398,7 @@ reducers[ABANDON_CLAIM_SUCCEEDED] = (state, action) => { const newMyClaims = state.myClaims ? state.myClaims.slice() : []; const newMyChannelClaims = state.myChannelClaims ? state.myChannelClaims.slice() : []; const claimsByUri = Object.assign({}, state.claimsByUri); + const newMyCollectionClaims = state.myCollectionClaims ? state.myCollectionClaims.slice() : []; Object.keys(claimsByUri).forEach(uri => { if (claimsByUri[uri] === claimId) { @@ -5494,12 +6407,14 @@ reducers[ABANDON_CLAIM_SUCCEEDED] = (state, action) => { }); const myClaims = newMyClaims.filter(i => i !== claimId); const myChannelClaims = newMyChannelClaims.filter(i => i !== claimId); + const myCollectionClaims = newMyCollectionClaims.filter(i => i !== claimId); delete byId[claimId]; return Object.assign({}, state, { myClaims, myChannelClaims, + myCollectionClaims, byId, claimsByUri }); @@ -5549,6 +6464,59 @@ reducers[UPDATE_CHANNEL_FAILED] = (state, action) => { }); }; +reducers[CLEAR_COLLECTION_ERRORS] = state => _extends$9({}, state, { + createCollectionError: null, + updateCollectionError: null +}); + +reducers[COLLECTION_PUBLISH_STARTED] = state => _extends$9({}, state, { + creatingCollection: true, + createCollectionError: null +}); + +reducers[COLLECTION_PUBLISH_COMPLETED] = (state, action) => { + const myCollections = state.myCollectionClaims || []; + const myClaims = state.myClaims || []; + const { claimId } = action.data; + let myClaimIds = new Set(myClaims); + let myCollectionClaimsSet = new Set(myCollections); + myClaimIds.add(claimId); + myCollectionClaimsSet.add(claimId); + return Object.assign({}, state, { + creatingCollection: false, + myClaims: Array.from(myClaimIds), + myCollectionClaims: Array.from(myCollectionClaimsSet) + }); +}; + +reducers[COLLECTION_PUBLISH_FAILED] = (state, action) => { + return Object.assign({}, state, { + creatingCollection: false, + createCollectionError: action.data.error + }); +}; + +reducers[COLLECTION_PUBLISH_UPDATE_STARTED] = (state, action) => { + return Object.assign({}, state, { + updateCollectionError: '', + updatingCollection: true + }); +}; + +reducers[COLLECTION_PUBLISH_UPDATE_COMPLETED] = (state, action) => { + return Object.assign({}, state, { + updateCollectionError: '', + updatingCollection: false + }); +}; + +reducers[COLLECTION_PUBLISH_UPDATE_FAILED] = (state, action) => { + return Object.assign({}, state, { + updateCollectionError: action.data.error, + updatingCollection: false + }); +}; + reducers[IMPORT_CHANNEL_STARTED] = state => Object.assign({}, state, { pendingChannelImports: true }); reducers[IMPORT_CHANNEL_COMPLETED] = state => Object.assign({}, state, { pendingChannelImports: false }); @@ -6065,11 +7033,9 @@ const defaultState$4 = { thumbnail_url: '', thumbnailPath: '', uploadThumbnailStatus: API_DOWN, - thumbnailError: undefined, description: '', language: '', releaseTime: undefined, - releaseTimeEdited: undefined, nsfw: false, channel: CHANNEL_ANONYMOUS, channelId: '', @@ -6576,11 +7542,216 @@ const walletReducer = handleActions({ }) }, defaultState$5); +var _extends$e = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +const getTimestamp$1 = () => { + return Math.floor(Date.now() / 1000); +}; + +const defaultState$6 = { + builtin: { + watchlater: { + items: [], + id: WATCH_LATER_ID, + name: 'Watch Later', + updatedAt: getTimestamp$1(), + type: COL_TYPE_PLAYLIST + }, + favorites: { + items: [], + id: FAVORITES_ID, + name: 'Favorites', + type: COL_TYPE_PLAYLIST, + updatedAt: getTimestamp$1() + } + }, + resolved: {}, + unpublished: {}, // sync + edited: {}, + pending: {}, + saved: [], + isResolvingCollectionById: {}, + error: null +}; + +const collectionsReducer = handleActions({ + [COLLECTION_NEW]: (state, action) => { + const { entry: params } = action.data; // { id:, items: Array} + // entry + const newListTemplate = { + id: params.id, + name: params.name, + items: [], + updatedAt: getTimestamp$1(), + type: params.type + }; + + const newList = Object.assign({}, newListTemplate, _extends$e({}, params)); + const { unpublished: lists } = state; + const newLists = Object.assign({}, lists, { [params.id]: newList }); + + return _extends$e({}, state, { + unpublished: newLists + }); + }, + + [COLLECTION_DELETE]: (state, action) => { + const { id, collectionKey } = action.data; + const { edited: editList, unpublished: unpublishedList, pending: pendingList } = state; + const newEditList = Object.assign({}, editList); + const newUnpublishedList = Object.assign({}, unpublishedList); + + const newPendingList = Object.assign({}, pendingList); + + if (collectionKey && state[collectionKey] && state[collectionKey][id]) { + const newList = Object.assign({}, state[collectionKey]); + delete newList[id]; + return _extends$e({}, state, { + [collectionKey]: newList + }); + } else { + if (newEditList[id]) { + delete newEditList[id]; + } else if (newUnpublishedList[id]) { + delete newUnpublishedList[id]; + } else if (newPendingList[id]) { + delete newPendingList[id]; + } + } + return _extends$e({}, state, { + edited: newEditList, + unpublished: newUnpublishedList, + pending: newPendingList + }); + }, + + [COLLECTION_PENDING]: (state, action) => { + const { localId, claimId } = action.data; + const { edited: editList, unpublished: unpublishedList, pending: pendingList } = state; + const newEditList = Object.assign({}, editList); + const newUnpublishedList = Object.assign({}, unpublishedList); + const newPendingList = Object.assign({}, pendingList); + + if (localId) { + // new publish + newPendingList[claimId] = Object.assign({}, newUnpublishedList[localId] || {}); + delete newUnpublishedList[localId]; + } else { + // edit update + newPendingList[claimId] = Object.assign({}, newEditList[claimId]); + delete newEditList[claimId]; + } + + return _extends$e({}, state, { + edited: newEditList, + unpublished: newUnpublishedList, + pending: newPendingList + }); + }, + + [COLLECTION_EDIT]: (state, action) => { + const { id, collectionKey, collection } = action.data; + + if (BUILTIN_LISTS.includes(id)) { + const { builtin: lists } = state; + return _extends$e({}, state, { + [collectionKey]: _extends$e({}, lists, { [id]: collection }) + }); + } + + if (collectionKey === 'edited') { + const { edited: lists } = state; + return _extends$e({}, state, { + edited: _extends$e({}, lists, { [id]: collection }) + }); + } + const { unpublished: lists } = state; + return _extends$e({}, state, { + unpublished: _extends$e({}, lists, { [id]: collection }) + }); + }, + + [COLLECTION_ERROR]: (state, action) => { + return Object.assign({}, state, { + error: action.data.message + }); + }, + + [COLLECTION_ITEMS_RESOLVE_STARTED]: (state, action) => { + const { ids } = action.data; + const { isResolvingCollectionById } = state; + const newResolving = Object.assign({}, isResolvingCollectionById); + ids.forEach(id => { + newResolving[id] = true; + }); + return Object.assign({}, state, _extends$e({}, state, { + error: '', + isResolvingCollectionById: newResolving + })); + }, + [USER_STATE_POPULATE]: (state, action) => { + const { builtinCollections, savedCollections, unpublishedCollections } = action.data; + return _extends$e({}, state, { + unpublished: unpublishedCollections || state.unpublished, + builtin: builtinCollections || state.builtin, + saved: savedCollections || state.saved + }); + }, + [COLLECTION_ITEMS_RESOLVE_COMPLETED]: (state, action) => { + const { resolvedCollections, failedCollectionIds } = action.data; + const { pending, edited, isResolvingCollectionById, resolved } = state; + const newPending = Object.assign({}, pending); + const newEdited = Object.assign({}, edited); + const newResolved = Object.assign({}, resolved, resolvedCollections); + + const resolvedIds = Object.keys(resolvedCollections); + const newResolving = Object.assign({}, isResolvingCollectionById); + if (resolvedCollections && Object.keys(resolvedCollections).length) { + resolvedIds.forEach(resolvedId => { + if (newEdited[resolvedId]) { + if (newEdited[resolvedId]['updatedAt'] < resolvedCollections[resolvedId]['updatedAt']) { + delete newEdited[resolvedId]; + } + } + delete newResolving[resolvedId]; + if (newPending[resolvedId]) { + delete newPending[resolvedId]; + } + }); + } + + if (failedCollectionIds && Object.keys(failedCollectionIds).length) { + failedCollectionIds.forEach(failedId => { + delete newResolving[failedId]; + }); + } + + return Object.assign({}, state, _extends$e({}, state, { + pending: newPending, + resolved: newResolved, + edited: newEdited, + isResolvingCollectionById: newResolving + })); + }, + [COLLECTION_ITEMS_RESOLVE_FAILED]: (state, action) => { + const { ids } = action.data; + const { isResolvingCollectionById } = state; + const newResolving = Object.assign({}, isResolvingCollectionById); + ids.forEach(id => { + delete newResolving[id]; + }); + return Object.assign({}, state, _extends$e({}, state, { + isResolvingCollectionById: newResolving, + error: action.data.message + })); + } +}, defaultState$6); + // -const selectState$4 = state => state.content || {}; +const selectState$5 = state => state.content || {}; -const makeSelectContentPositionForUri = uri => reselect.createSelector(selectState$4, makeSelectClaimForUri(uri), (state, claim) => { +const makeSelectContentPositionForUri = uri => reselect.createSelector(selectState$5, makeSelectClaimForUri(uri), (state, claim) => { if (!claim) { return null; } @@ -6589,14 +7760,14 @@ const makeSelectContentPositionForUri = uri => reselect.createSelector(selectSta return state.positions[id] ? state.positions[id][outpoint] : null; }); -var _extends$e = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$f = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -const selectState$5 = state => state.notifications || {}; +const selectState$6 = state => state.notifications || {}; -const selectToast = reselect.createSelector(selectState$5, state => { +const selectToast = reselect.createSelector(selectState$6, state => { if (state.toasts.length) { const { id, params } = state.toasts[0]; - return _extends$e({ + return _extends$f({ id }, params); } @@ -6604,7 +7775,7 @@ const selectToast = reselect.createSelector(selectState$5, state => { return null; }); -const selectError = reselect.createSelector(selectState$5, state => { +const selectError = reselect.createSelector(selectState$6, state => { if (state.errors.length) { const { error } = state.errors[0]; return { @@ -6618,6 +7789,7 @@ const selectError = reselect.createSelector(selectState$5, state => { exports.ABANDON_STATES = abandon_states; exports.ACTIONS = action_types; exports.CLAIM_VALUES = claim; +exports.COLLECTIONS_CONSTS = collections; exports.DAEMON_SETTINGS = daemon_settings; exports.DEFAULT_FOLLOWED_TAGS = DEFAULT_FOLLOWED_TAGS; exports.DEFAULT_KNOWN_TAGS = DEFAULT_KNOWN_TAGS; @@ -6639,6 +7811,7 @@ exports.batchActions = batchActions; exports.buildSharedStateMiddleware = buildSharedStateMiddleware; exports.buildURI = buildURI; exports.claimsReducer = claimsReducer; +exports.collectionsReducer = collectionsReducer; exports.contentReducer = contentReducer; exports.convertToShareLink = convertToShareLink; exports.createNormalizedClaimSearchKey = createNormalizedClaimSearchKey; @@ -6656,6 +7829,10 @@ exports.doClearPublish = doClearPublish; exports.doClearPurchasedUriSuccess = doClearPurchasedUriSuccess; exports.doClearRepostError = doClearRepostError; exports.doClearSupport = doClearSupport; +exports.doCollectionDelete = doCollectionDelete; +exports.doCollectionEdit = doCollectionEdit; +exports.doCollectionPublish = doCollectionPublish; +exports.doCollectionPublishUpdate = doCollectionPublishUpdate; exports.doCreateChannel = doCreateChannel; exports.doDismissError = doDismissError; exports.doDismissToast = doDismissToast; @@ -6663,8 +7840,11 @@ exports.doError = doError; exports.doFetchChannelListMine = doFetchChannelListMine; exports.doFetchClaimListMine = doFetchClaimListMine; exports.doFetchClaimsByChannel = doFetchClaimsByChannel; +exports.doFetchCollectionListMine = doFetchCollectionListMine; exports.doFetchFileInfo = doFetchFileInfo; exports.doFetchFileInfos = doFetchFileInfos; +exports.doFetchItemsInCollection = doFetchItemsInCollection; +exports.doFetchItemsInCollections = doFetchItemsInCollections; exports.doFetchTransactions = doFetchTransactions; exports.doFetchTxoPage = doFetchTxoPage; exports.doFetchUtxoCounts = doFetchUtxoCounts; @@ -6672,6 +7852,7 @@ exports.doFileGet = doFileGet; exports.doFileList = doFileList; exports.doGetNewAddress = doGetNewAddress; exports.doImportChannel = doImportChannel; +exports.doLocalCollectionCreate = doLocalCollectionCreate; exports.doPopulateSharedUserState = doPopulateSharedUserState; exports.doPreferenceGet = doPreferenceGet; exports.doPreferenceSet = doPreferenceSet; @@ -6719,6 +7900,8 @@ exports.makeSelectChannelPermUrlForClaimUri = makeSelectChannelPermUrlForClaimUr exports.makeSelectClaimForClaimId = makeSelectClaimForClaimId; exports.makeSelectClaimForUri = makeSelectClaimForUri; exports.makeSelectClaimHasSource = makeSelectClaimHasSource; +exports.makeSelectClaimIdForUri = makeSelectClaimIdForUri; +exports.makeSelectClaimIdsForCollectionId = makeSelectClaimIdsForCollectionId; exports.makeSelectClaimIsMine = makeSelectClaimIsMine; exports.makeSelectClaimIsNsfw = makeSelectClaimIsNsfw; exports.makeSelectClaimIsPending = makeSelectClaimIsPending; @@ -6726,19 +7909,26 @@ exports.makeSelectClaimIsStreamPlaceholder = makeSelectClaimIsStreamPlaceholder; exports.makeSelectClaimWasPurchased = makeSelectClaimWasPurchased; exports.makeSelectClaimsInChannelForCurrentPageState = makeSelectClaimsInChannelForCurrentPageState; exports.makeSelectClaimsInChannelForPage = makeSelectClaimsInChannelForPage; +exports.makeSelectCollectionForId = makeSelectCollectionForId; +exports.makeSelectCollectionForIdHasClaimUrl = makeSelectCollectionForIdHasClaimUrl; +exports.makeSelectCollectionIsMine = makeSelectCollectionIsMine; exports.makeSelectContentPositionForUri = makeSelectContentPositionForUri; exports.makeSelectContentTypeForUri = makeSelectContentTypeForUri; +exports.makeSelectCountForCollectionId = makeSelectCountForCollectionId; exports.makeSelectCoverForUri = makeSelectCoverForUri; exports.makeSelectDateForUri = makeSelectDateForUri; exports.makeSelectDownloadPathForUri = makeSelectDownloadPathForUri; exports.makeSelectDownloadingForUri = makeSelectDownloadingForUri; +exports.makeSelectEditedCollectionForId = makeSelectEditedCollectionForId; exports.makeSelectEffectiveAmountForUri = makeSelectEffectiveAmountForUri; exports.makeSelectFetchingChannelClaims = makeSelectFetchingChannelClaims; exports.makeSelectFileInfoForUri = makeSelectFileInfoForUri; exports.makeSelectFileNameForUri = makeSelectFileNameForUri; exports.makeSelectFilePartlyDownloaded = makeSelectFilePartlyDownloaded; exports.makeSelectFilteredTransactionsForPage = makeSelectFilteredTransactionsForPage; +exports.makeSelectIndexForUrlInCollection = makeSelectIndexForUrlInCollection; exports.makeSelectIsAbandoningClaimForUri = makeSelectIsAbandoningClaimForUri; +exports.makeSelectIsResolvingCollectionForId = makeSelectIsResolvingCollectionForId; exports.makeSelectIsUriResolving = makeSelectIsUriResolving; exports.makeSelectLatestTransactions = makeSelectLatestTransactions; exports.makeSelectLoadingForUri = makeSelectLoadingForUri; @@ -6746,15 +7936,20 @@ exports.makeSelectMediaTypeForUri = makeSelectMediaTypeForUri; exports.makeSelectMetadataForUri = makeSelectMetadataForUri; exports.makeSelectMetadataItemForUri = makeSelectMetadataItemForUri; exports.makeSelectMyChannelPermUrlForName = makeSelectMyChannelPermUrlForName; +exports.makeSelectMyPublishedCollectionForId = makeSelectMyPublishedCollectionForId; exports.makeSelectMyPurchasesForPage = makeSelectMyPurchasesForPage; exports.makeSelectMyStreamUrlsForPage = makeSelectMyStreamUrlsForPage; +exports.makeSelectNameForCollectionId = makeSelectNameForCollectionId; +exports.makeSelectNextUrlForCollectionAndUrl = makeSelectNextUrlForCollectionAndUrl; exports.makeSelectNsfwCountForChannel = makeSelectNsfwCountForChannel; exports.makeSelectNsfwCountFromUris = makeSelectNsfwCountFromUris; exports.makeSelectOmittedCountForChannel = makeSelectOmittedCountForChannel; exports.makeSelectPendingAmountByUri = makeSelectPendingAmountByUri; exports.makeSelectPendingClaimForUri = makeSelectPendingClaimForUri; +exports.makeSelectPendingCollectionForId = makeSelectPendingCollectionForId; exports.makeSelectPermanentUrlForUri = makeSelectPermanentUrlForUri; exports.makeSelectPublishFormValue = makeSelectPublishFormValue; +exports.makeSelectPublishedCollectionForId = makeSelectPublishedCollectionForId; exports.makeSelectReflectingClaimForUri = makeSelectReflectingClaimForUri; exports.makeSelectSearchDownloadUrlsCount = makeSelectSearchDownloadUrlsCount; exports.makeSelectSearchDownloadUrlsForPage = makeSelectSearchDownloadUrlsForPage; @@ -6771,7 +7966,9 @@ exports.makeSelectTotalItemsForChannel = makeSelectTotalItemsForChannel; exports.makeSelectTotalPagesForChannel = makeSelectTotalPagesForChannel; exports.makeSelectTotalPagesInChannelSearch = makeSelectTotalPagesInChannelSearch; exports.makeSelectTotalStakedAmountForChannelUri = makeSelectTotalStakedAmountForChannelUri; +exports.makeSelectUnpublishedCollectionForId = makeSelectUnpublishedCollectionForId; exports.makeSelectUriIsStreamable = makeSelectUriIsStreamable; +exports.makeSelectUrlsForCollectionId = makeSelectUrlsForCollectionId; exports.normalizeURI = normalizeURI; exports.notificationsReducer = notificationsReducer; exports.parseQueryParams = parseQueryParams; @@ -6787,6 +7984,7 @@ exports.selectAllFetchingChannelClaims = selectAllFetchingChannelClaims; exports.selectAllMyClaimsByOutpoint = selectAllMyClaimsByOutpoint; exports.selectBalance = selectBalance; exports.selectBlocks = selectBlocks; +exports.selectBuiltinCollections = selectBuiltinCollections; exports.selectChannelClaimCounts = selectChannelClaimCounts; exports.selectChannelImportPending = selectChannelImportPending; exports.selectClaimIdsByUri = selectClaimIdsByUri; @@ -6796,7 +7994,9 @@ exports.selectClaimsBalance = selectClaimsBalance; exports.selectClaimsById = selectClaimsById; exports.selectClaimsByUri = selectClaimsByUri; exports.selectCreateChannelError = selectCreateChannelError; +exports.selectCreateCollectionError = selectCreateCollectionError; exports.selectCreatingChannel = selectCreatingChannel; +exports.selectCreatingCollection = selectCreatingCollection; exports.selectCurrentChannelPage = selectCurrentChannelPage; exports.selectDownloadUrlsCount = selectDownloadUrlsCount; exports.selectDownloadedUris = selectDownloadedUris; @@ -6811,6 +8011,7 @@ exports.selectFetchingClaimSearch = selectFetchingClaimSearch; exports.selectFetchingClaimSearchByQuery = selectFetchingClaimSearchByQuery; exports.selectFetchingMyChannels = selectFetchingMyChannels; exports.selectFetchingMyClaimsPageError = selectFetchingMyClaimsPageError; +exports.selectFetchingMyCollections = selectFetchingMyCollections; exports.selectFetchingMyPurchasesError = selectFetchingMyPurchasesError; exports.selectFetchingTxosError = selectFetchingTxosError; exports.selectFileInfosByOutpoint = selectFileInfosByOutpoint; @@ -6846,9 +8047,15 @@ exports.selectMyClaimsPageItemCount = selectMyClaimsPageItemCount; exports.selectMyClaimsPageNumber = selectMyClaimsPageNumber; exports.selectMyClaimsRaw = selectMyClaimsRaw; exports.selectMyClaimsWithoutChannels = selectMyClaimsWithoutChannels; +exports.selectMyCollectionIds = selectMyCollectionIds; +exports.selectMyEditedCollections = selectMyEditedCollections; +exports.selectMyPublishedCollections = selectMyPublishedCollections; +exports.selectMyPublishedMixedCollections = selectMyPublishedMixedCollections; +exports.selectMyPublishedPlaylistCollections = selectMyPublishedPlaylistCollections; exports.selectMyPurchases = selectMyPurchases; exports.selectMyPurchasesCount = selectMyPurchasesCount; exports.selectMyStreamUrlsCount = selectMyStreamUrlsCount; +exports.selectMyUnpublishedCollections = selectMyUnpublishedCollections; exports.selectPendingClaims = selectPendingClaims; exports.selectPendingConsolidateTxid = selectPendingConsolidateTxid; exports.selectPendingIds = selectPendingIds; @@ -6864,7 +8071,9 @@ exports.selectReflectingById = selectReflectingById; exports.selectRepostError = selectRepostError; exports.selectRepostLoading = selectRepostLoading; exports.selectReservedBalance = selectReservedBalance; +exports.selectResolvedCollections = selectResolvedCollections; exports.selectResolvingUris = selectResolvingUris; +exports.selectSavedCollectionIds = selectSavedCollectionIds; exports.selectSupportsBalance = selectSupportsBalance; exports.selectSupportsByOutpoint = selectSupportsByOutpoint; exports.selectTakeOverAmount = selectTakeOverAmount; @@ -6882,7 +8091,9 @@ exports.selectTxoPage = selectTxoPage; exports.selectTxoPageNumber = selectTxoPageNumber; exports.selectTxoPageParams = selectTxoPageParams; exports.selectUpdateChannelError = selectUpdateChannelError; +exports.selectUpdateCollectionError = selectUpdateCollectionError; exports.selectUpdatingChannel = selectUpdatingChannel; +exports.selectUpdatingCollection = selectUpdatingCollection; exports.selectUrisLoading = selectUrisLoading; exports.selectUtxoCounts = selectUtxoCounts; exports.selectWalletDecryptPending = selectWalletDecryptPending; diff --git a/dist/flow-typed/Claim.js b/dist/flow-typed/Claim.js index 655a4bc..e03eeae 100644 --- a/dist/flow-typed/Claim.js +++ b/dist/flow-typed/Claim.js @@ -1,11 +1,15 @@ // @flow -declare type Claim = StreamClaim | ChannelClaim; +declare type Claim = StreamClaim | ChannelClaim | CollectionClaim; declare type ChannelClaim = GenericClaim & { value: ChannelMetadata, }; +declare type CollectionClaim = GenericClaim & { + value: CollectionMetadata, +}; + declare type StreamClaim = GenericClaim & { value: StreamMetadata, }; @@ -30,7 +34,7 @@ declare type GenericClaim = { short_url: string, // permanent_url with short id, no channel txid: string, // unique tx id type: 'claim' | 'update' | 'support', - value_type: 'stream' | 'channel', + value_type: 'stream' | 'channel' | 'collection', signing_channel?: ChannelClaim, reposted_claim?: GenericClaim, repost_channel_url?: string, @@ -74,6 +78,10 @@ declare type ChannelMetadata = GenericMetadata & { featured?: Array, }; +declare type CollectionMetadata = GenericMetadata & { + claims: Array, +} + declare type StreamMetadata = GenericMetadata & { license?: string, // License "title" ex: Creative Commons, Custom copyright license_url?: string, // Link to full license @@ -136,3 +144,71 @@ declare type PurchaseReceipt = { txid: string, type: 'purchase', }; + +declare type ClaimActionResolveInfo = { + [string]: { + stream: ?StreamClaim, + channel: ?ChannelClaim, + claimsInChannel: ?number, + collection: ?CollectionClaim, + }, +} + +declare type ChannelUpdateParams = { + claim_id: string, + bid?: string, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + replace?: boolean, + languages?: Array, + locations?: Array, + blocking?: boolean, +} + +declare type ChannelPublishParams = { + name: string, + bid: string, + blocking?: true, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + languages?: Array, +} + +declare type CollectionUpdateParams = { + claim_id: string, + claim_ids?: Array, + bid?: string, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + replace?: boolean, + languages?: Array, + locations?: Array, + blocking?: boolean, +} + +declare type CollectionPublishParams = { + name: string, + bid: string, + claim_ids: Array, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + tags?: Array, + languages?: Array, +} diff --git a/dist/flow-typed/Collections.js b/dist/flow-typed/Collections.js new file mode 100644 index 0000000..f70825a --- /dev/null +++ b/dist/flow-typed/Collections.js @@ -0,0 +1,34 @@ +declare type Collection = { + id: string, + items: Array, + name: string, + type: string, + updatedAt: number, + totalItems?: number, + sourceId?: string, // if copied, claimId of original collection +}; + +declare type CollectionState = { + unpublished: CollectionGroup, + resolved: CollectionGroup, + pending: CollectionGroup, + edited: CollectionGroup, + builtin: CollectionGroup, + saved: Array, + isResolvingCollectionById: { [string]: boolean }, + error?: string | null, +}; + +declare type CollectionGroup = { + [string]: Collection, +} + +declare type CollectionEditParams = { + claims?: Array, + remove?: boolean, + claimIds?: Array, + replace?: boolean, + order?: { from: number, to: number }, + type?: string, + name?: string, +} diff --git a/dist/flow-typed/Lbry.js b/dist/flow-typed/Lbry.js index 0225fa2..7284455 100644 --- a/dist/flow-typed/Lbry.js +++ b/dist/flow-typed/Lbry.js @@ -170,6 +170,37 @@ declare type ChannelSignResponse = { signing_ts: string, }; +declare type CollectionCreateResponse = { + outputs: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +} + +declare type CollectionListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type CollectionResolveResponse = { + items: Array, + total_items: number, +}; + +declare type CollectionResolveOptions = { + claim_id: string, +}; + +declare type CollectionListOptions = { + page: number, + page_size: number, + resolve?: boolean, +}; + declare type FileListResponse = { items: Array, page: number, @@ -288,6 +319,10 @@ declare type LbryTypes = { support_abandon: (params: {}) => Promise, stream_repost: (params: StreamRepostOptions) => Promise, purchase_list: (params: PurchaseListOptions) => Promise, + collection_resolve: (params: CollectionResolveOptions) => Promise, + collection_list: (params: CollectionListOptions) => Promise, + collection_create: (params: {}) => Promise, + collection_update: (params: {}) => Promise, // File fetching and manipulation file_list: (params: {}) => Promise, diff --git a/flow-typed/Claim.js b/flow-typed/Claim.js index 655a4bc..e03eeae 100644 --- a/flow-typed/Claim.js +++ b/flow-typed/Claim.js @@ -1,11 +1,15 @@ // @flow -declare type Claim = StreamClaim | ChannelClaim; +declare type Claim = StreamClaim | ChannelClaim | CollectionClaim; declare type ChannelClaim = GenericClaim & { value: ChannelMetadata, }; +declare type CollectionClaim = GenericClaim & { + value: CollectionMetadata, +}; + declare type StreamClaim = GenericClaim & { value: StreamMetadata, }; @@ -30,7 +34,7 @@ declare type GenericClaim = { short_url: string, // permanent_url with short id, no channel txid: string, // unique tx id type: 'claim' | 'update' | 'support', - value_type: 'stream' | 'channel', + value_type: 'stream' | 'channel' | 'collection', signing_channel?: ChannelClaim, reposted_claim?: GenericClaim, repost_channel_url?: string, @@ -74,6 +78,10 @@ declare type ChannelMetadata = GenericMetadata & { featured?: Array, }; +declare type CollectionMetadata = GenericMetadata & { + claims: Array, +} + declare type StreamMetadata = GenericMetadata & { license?: string, // License "title" ex: Creative Commons, Custom copyright license_url?: string, // Link to full license @@ -136,3 +144,71 @@ declare type PurchaseReceipt = { txid: string, type: 'purchase', }; + +declare type ClaimActionResolveInfo = { + [string]: { + stream: ?StreamClaim, + channel: ?ChannelClaim, + claimsInChannel: ?number, + collection: ?CollectionClaim, + }, +} + +declare type ChannelUpdateParams = { + claim_id: string, + bid?: string, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + replace?: boolean, + languages?: Array, + locations?: Array, + blocking?: boolean, +} + +declare type ChannelPublishParams = { + name: string, + bid: string, + blocking?: true, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + languages?: Array, +} + +declare type CollectionUpdateParams = { + claim_id: string, + claim_ids?: Array, + bid?: string, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + replace?: boolean, + languages?: Array, + locations?: Array, + blocking?: boolean, +} + +declare type CollectionPublishParams = { + name: string, + bid: string, + claim_ids: Array, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + tags?: Array, + languages?: Array, +} diff --git a/flow-typed/Collections.js b/flow-typed/Collections.js new file mode 100644 index 0000000..f70825a --- /dev/null +++ b/flow-typed/Collections.js @@ -0,0 +1,34 @@ +declare type Collection = { + id: string, + items: Array, + name: string, + type: string, + updatedAt: number, + totalItems?: number, + sourceId?: string, // if copied, claimId of original collection +}; + +declare type CollectionState = { + unpublished: CollectionGroup, + resolved: CollectionGroup, + pending: CollectionGroup, + edited: CollectionGroup, + builtin: CollectionGroup, + saved: Array, + isResolvingCollectionById: { [string]: boolean }, + error?: string | null, +}; + +declare type CollectionGroup = { + [string]: Collection, +} + +declare type CollectionEditParams = { + claims?: Array, + remove?: boolean, + claimIds?: Array, + replace?: boolean, + order?: { from: number, to: number }, + type?: string, + name?: string, +} diff --git a/flow-typed/Lbry.js b/flow-typed/Lbry.js index 0225fa2..7284455 100644 --- a/flow-typed/Lbry.js +++ b/flow-typed/Lbry.js @@ -170,6 +170,37 @@ declare type ChannelSignResponse = { signing_ts: string, }; +declare type CollectionCreateResponse = { + outputs: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +} + +declare type CollectionListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type CollectionResolveResponse = { + items: Array, + total_items: number, +}; + +declare type CollectionResolveOptions = { + claim_id: string, +}; + +declare type CollectionListOptions = { + page: number, + page_size: number, + resolve?: boolean, +}; + declare type FileListResponse = { items: Array, page: number, @@ -288,6 +319,10 @@ declare type LbryTypes = { support_abandon: (params: {}) => Promise, stream_repost: (params: StreamRepostOptions) => Promise, purchase_list: (params: PurchaseListOptions) => Promise, + collection_resolve: (params: CollectionResolveOptions) => Promise, + collection_list: (params: CollectionListOptions) => Promise, + collection_create: (params: {}) => Promise, + collection_update: (params: {}) => Promise, // File fetching and manipulation file_list: (params: {}) => Promise, diff --git a/src/constants/action_types.js b/src/constants/action_types.js index 5e3f672..bf16da2 100644 --- a/src/constants/action_types.js +++ b/src/constants/action_types.js @@ -102,6 +102,9 @@ export const ABANDON_CLAIM_SUCCEEDED = 'ABANDON_CLAIM_SUCCEEDED'; export const FETCH_CHANNEL_LIST_STARTED = 'FETCH_CHANNEL_LIST_STARTED'; export const FETCH_CHANNEL_LIST_COMPLETED = 'FETCH_CHANNEL_LIST_COMPLETED'; export const FETCH_CHANNEL_LIST_FAILED = 'FETCH_CHANNEL_LIST_FAILED'; +export const FETCH_COLLECTION_LIST_STARTED = 'FETCH_COLLECTION_LIST_STARTED'; +export const FETCH_COLLECTION_LIST_COMPLETED = 'FETCH_COLLECTION_LIST_COMPLETED'; +export const FETCH_COLLECTION_LIST_FAILED = 'FETCH_COLLECTION_LIST_FAILED'; export const CREATE_CHANNEL_STARTED = 'CREATE_CHANNEL_STARTED'; export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED'; export const CREATE_CHANNEL_FAILED = 'CREATE_CHANNEL_FAILED'; @@ -111,6 +114,7 @@ export const UPDATE_CHANNEL_FAILED = 'UPDATE_CHANNEL_FAILED'; export const IMPORT_CHANNEL_STARTED = 'IMPORT_CHANNEL_STARTED'; export const IMPORT_CHANNEL_COMPLETED = 'IMPORT_CHANNEL_COMPLETED'; export const IMPORT_CHANNEL_FAILED = 'IMPORT_CHANNEL_FAILED'; +export const CLEAR_CHANNEL_ERRORS = 'CLEAR_CHANNEL_ERRORS'; export const PUBLISH_STARTED = 'PUBLISH_STARTED'; export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED'; export const PUBLISH_FAILED = 'PUBLISH_FAILED'; @@ -129,7 +133,6 @@ export const CLAIM_REPOST_STARTED = 'CLAIM_REPOST_STARTED'; export const CLAIM_REPOST_COMPLETED = 'CLAIM_REPOST_COMPLETED'; export const CLAIM_REPOST_FAILED = 'CLAIM_REPOST_FAILED'; export const CLEAR_REPOST_ERROR = 'CLEAR_REPOST_ERROR'; -export const CLEAR_CHANNEL_ERRORS = 'CLEAR_CHANNEL_ERRORS'; export const CHECK_PUBLISH_NAME_STARTED = 'CHECK_PUBLISH_NAME_STARTED'; export const CHECK_PUBLISH_NAME_COMPLETED = 'CHECK_PUBLISH_NAME_COMPLETED'; export const UPDATE_PENDING_CLAIMS = 'UPDATE_PENDING_CLAIMS'; @@ -142,6 +145,27 @@ export const PURCHASE_LIST_STARTED = 'PURCHASE_LIST_STARTED'; export const PURCHASE_LIST_COMPLETED = 'PURCHASE_LIST_COMPLETED'; export const PURCHASE_LIST_FAILED = 'PURCHASE_LIST_FAILED'; +export const COLLECTION_PUBLISH_STARTED = 'COLLECTION_PUBLISH_STARTED'; +export const COLLECTION_PUBLISH_COMPLETED = 'COLLECTION_PUBLISH_COMPLETED'; +export const COLLECTION_PUBLISH_FAILED = 'COLLECTION_PUBLISH_FAILED'; +export const COLLECTION_PUBLISH_UPDATE_STARTED = 'COLLECTION_PUBLISH_UPDATE_STARTED'; +export const COLLECTION_PUBLISH_UPDATE_COMPLETED = 'COLLECTION_PUBLISH_UPDATE_COMPLETED'; +export const COLLECTION_PUBLISH_UPDATE_FAILED = 'COLLECTION_PUBLISH_UPDATE_FAILED'; +export const COLLECTION_PUBLISH_ABANDON_STARTED = 'COLLECTION_PUBLISH_ABANDON_STARTED'; +export const COLLECTION_PUBLISH_ABANDON_COMPLETED = 'COLLECTION_PUBLISH_ABANDON_COMPLETED'; +export const COLLECTION_PUBLISH_ABANDON_FAILED = 'COLLECTION_PUBLISH_ABANDON_FAILED'; +export const CLEAR_COLLECTION_ERRORS = 'CLEAR_COLLECTION_ERRORS'; +export const COLLECTION_ITEMS_RESOLVE_STARTED = 'COLLECTION_ITEMS_RESOLVE_STARTED'; +export const COLLECTION_ITEMS_RESOLVE_COMPLETED = 'COLLECTION_ITEMS_RESOLVE_COMPLETED'; +export const COLLECTION_ITEMS_RESOLVE_FAILED = 'COLLECTION_ITEMS_RESOLVE_FAILED'; +export const COLLECTION_NEW = 'COLLECTION_NEW'; +export const COLLECTION_DELETE = 'COLLECTION_DELETE'; +export const COLLECTION_PENDING = 'COLLECTION_PENDING'; +export const COLLECTION_EDIT = 'COLLECTION_EDIT'; +export const COLLECTION_COPY = 'COLLECTION_COPY'; +export const COLLECTION_SAVE = 'COLLECTION_SAVE'; +export const COLLECTION_ERROR = 'COLLECTION_ERROR'; + // Comments export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED'; export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED'; diff --git a/src/constants/collections.js b/src/constants/collections.js new file mode 100644 index 0000000..6c002e6 --- /dev/null +++ b/src/constants/collections.js @@ -0,0 +1,10 @@ +export const COLLECTION_ID = 'lid'; +export const COLLECTION_INDEX = 'linx'; + +export const COL_TYPE_PLAYLIST = 'playlist'; +export const COL_TYPE_CHANNELS = 'channelList'; + +export const WATCH_LATER_ID = 'watchlater'; +export const FAVORITES_ID = 'favorites'; +export const FAVORITE_CHANNELS_ID = 'favoriteChannels'; +export const BUILTIN_LISTS = [WATCH_LATER_ID, FAVORITES_ID, FAVORITE_CHANNELS_ID]; diff --git a/src/index.js b/src/index.js index d412d83..db1ef86 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import * as TXO_LIST from 'constants/txo_list'; import * as SPEECH_URLS from 'constants/speech_urls'; import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import * as SHARED_PREFERENCES from 'constants/shared_preferences'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import { DEFAULT_KNOWN_TAGS, DEFAULT_FOLLOWED_TAGS, MATURE_TAGS } from 'constants/tags'; import Lbry, { apiCall } from 'lbry'; import LbryFirst from 'lbry-first'; @@ -35,6 +36,7 @@ export { MATURE_TAGS, SPEECH_URLS, SHARED_PREFERENCES, + COLLECTIONS_CONSTS, }; // common @@ -57,6 +59,13 @@ export { buildSharedStateMiddleware } from 'redux/middleware/shared-state'; // actions export { doToast, doDismissToast, doError, doDismissError } from 'redux/actions/notifications'; +export { + doLocalCollectionCreate, + doFetchItemsInCollection, + doFetchItemsInCollections, + doCollectionEdit, + doCollectionDelete, +} from 'redux/actions/collections'; export { doFetchClaimsByChannel, @@ -66,6 +75,7 @@ export { doResolveUris, doResolveUri, doFetchChannelListMine, + doFetchCollectionListMine, doCreateChannel, doUpdateChannel, doClaimSearch, @@ -76,6 +86,8 @@ export { doCheckPublishNameAvailability, doPurchaseList, doCheckPendingClaims, + doCollectionPublish, + doCollectionPublishUpdate, } from 'redux/actions/claims'; export { doClearPurchasedUriSuccess, doPurchaseUri, doFileGet } from 'redux/actions/file'; @@ -140,11 +152,37 @@ export { fileInfoReducer } from 'redux/reducers/file_info'; export { notificationsReducer } from 'redux/reducers/notifications'; export { publishReducer } from 'redux/reducers/publish'; export { walletReducer } from 'redux/reducers/wallet'; +export { collectionsReducer } from 'redux/reducers/collections'; // selectors export { makeSelectContentPositionForUri } from 'redux/selectors/content'; export { selectToast, selectError } from 'redux/selectors/notifications'; +export { + selectSavedCollectionIds, + selectBuiltinCollections, + selectResolvedCollections, + selectMyUnpublishedCollections, + selectMyEditedCollections, + selectMyPublishedCollections, + selectMyPublishedMixedCollections, + selectMyPublishedPlaylistCollections, + makeSelectEditedCollectionForId, + makeSelectPendingCollectionForId, + makeSelectPublishedCollectionForId, + makeSelectCollectionIsMine, + makeSelectMyPublishedCollectionForId, + makeSelectUnpublishedCollectionForId, + makeSelectCollectionForId, + makeSelectUrlsForCollectionId, + makeSelectClaimIdsForCollectionId, + makeSelectNameForCollectionId, + makeSelectCountForCollectionId, + makeSelectIsResolvingCollectionForId, + makeSelectIndexForUrlInCollection, + makeSelectNextUrlForCollectionAndUrl, + makeSelectCollectionForIdHasClaimUrl, +} from 'redux/selectors/collections'; export { makeSelectClaimForUri, @@ -209,6 +247,8 @@ export { selectAllMyClaimsByOutpoint, selectMyClaimsOutpoints, selectFetchingMyChannels, + selectFetchingMyCollections, + selectMyCollectionIds, selectMyChannelClaims, selectResolvingUris, selectPlayingUri, @@ -237,6 +277,11 @@ export { selectFetchingMyPurchasesError, selectMyPurchasesCount, selectPurchaseUriSuccess, + makeSelectClaimIdForUri, + selectUpdatingCollection, + selectUpdateCollectionError, + selectCreatingCollection, + selectCreateCollectionError, } from 'redux/selectors/claims'; export { diff --git a/src/lbry.js b/src/lbry.js index bfa20eb..9a73384 100644 --- a/src/lbry.js +++ b/src/lbry.js @@ -90,6 +90,10 @@ const Lbry: LbryTypes = { support_create: params => daemonCallWithResult('support_create', params), support_list: params => daemonCallWithResult('support_list', params), stream_repost: params => daemonCallWithResult('stream_repost', params), + collection_resolve: params => daemonCallWithResult('collection_resolve', params), + collection_list: params => daemonCallWithResult('collection_list', params), + collection_create: params => daemonCallWithResult('collection_create', params), + collection_update: params => daemonCallWithResult('collection_update', params), // File fetching and manipulation file_list: (params = {}) => daemonCallWithResult('file_list', params), diff --git a/src/redux/actions/claims.js b/src/redux/actions/claims.js index cd0cb6d..cee7c13 100644 --- a/src/redux/actions/claims.js +++ b/src/redux/actions/claims.js @@ -10,16 +10,22 @@ import { selectClaimsByUri, selectMyChannelClaims, selectPendingIds, - selectClaimsById, } from 'redux/selectors/claims'; + import { doFetchTxoPage } from 'redux/actions/wallet'; import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; import { creditsToString } from 'util/format-credits'; import { batchActions } from 'util/batch-actions'; import { createNormalizedClaimSearchKey } from 'util/claim'; import { PAGE_SIZE } from 'constants/claim'; - -type ResolveEntries = Array<[string, GenericClaim]>; +import { + selectPendingCollections, +} from 'redux/selectors/collections'; +import { + doFetchItemsInCollection, + doFetchItemsInCollections, + doCollectionDelete, +} from 'redux/actions/collections'; export function doResolveUris( uris: Array, @@ -61,9 +67,12 @@ export function doResolveUris( stream: ?StreamClaim, channel: ?ChannelClaim, claimsInChannel: ?number, + collection: ?CollectionClaim, }, } = {}; + const collectionIds: Array = []; + return Lbry.resolve({ urls: urisToResolve, ...options }).then( async(result: ResolveResponse) => { let repostedResults = {}; @@ -80,6 +89,7 @@ export function doResolveUris( // https://github.com/facebook/flow/issues/2221 if (uriResolveInfo) { if (uriResolveInfo.error) { + // $FlowFixMe resolveInfo[uri] = { ...fallbackResolveInfo }; } else { if (checkReposts) { @@ -96,6 +106,10 @@ export function doResolveUris( result.channel = uriResolveInfo; // $FlowFixMe result.claimsInChannel = uriResolveInfo.meta.claims_in_channel; + } else if (uriResolveInfo.value_type === 'collection') { + result.collection = uriResolveInfo; + // $FlowFixMe + collectionIds.push(uriResolveInfo.claim_id); } else { result.stream = uriResolveInfo; if (uriResolveInfo.signing_channel) { @@ -127,6 +141,11 @@ export function doResolveUris( type: ACTIONS.RESOLVE_URIS_COMPLETED, data: { resolveInfo }, }); + + if (collectionIds.length) { + dispatch(doFetchItemsInCollections({ collectionIds: collectionIds, pageSize: 5 })); + } + return result; } ); @@ -399,7 +418,7 @@ export function doCreateChannel(name: string, amount: number, optionalParams: an description?: string, website_url?: string, email?: string, - tags?: Array, + tags?: Array, languages?: Array, } = { name, @@ -493,7 +512,6 @@ export function doUpdateChannel(params: any, cb: any) { } // we'll need to remove these once we add locations/channels to channel page edit/create options - if (channelClaim && channelClaim.value && channelClaim.value.locations) { updateParams.locations = channelClaim.value.locations; } @@ -531,7 +549,7 @@ export function doImportChannel(certificate: string) { }); return Lbry.channel_import({ channel_data: certificate }) - .then((result: string) => { + .then(() => { dispatch({ type: ACTIONS.IMPORT_CHANNEL_COMPLETED, }); @@ -573,11 +591,45 @@ export function doFetchChannelListMine( }; } +export function doFetchCollectionListMine(page: number = 1, pageSize: number = 99999) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.FETCH_COLLECTION_LIST_STARTED, + }); + + const callback = (response: CollectionListResponse) => { + const { items } = response; + dispatch({ + type: ACTIONS.FETCH_COLLECTION_LIST_COMPLETED, + data: { claims: items }, + }); + dispatch( + doFetchItemsInCollections({ + collectionIds: items.map(claim => claim.claim_id), + page_size: 5, + }) + ); + }; + + const failure = error => { + dispatch({ + type: ACTIONS.FETCH_COLLECTION_LIST_FAILED, + data: error, + }); + }; + + Lbry.collection_list({ page, page_size: pageSize, resolve_claims: 1, resolve: true }).then( + callback, + failure + ); + }; +} + export function doClaimSearch( options: { page_size: number, page: number, - no_totals: boolean, + no_totals?: boolean, any_tags?: Array, claim_ids?: Array, channel_ids?: Array, @@ -618,7 +670,7 @@ export function doClaimSearch( pageSize: options.page_size, }, }); - return true; + return resolveInfo; }; const failure = err => { @@ -638,8 +690,7 @@ export function doClaimSearch( } export function doRepost(options: StreamRepostOptions) { - return (dispatch: Dispatch) => { - // $FlowFixMe + return (dispatch: Dispatch): Promise => { return new Promise(resolve => { dispatch({ type: ACTIONS.CLAIM_REPOST_STARTED, @@ -679,6 +730,189 @@ export function doRepost(options: StreamRepostOptions) { }; } +export function doCollectionPublish( + options: { + name: string, + bid: string, + blocking: true, + title?: string, + channel_id?: string, + thumbnail_url?: string, + description?: string, + tags?: Array, + languages?: Array, + claims: Array, + }, + localId: string +) { + return (dispatch: Dispatch): Promise => { + // $FlowFixMe + + const params: { + name: string, + bid: string, + channel_id?: string, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + tags?: Array, + languages?: Array, + claims: Array, + } = { + name: options.name, + bid: creditsToString(options.bid), + title: options.title, + thumbnail_url: options.thumbnail_url, + description: options.description, + tags: [], + languages: options.languages || [], + locations: [], + blocking: true, + claims: options.claims, + }; + + if (options.tags) { + params['tags'] = options.tags.map(tag => tag.name); + } + + if (options.channel_id) { + params['channel_id'] = options.channel_id; + } + + return new Promise(resolve => { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_STARTED, + }); + + function success(response) { + const collectionClaim = response.outputs[0]; + dispatch( + batchActions( + { + type: ACTIONS.COLLECTION_PUBLISH_COMPLETED, + data: { claimId: collectionClaim.claim_id }, + }, + // move unpublished collection to pending collection with new publish id + // recent publish won't resolve this second. handle it in checkPending + { + type: ACTIONS.UPDATE_PENDING_CLAIMS, + data: { + claims: [collectionClaim], + }, + } + ) + ); + dispatch({ + type: ACTIONS.COLLECTION_PENDING, + data: { localId: localId, claimId: collectionClaim.claim_id }, + }); + dispatch(doCheckPendingClaims()); + dispatch(doFetchCollectionListMine(1, 10)); + return resolve(collectionClaim); + } + + function failure(error) { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_FAILED, + data: { + error: error.message, + }, + }); + } + + return Lbry.collection_create(params).then(success, failure); + }); + }; +} + +export function doCollectionPublishUpdate(options: { + bid?: string, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + claim_id: string, + tags?: Array, + languages?: Array, + claims?: Array, +}) { + return (dispatch: Dispatch): Promise => { + // TODO: implement one click update + + const updateParams: { + bid?: string, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + claim_id: string, + tags?: Array, + languages?: Array, + claims?: Array, + clear_claims: boolean, + } = { + bid: creditsToString(options.bid), + title: options.title, + thumbnail_url: options.thumbnail_url, + description: options.description, + tags: [], + languages: options.languages || [], + locations: [], + blocking: true, + claim_id: options.claim_id, + clear_claims: true, + }; + + if (options.tags) { + updateParams['tags'] = options.tags.map(tag => tag.name); + } + + if (options.claims) { + updateParams['claims'] = options.claims; + } + return new Promise(resolve => { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_UPDATE_STARTED, + }); + + function success(response) { + const collectionClaim = response.outputs[0]; + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_UPDATE_COMPLETED, + data: { + collectionClaim, + }, + }); + dispatch({ + type: ACTIONS.COLLECTION_PENDING, + data: { claimId: collectionClaim.claim_id }, + }); + dispatch({ + type: ACTIONS.UPDATE_PENDING_CLAIMS, + data: { + claims: [collectionClaim], + }, + }); + dispatch(doCheckPendingClaims()); + dispatch(doFetchCollectionListMine(1, 10)); + return resolve(collectionClaim); + } + + function failure(error) { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_UPDATE_FAILED, + data: { + error: error.message, + }, + }); + } + + return Lbry.collection_update(updateParams).then(success, failure); + }); + }; +} + export function doCheckPublishNameAvailability(name: string) { return (dispatch: Dispatch) => { dispatch({ @@ -750,6 +984,7 @@ export const doCheckPendingClaims = (onConfirmed: Function) => ( const checkClaimList = () => { const state = getState(); const pendingIdSet = new Set(selectPendingIds(state)); + const pendingCollections = selectPendingCollections(state); Lbry.claim_list({ page: 1, page_size: 10 }) .then(result => { const claims = result.items; @@ -758,6 +993,10 @@ export const doCheckPendingClaims = (onConfirmed: Function) => ( const { claim_id: claimId } = claim; if (claim.confirmations > 0 && pendingIdSet.has(claimId)) { pendingIdSet.delete(claimId); + if (Object.keys(pendingCollections).includes(claim.claim_id)) { + dispatch(doFetchItemsInCollection({ collectionId: claim.claim_id })); + dispatch(doCollectionDelete(claim.claim_id, 'pending')); + } claimsToConfirm.push(claim); if (onConfirmed) { onConfirmed(claim); diff --git a/src/redux/actions/collections.js b/src/redux/actions/collections.js new file mode 100644 index 0000000..43176e3 --- /dev/null +++ b/src/redux/actions/collections.js @@ -0,0 +1,494 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import { v4 as uuid } from 'uuid'; +import Lbry from 'lbry'; +import { doClaimSearch, doAbandonClaim } from 'redux/actions/claims'; +import { makeSelectClaimForClaimId } from 'redux/selectors/claims'; +import { + makeSelectCollectionForId, + // makeSelectPublishedCollectionForId, // for "save" or "copy" action + makeSelectMyPublishedCollectionForId, + makeSelectPublishedCollectionForId, + makeSelectUnpublishedCollectionForId, + makeSelectEditedCollectionForId, +} from 'redux/selectors/collections'; +import * as COLS from 'constants/collections'; + +const getTimestamp = () => { + return Math.floor(Date.now() / 1000); +}; + +const FETCH_BATCH_SIZE = 10; + +export const doLocalCollectionCreate = ( + name: string, + collectionItems: Array, + type: string, + sourceId: string +) => (dispatch: Dispatch) => { + return dispatch({ + type: ACTIONS.COLLECTION_NEW, + data: { + entry: { + id: uuid(), // start with a uuid, this becomes a claimId after publish + name: name, + updatedAt: getTimestamp(), + items: collectionItems || [], + sourceId: sourceId, + type: type, + }, + }, + }); +}; + +export const doCollectionDelete = (id: string, colKey: ?string = undefined) => ( + dispatch: Dispatch, + getState: GetState +) => { + const state = getState(); + const claim = makeSelectClaimForClaimId(id)(state); + const collectionDelete = () => + dispatch({ + type: ACTIONS.COLLECTION_DELETE, + data: { + id: id, + collectionKey: colKey, + }, + }); + if (claim && !colKey) { + // could support "abandon, but keep" later + const { txid, nout } = claim; + return dispatch(doAbandonClaim(txid, nout, collectionDelete)); + } + return collectionDelete(); +}; + +// Given a collection, save its collectionId to be resolved and displayed in Library +// export const doCollectionSave = ( +// id: string, +// ) => (dispatch: Dispatch) => { +// return dispatch({ +// type: ACTIONS.COLLECTION_SAVE, +// data: { +// id: id, +// }, +// }); +// }; + +// Given a collection and name, copy it to a local private collection with a name +// export const doCollectionCopy = ( +// id: string, +// ) => (dispatch: Dispatch) => { +// return dispatch({ +// type: ACTIONS.COLLECTION_COPY, +// data: { +// id: id, +// }, +// }); +// }; + +export const doFetchItemsInCollections = ( + resolveItemsOptions: { + collectionIds: Array, + pageSize?: number, + }, + resolveStartedCallback?: () => void +) => async (dispatch: Dispatch, getState: GetState) => { + /* + 1) make sure all the collection claims are loaded into claims reducer, search/resolve if necessary. + 2) get the item claims for each + 3) format and make sure they're in the order as in the claim + 4) Build the collection objects and update collections reducer + 5) Update redux claims reducer + */ + let state = getState(); + const { collectionIds, pageSize } = resolveItemsOptions; + + dispatch({ + type: ACTIONS.COLLECTION_ITEMS_RESOLVE_STARTED, + data: { ids: collectionIds }, + }); + + if (resolveStartedCallback) resolveStartedCallback(); + + const collectionIdsToSearch = collectionIds.filter(claimId => !state.claims.byId[claimId]); + + if (collectionIdsToSearch.length) { + await dispatch(doClaimSearch({ claim_ids: collectionIdsToSearch, page: 1, page_size: 9999 })); + } + + const stateAfterClaimSearch = getState(); + + async function fetchItemsForCollectionClaim(claim: CollectionClaim, pageSize?: number) { + const totalItems = claim.value.claims && claim.value.claims.length; + const claimId = claim.claim_id; + const itemOrder = claim.value.claims; + + const sortResults = (items: Array, claimList) => { + const newItems: Array = []; + claimList.forEach(id => { + const index = items.findIndex(i => i.claim_id === id); + if (index >= 0) { + newItems.push(items[index]); + } + }); + /* + This will return newItems[] of length less than total_items below + if one or more of the claims has been abandoned. That's ok for now. + */ + return newItems; + }; + + const mergeBatches = ( + arrayOfResults: Array<{ items: Array, total_items: number }>, + claimList: Array + ) => { + const mergedResults: { items: Array, total_items: number } = { + items: [], + total_items: 0, + }; + arrayOfResults.forEach(result => { + mergedResults.items = mergedResults.items.concat(result.items); + mergedResults.total_items = result.total_items; + }); + + mergedResults.items = sortResults(mergedResults.items, claimList); + return mergedResults; + }; + + try { + const batchSize = pageSize || FETCH_BATCH_SIZE; + const batches: Array> = []; + + for (let i = 0; i < Math.ceil(totalItems / batchSize); i++) { + batches[i] = Lbry.claim_search({ + claim_ids: claim.value.claims, + page: i + 1, + page_size: batchSize, + }); + } + const itemsInBatches = await Promise.all(batches); + const result = mergeBatches(itemsInBatches, itemOrder); + + // $FlowFixMe + const itemsById: { claimId: string, items?: ?Array } = { claimId: claimId }; + if (result.items) { + itemsById.items = result.items; + } else { + itemsById.items = null; + } + return itemsById; + } catch (e) { + return { + claimId: claimId, + items: null, + }; + } + } + + function formatForClaimActions(resultClaimsByUri) { + const formattedClaims = {}; + Object.entries(resultClaimsByUri).forEach(([uri, uriResolveInfo]) => { + // Flow has terrible Object.entries support + // https://github.com/facebook/flow/issues/2221 + if (uriResolveInfo) { + let result = {}; + if (uriResolveInfo.value_type === 'channel') { + result.channel = uriResolveInfo; + // $FlowFixMe + result.claimsInChannel = uriResolveInfo.meta.claims_in_channel; + // ALSO SKIP COLLECTIONS + } else if (uriResolveInfo.value_type === 'collection') { + result.collection = uriResolveInfo; + } else { + result.stream = uriResolveInfo; + if (uriResolveInfo.signing_channel) { + result.channel = uriResolveInfo.signing_channel; + result.claimsInChannel = + (uriResolveInfo.signing_channel.meta && + uriResolveInfo.signing_channel.meta.claims_in_channel) || + 0; + } + } + // $FlowFixMe + formattedClaims[uri] = result; + } + }); + return formattedClaims; + } + + const invalidCollectionIds = []; + const promisedCollectionItemFetches = []; + collectionIds.forEach(collectionId => { + const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch); + if (!claim) { + invalidCollectionIds.push(collectionId); + } else { + promisedCollectionItemFetches.push(fetchItemsForCollectionClaim(claim, pageSize)); + } + }); + + // $FlowFixMe + const collectionItemsById: Array<{ + claimId: string, + items: ?Array, + }> = await Promise.all(promisedCollectionItemFetches); + + const newCollectionObjectsById = {}; + const resolvedItemsByUrl = {}; + collectionItemsById.forEach(entry => { + // $FlowFixMe + const collectionItems: Array = entry.items; + const collectionId = entry.claimId; + if (collectionItems) { + const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch); + + const editedCollection = makeSelectEditedCollectionForId(collectionId)(stateAfterClaimSearch); + const { name, timestamp, value } = claim || {}; + const { title } = value; + const valueTypes = new Set(); + const streamTypes = new Set(); + + let newItems = []; + let isPlaylist; + + if (collectionItems) { + collectionItems.forEach(collectionItem => { + newItems.push(collectionItem.permanent_url); + valueTypes.add(collectionItem.value_type); + if (collectionItem.value.stream_type) { + streamTypes.add(collectionItem.value.stream_type); + } + resolvedItemsByUrl[collectionItem.canonical_url] = collectionItem; + }); + isPlaylist = + valueTypes.size === 1 && + valueTypes.has('stream') && + ((streamTypes.size === 1 && (streamTypes.has('audio') || streamTypes.has('video'))) || + (streamTypes.size === 2 && (streamTypes.has('audio') && streamTypes.has('video')))); + } + + newCollectionObjectsById[collectionId] = { + items: newItems, + id: collectionId, + name: title || name, + itemCount: claim.value.claims.length, + type: isPlaylist ? 'playlist' : 'collection', + updatedAt: timestamp, + }; + + if (editedCollection && timestamp > editedCollection['updatedAt']) { + dispatch({ + type: ACTIONS.COLLECTION_DELETE, + data: { + id: collectionId, + collectionKey: 'edited', + }, + }); + } + } else { + invalidCollectionIds.push(collectionId); + } + }); + const formattedClaimsByUri = formatForClaimActions(collectionItemsById); + + dispatch({ + type: ACTIONS.RESOLVE_URIS_COMPLETED, + data: { resolveInfo: formattedClaimsByUri }, + }); + + dispatch({ + type: ACTIONS.COLLECTION_ITEMS_RESOLVE_COMPLETED, + data: { + resolvedCollections: newCollectionObjectsById, + failedCollectionIds: invalidCollectionIds, + }, + }); +}; + +export const doFetchItemsInCollection = ( + options: { collectionId: string, pageSize?: number }, + cb?: () => void +) => { + const { collectionId, pageSize } = options; + const newOptions: { collectionIds: Array, pageSize?: number } = { + collectionIds: [collectionId], + }; + if (pageSize) newOptions.pageSize = pageSize; + return doFetchItemsInCollections(newOptions, cb); +}; + +export const doCollectionEdit = (collectionId: string, params: CollectionEditParams) => async ( + dispatch: Dispatch, + getState: GetState +) => { + const state = getState(); + const collection: Collection = makeSelectCollectionForId(collectionId)(state); + const editedCollection: Collection = makeSelectEditedCollectionForId(collectionId)(state); + const unpublishedCollection: Collection = makeSelectUnpublishedCollectionForId(collectionId)( + state + ); + const publishedCollection: Collection = makeSelectPublishedCollectionForId(collectionId)(state); // needs to be published only + + const generateCollectionItemsFromSearchResult = results => { + return ( + Object.values(results) + // $FlowFixMe + .reduce( + ( + acc, + cur: { + stream: ?StreamClaim, + channel: ?ChannelClaim, + claimsInChannel: ?number, + collection: ?CollectionClaim, + } + ) => { + let url; + if (cur.stream) { + url = cur.stream.permanent_url; + } else if (cur.channel) { + url = cur.channel.permanent_url; + } else if (cur.collection) { + url = cur.collection.permanent_url; + } else { + return acc; + } + acc.push(url); + return acc; + }, + [] + ) + ); + }; + + if (!collection) { + return dispatch({ + type: ACTIONS.COLLECTION_ERROR, + data: { + message: 'collection does not exist', + }, + }); + } + + let currentItems = collection.items ? collection.items.concat() : []; + const { claims: passedClaims, order, claimIds, replace, remove, type } = params; + + const collectionType = type || collection.type; + let newItems: Array = currentItems; + + if (passedClaims) { + if (remove) { + const passedUrls = passedClaims.map(claim => claim.permanent_url); + // $FlowFixMe // need this? + newItems = currentItems.filter((item: string) => !passedUrls.includes(item)); + } else { + passedClaims.forEach(claim => newItems.push(claim.permanent_url)); + } + } + + if (claimIds) { + const batches = []; + if (claimIds.length > 50) { + for (let i = 0; i < Math.ceil(claimIds.length / 50); i++) { + batches[i] = claimIds.slice(i * 50, (i + 1) * 50); + } + } else { + batches[0] = claimIds; + } + const resultArray = await Promise.all( + batches.map(batch => { + let options = { claim_ids: batch, page: 1, page_size: 50 }; + return dispatch(doClaimSearch(options)); + }) + ); + + const searchResults = Object.assign({}, ...resultArray); + + if (replace) { + newItems = generateCollectionItemsFromSearchResult(searchResults); + } else { + newItems = currentItems.concat(generateCollectionItemsFromSearchResult(searchResults)); + } + } + + if (order) { + const [movedItem] = currentItems.splice(order.from, 1); + currentItems.splice(order.to, 0, movedItem); + } + + // console.log('p&e', publishedCollection.items, newItems, publishedCollection.items.join(','), newItems.join(',')) + if (editedCollection) { + // delete edited if newItems are the same as publishedItems + if (publishedCollection.items.join(',') === newItems.join(',')) { + dispatch({ + type: ACTIONS.COLLECTION_DELETE, + data: { + id: collectionId, + collectionKey: 'edited', + }, + }); + } else { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'edited', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } + } else if (publishedCollection) { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'edited', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } else if (COLS.BUILTIN_LISTS.includes(collectionId)) { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'builtin', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } else if (unpublishedCollection) { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'unpublished', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } + return true; +}; diff --git a/src/redux/actions/publish.js b/src/redux/actions/publish.js index 36222c0..4732ebd 100644 --- a/src/redux/actions/publish.js +++ b/src/redux/actions/publish.js @@ -21,7 +21,6 @@ export const doResetThumbnailStatus = () => (dispatch: Dispatch) => { type: ACTIONS.UPDATE_PUBLISH_FORM, data: { thumbnailPath: '', - thumbnailError: undefined, }, }); @@ -97,13 +96,6 @@ export const doUploadThumbnail = ( ); }; - dispatch({ - type: ACTIONS.UPDATE_PUBLISH_FORM, - data: { - thumbnailError: undefined, - }, - }); - const doUpload = data => { return fetch(SPEECH_PUBLISH, { method: 'POST', @@ -208,8 +200,7 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis description, fee, languages, - releaseTime: release_time, - releaseTimeEdited: undefined, + release_time: release_time ? Number(release_time) * 1000 : undefined, thumbnail: thumbnail ? thumbnail.url : null, title, uri, @@ -263,7 +254,7 @@ export const doPublish = (success: Function, fail: Function, preview: Function) filePath, description, language, - releaseTimeEdited, + releaseTime, license, licenseUrl, useLBRYUploader, @@ -356,8 +347,8 @@ export const doPublish = (success: Function, fail: Function, preview: Function) } // Set release time to curret date. On edits, keep original release/transaction time as release_time - if (releaseTimeEdited) { - publishPayload.release_time = releaseTimeEdited; + if (releaseTime) { + publishPayload.release_time = Number(Math.round(new Date(releaseTime) / 1000)); } else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) { publishPayload.release_time = Number(myClaimForUri.value.release_time); } else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) { diff --git a/src/redux/actions/sync.js b/src/redux/actions/sync.js index 14c7f00..032a8ab 100644 --- a/src/redux/actions/sync.js +++ b/src/redux/actions/sync.js @@ -13,6 +13,9 @@ type SharedData = { settings?: any, app_welcome_version?: number, sharing_3P?: boolean, + unpublishedCollections: CollectionGroup, + builtinCollections: CollectionGroup, + savedCollections: Array, }, }; @@ -27,6 +30,9 @@ function extractUserState(rawObj: SharedData) { settings, app_welcome_version, sharing_3P, + unpublishedCollections, + builtinCollections, + savedCollections, } = rawObj.value; return { @@ -38,6 +44,9 @@ function extractUserState(rawObj: SharedData) { ...(settings ? { settings } : {}), ...(app_welcome_version ? { app_welcome_version } : {}), ...(sharing_3P ? { sharing_3P } : {}), + ...(unpublishedCollections ? { unpublishedCollections } : {}), + ...(builtinCollections ? { builtinCollections } : {}), + ...(savedCollections ? { savedCollections } : {}), }; } @@ -55,6 +64,9 @@ export function doPopulateSharedUserState(sharedSettings: any) { settings, app_welcome_version, sharing_3P, + unpublishedCollections, + builtinCollections, + savedCollections, } = extractUserState(sharedSettings); dispatch({ type: ACTIONS.USER_STATE_POPULATE, @@ -67,6 +79,9 @@ export function doPopulateSharedUserState(sharedSettings: any) { settings, welcomeVersion: app_welcome_version, allowAnalytics: sharing_3P, + unpublishedCollections, + builtinCollections, + savedCollections, }, }); }; diff --git a/src/redux/middleware/shared-state.js b/src/redux/middleware/shared-state.js index 3d40834..031e9a4 100644 --- a/src/redux/middleware/shared-state.js +++ b/src/redux/middleware/shared-state.js @@ -26,7 +26,6 @@ export const buildSharedStateMiddleware = ( clearTimeout(timeout); const actionResult = next(action); // Call `getState` after calling `next` to ensure the state has updated in response to the action - function runPreferences() { const nextState: { user: any, settings: any } = getState(); const syncEnabled = diff --git a/src/redux/reducers/claims.js b/src/redux/reducers/claims.js index 1b77d3a..e167aac 100644 --- a/src/redux/reducers/claims.js +++ b/src/redux/reducers/claims.js @@ -13,6 +13,7 @@ import mergeClaim from 'util/merge-claim'; type State = { createChannelError: ?string, + createCollectionError: ?string, channelClaimCounts: { [string]: number }, claimsByUri: { [string]: string }, byId: { [string]: Claim }, @@ -21,9 +22,11 @@ type State = { reflectingById: { [string]: ReflectingUpdate }, myClaims: ?Array, myChannelClaims: ?Array, + myCollectionClaims: ?Array, abandoningById: { [string]: boolean }, fetchingChannelClaims: { [string]: number }, fetchingMyChannels: boolean, + fetchingMyCollections: boolean, fetchingClaimSearchByQuery: { [string]: boolean }, purchaseUriSuccess: boolean, myPurchases: ?Array, @@ -34,6 +37,7 @@ type State = { claimSearchByQuery: { [string]: Array }, claimSearchByQueryLastPageReached: { [string]: Array }, creatingChannel: boolean, + creatingCollection: boolean, paginatedClaimsByChannel: { [string]: { all: Array, @@ -43,7 +47,9 @@ type State = { }, }, updateChannelError: ?string, + updateCollectionError: ?string, updatingChannel: boolean, + updatingCollection: boolean, pendingChannelImport: string | boolean, repostLoading: boolean, repostError: ?string, @@ -66,6 +72,7 @@ const defaultState = { fetchingChannelClaims: {}, resolvingUris: [], myChannelClaims: undefined, + myCollectionClaims: [], myClaims: undefined, myPurchases: undefined, myPurchasesPageNumber: undefined, @@ -74,6 +81,7 @@ const defaultState = { fetchingMyPurchases: false, fetchingMyPurchasesError: undefined, fetchingMyChannels: false, + fetchingMyCollections: false, abandoningById: {}, pendingIds: [], reflectingById: {}, @@ -82,9 +90,13 @@ const defaultState = { claimSearchByQueryLastPageReached: {}, fetchingClaimSearchByQuery: {}, updateChannelError: '', + updateCollectionError: '', updatingChannel: false, creatingChannel: false, createChannelError: undefined, + updatingCollection: false, + creatingCollection: false, + createCollectionError: undefined, pendingChannelImport: false, repostLoading: false, repostError: undefined, @@ -100,15 +112,7 @@ const defaultState = { }; function handleClaimAction(state: State, action: any): State { - const { - resolveInfo, - }: { - [string]: { - stream: ?StreamClaim, - channel: ?ChannelClaim, - claimsInChannel: ?number, - }, - } = action.data; + const { resolveInfo }: ClaimActionResolveInfo = action.data; const byUri = Object.assign({}, state.claimsByUri); const byId = Object.assign({}, state.byId); @@ -119,7 +123,7 @@ function handleClaimAction(state: State, action: any): State { Object.entries(resolveInfo).forEach(([url: string, resolveResponse: ResolveResponse]) => { // $FlowFixMe - const { claimsInChannel, stream, channel: channelFromResolve } = resolveResponse; + const { claimsInChannel, stream, channel: channelFromResolve, collection } = resolveResponse; const channel = channelFromResolve || (stream && stream.signing_channel); if (stream) { @@ -158,15 +162,32 @@ function handleClaimAction(state: State, action: any): State { } else { byId[channel.claim_id] = channel; } - // Also add the permanent_url here until lighthouse returns canonical_url for search results + byUri[channel.permanent_url] = channel.claim_id; byUri[channel.canonical_url] = channel.claim_id; newResolvingUrls.delete(channel.canonical_url); newResolvingUrls.delete(channel.permanent_url); } + if (collection) { + if (pendingIds.includes(collection.claim_id)) { + byId[collection.claim_id] = mergeClaim(collection, byId[collection.claim_id]); + } else { + byId[collection.claim_id] = collection; + } + byUri[url] = collection.claim_id; + byUri[collection.canonical_url] = collection.claim_id; + byUri[collection.permanent_url] = collection.claim_id; + newResolvingUrls.delete(collection.canonical_url); + newResolvingUrls.delete(collection.permanent_url); + + if (collection.is_my_output) { + myClaimIds.add(collection.claim_id); + } + } + newResolvingUrls.delete(url); - if (!stream && !channel && !pendingIds.includes(byUri[url])) { + if (!stream && !channel && !collection && !pendingIds.includes(byUri[url])) { byUri[url] = null; } }); @@ -306,6 +327,53 @@ reducers[ACTIONS.FETCH_CHANNEL_LIST_FAILED] = (state: State, action: any): State }); }; +reducers[ACTIONS.FETCH_COLLECTION_LIST_STARTED] = (state: State): State => ({ + ...state, + fetchingMyCollections: true, +}); + +reducers[ACTIONS.FETCH_COLLECTION_LIST_COMPLETED] = (state: State, action: any): State => { + const { claims }: { claims: Array } = action.data; + const myClaims = state.myClaims || []; + let myClaimIds = new Set(myClaims); + const pendingIds = state.pendingIds || []; + let myCollectionClaimsSet = new Set([]); + const byId = Object.assign({}, state.byId); + const byUri = Object.assign({}, state.claimsByUri); + + if (claims.length) { + myCollectionClaimsSet = new Set(state.myCollectionClaims); + claims.forEach(claim => { + const { meta } = claim; + const { canonical_url: canonicalUrl, permanent_url: permanentUrl, claim_id: claimId } = claim; + + byUri[canonicalUrl] = claimId; + byUri[permanentUrl] = claimId; + + // $FlowFixMe + myCollectionClaimsSet.add(claimId); + // we don't want to overwrite a pending result with a resolve + if (!pendingIds.some(c => c === claimId)) { + byId[claimId] = claim; + } + myClaimIds.add(claimId); + }); + } + + return { + ...state, + byId, + claimsByUri: byUri, + fetchingMyCollections: false, + myCollectionClaims: Array.from(myCollectionClaimsSet), + myClaims: myClaimIds ? Array.from(myClaimIds) : null, + }; +}; + +reducers[ACTIONS.FETCH_COLLECTION_LIST_FAILED] = (state: State): State => { + return { ...state, fetchingMyCollections: false }; +}; + reducers[ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED] = (state: State, action: any): State => { const { uri, page } = action.data; const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims); @@ -455,6 +523,7 @@ reducers[ACTIONS.ABANDON_CLAIM_SUCCEEDED] = (state: State, action: any): State = const newMyClaims = state.myClaims ? state.myClaims.slice() : []; const newMyChannelClaims = state.myChannelClaims ? state.myChannelClaims.slice() : []; const claimsByUri = Object.assign({}, state.claimsByUri); + const newMyCollectionClaims = state.myCollectionClaims ? state.myCollectionClaims.slice() : []; Object.keys(claimsByUri).forEach(uri => { if (claimsByUri[uri] === claimId) { @@ -463,12 +532,14 @@ reducers[ACTIONS.ABANDON_CLAIM_SUCCEEDED] = (state: State, action: any): State = }); const myClaims = newMyClaims.filter(i => i !== claimId); const myChannelClaims = newMyChannelClaims.filter(i => i !== claimId); + const myCollectionClaims = newMyCollectionClaims.filter(i => i !== claimId); delete byId[claimId]; return Object.assign({}, state, { myClaims, myChannelClaims, + myCollectionClaims, byId, claimsByUri, }); @@ -520,6 +591,61 @@ reducers[ACTIONS.UPDATE_CHANNEL_FAILED] = (state: State, action: any): State => }); }; +reducers[ACTIONS.CLEAR_COLLECTION_ERRORS] = (state: State): State => ({ + ...state, + createCollectionError: null, + updateCollectionError: null, +}); + +reducers[ACTIONS.COLLECTION_PUBLISH_STARTED] = (state: State): State => ({ + ...state, + creatingCollection: true, + createCollectionError: null, +}); + +reducers[ACTIONS.COLLECTION_PUBLISH_COMPLETED] = (state: State, action: any): State => { + const myCollections = state.myCollectionClaims || []; + const myClaims = state.myClaims || []; + const { claimId } = action.data; + let myClaimIds = new Set(myClaims); + let myCollectionClaimsSet = new Set(myCollections); + myClaimIds.add(claimId); + myCollectionClaimsSet.add(claimId); + return Object.assign({}, state, { + creatingCollection: false, + myClaims: Array.from(myClaimIds), + myCollectionClaims: Array.from(myCollectionClaimsSet), + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_FAILED] = (state: State, action: any): State => { + return Object.assign({}, state, { + creatingCollection: false, + createCollectionError: action.data.error, + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_UPDATE_STARTED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateCollectionError: '', + updatingCollection: true, + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_UPDATE_COMPLETED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateCollectionError: '', + updatingCollection: false, + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_UPDATE_FAILED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateCollectionError: action.data.error, + updatingCollection: false, + }); +}; + reducers[ACTIONS.IMPORT_CHANNEL_STARTED] = (state: State): State => Object.assign({}, state, { pendingChannelImports: true }); diff --git a/src/redux/reducers/collections.js b/src/redux/reducers/collections.js new file mode 100644 index 0000000..acd4fa7 --- /dev/null +++ b/src/redux/reducers/collections.js @@ -0,0 +1,223 @@ +// @flow +import { handleActions } from 'util/redux-utils'; +import * as ACTIONS from 'constants/action_types'; +import * as COLS from 'constants/collections'; + +const getTimestamp = () => { + return Math.floor(Date.now() / 1000); +}; + +const defaultState: CollectionState = { + builtin: { + watchlater: { + items: [], + id: COLS.WATCH_LATER_ID, + name: 'Watch Later', + updatedAt: getTimestamp(), + type: COLS.COL_TYPE_PLAYLIST, + }, + favorites: { + items: [], + id: COLS.FAVORITES_ID, + name: 'Favorites', + type: COLS.COL_TYPE_PLAYLIST, + updatedAt: getTimestamp(), + }, + }, + resolved: {}, + unpublished: {}, // sync + edited: {}, + pending: {}, + saved: [], + isResolvingCollectionById: {}, + error: null, +}; + +const collectionsReducer = handleActions( + { + [ACTIONS.COLLECTION_NEW]: (state, action) => { + const { entry: params } = action.data; // { id:, items: Array} + // entry + const newListTemplate = { + id: params.id, + name: params.name, + items: [], + updatedAt: getTimestamp(), + type: params.type, + }; + + const newList = Object.assign({}, newListTemplate, { ...params }); + const { unpublished: lists } = state; + const newLists = Object.assign({}, lists, { [params.id]: newList }); + + return { + ...state, + unpublished: newLists, + }; + }, + + [ACTIONS.COLLECTION_DELETE]: (state, action) => { + const { id, collectionKey } = action.data; + const { edited: editList, unpublished: unpublishedList, pending: pendingList } = state; + const newEditList = Object.assign({}, editList); + const newUnpublishedList = Object.assign({}, unpublishedList); + + const newPendingList = Object.assign({}, pendingList); + + if (collectionKey && state[collectionKey] && state[collectionKey][id]) { + const newList = Object.assign({}, state[collectionKey]); + delete newList[id]; + return { + ...state, + [collectionKey]: newList, + }; + } else { + if (newEditList[id]) { + delete newEditList[id]; + } else if (newUnpublishedList[id]) { + delete newUnpublishedList[id]; + } else if (newPendingList[id]) { + delete newPendingList[id]; + } + } + return { + ...state, + edited: newEditList, + unpublished: newUnpublishedList, + pending: newPendingList, + }; + }, + + [ACTIONS.COLLECTION_PENDING]: (state, action) => { + const { localId, claimId } = action.data; + const { edited: editList, unpublished: unpublishedList, pending: pendingList } = state; + const newEditList = Object.assign({}, editList); + const newUnpublishedList = Object.assign({}, unpublishedList); + const newPendingList = Object.assign({}, pendingList); + + if (localId) { + // new publish + newPendingList[claimId] = Object.assign({}, newUnpublishedList[localId] || {}); + delete newUnpublishedList[localId]; + } else { + // edit update + newPendingList[claimId] = Object.assign({}, newEditList[claimId]); + delete newEditList[claimId]; + } + + return { + ...state, + edited: newEditList, + unpublished: newUnpublishedList, + pending: newPendingList, + }; + }, + + [ACTIONS.COLLECTION_EDIT]: (state, action) => { + const { id, collectionKey, collection } = action.data; + + if (COLS.BUILTIN_LISTS.includes(id)) { + const { builtin: lists } = state; + return { + ...state, + [collectionKey]: { ...lists, [id]: collection }, + }; + } + + if (collectionKey === 'edited') { + const { edited: lists } = state; + return { + ...state, + edited: { ...lists, [id]: collection }, + }; + } + const { unpublished: lists } = state; + return { + ...state, + unpublished: { ...lists, [id]: collection }, + }; + }, + + [ACTIONS.COLLECTION_ERROR]: (state, action) => { + return Object.assign({}, state, { + error: action.data.message, + }); + }, + + [ACTIONS.COLLECTION_ITEMS_RESOLVE_STARTED]: (state, action) => { + const { ids } = action.data; + const { isResolvingCollectionById } = state; + const newResolving = Object.assign({}, isResolvingCollectionById); + ids.forEach(id => { + newResolving[id] = true; + }); + return Object.assign({}, state, { + ...state, + error: '', + isResolvingCollectionById: newResolving, + }); + }, + [ACTIONS.USER_STATE_POPULATE]: (state, action) => { + const { builtinCollections, savedCollections, unpublishedCollections } = action.data; + return { + ...state, + unpublished: unpublishedCollections || state.unpublished, + builtin: builtinCollections || state.builtin, + saved: savedCollections || state.saved, + }; + }, + [ACTIONS.COLLECTION_ITEMS_RESOLVE_COMPLETED]: (state, action) => { + const { resolvedCollections, failedCollectionIds } = action.data; + const { pending, edited, isResolvingCollectionById, resolved } = state; + const newPending = Object.assign({}, pending); + const newEdited = Object.assign({}, edited); + const newResolved = Object.assign({}, resolved, resolvedCollections); + + const resolvedIds = Object.keys(resolvedCollections); + const newResolving = Object.assign({}, isResolvingCollectionById); + if (resolvedCollections && Object.keys(resolvedCollections).length) { + resolvedIds.forEach(resolvedId => { + if (newEdited[resolvedId]) { + if (newEdited[resolvedId]['updatedAt'] < resolvedCollections[resolvedId]['updatedAt']) { + delete newEdited[resolvedId]; + } + } + delete newResolving[resolvedId]; + if (newPending[resolvedId]) { + delete newPending[resolvedId]; + } + }); + } + + if (failedCollectionIds && Object.keys(failedCollectionIds).length) { + failedCollectionIds.forEach(failedId => { + delete newResolving[failedId]; + }); + } + + return Object.assign({}, state, { + ...state, + pending: newPending, + resolved: newResolved, + edited: newEdited, + isResolvingCollectionById: newResolving, + }); + }, + [ACTIONS.COLLECTION_ITEMS_RESOLVE_FAILED]: (state, action) => { + const { ids } = action.data; + const { isResolvingCollectionById } = state; + const newResolving = Object.assign({}, isResolvingCollectionById); + ids.forEach(id => { + delete newResolving[id]; + }); + return Object.assign({}, state, { + ...state, + isResolvingCollectionById: newResolving, + error: action.data.message, + }); + }, + }, + defaultState +); + +export { collectionsReducer }; diff --git a/src/redux/reducers/publish.js b/src/redux/reducers/publish.js index 794c43d..e89564d 100644 --- a/src/redux/reducers/publish.js +++ b/src/redux/reducers/publish.js @@ -22,11 +22,9 @@ type PublishState = { thumbnail_url: string, thumbnailPath: string, uploadThumbnailStatus: string, - thumbnailError: ?boolean, description: string, language: string, - releaseTime: ?number, - releaseTimeEdited: ?number, + releaseTime: ?string, channel: string, channelId: ?string, name: string, @@ -57,11 +55,9 @@ const defaultState: PublishState = { thumbnail_url: '', thumbnailPath: '', uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN, - thumbnailError: undefined, description: '', language: '', releaseTime: undefined, - releaseTimeEdited: undefined, nsfw: false, channel: CHANNEL_ANONYMOUS, channelId: '', diff --git a/src/redux/selectors/claims.js b/src/redux/selectors/claims.js index d7b151d..cca04fd 100644 --- a/src/redux/selectors/claims.js +++ b/src/redux/selectors/claims.js @@ -95,6 +95,12 @@ export const makeSelectClaimIsPending = (uri: string) => } ); +export const makeSelectClaimIdForUri = (uri: string) => + createSelector( + selectClaimIdsByUri, + claimIds => claimIds[uri] + ); + export const selectReflectingById = createSelector( selectState, state => state.reflectingById @@ -531,6 +537,11 @@ export const selectFetchingMyChannels = createSelector( state => state.fetchingMyChannels ); +export const selectFetchingMyCollections = createSelector( + selectState, + state => state.fetchingMyCollections +); + export const selectMyChannelClaims = createSelector( selectState, selectClaimsById, @@ -557,6 +568,11 @@ export const selectMyChannelUrls = createSelector( claims => (claims ? claims.map(claim => claim.canonical_url || claim.permanent_url) : undefined) ); +export const selectMyCollectionIds = createSelector( + selectState, + state => state.myCollectionClaims +); + export const selectResolvingUris = createSelector( selectState, state => state.resolvingUris || [] @@ -917,3 +933,23 @@ export const makeSelectStakedLevelForChannelUri = (uri: string) => return level; } ); + +export const selectUpdatingCollection = createSelector( + selectState, + state => state.updatingCollection +); + +export const selectUpdateCollectionError = createSelector( + selectState, + state => state.updateCollectionError +); + +export const selectCreatingCollection = createSelector( + selectState, + state => state.creatingCollection +); + +export const selectCreateCollectionError = createSelector( + selectState, + state => state.createCollectionError +); diff --git a/src/redux/selectors/collections.js b/src/redux/selectors/collections.js new file mode 100644 index 0000000..92e2cf7 --- /dev/null +++ b/src/redux/selectors/collections.js @@ -0,0 +1,234 @@ +// @flow +import { createSelector } from 'reselect'; +import { selectMyCollectionIds } from 'redux/selectors/claims'; +import { parseURI } from 'lbryURI'; + +const selectState = (state: { collections: CollectionState }) => state.collections; + +export const selectSavedCollectionIds = createSelector( + selectState, + collectionState => collectionState.saved +); + +export const selectBuiltinCollections = createSelector( + selectState, + state => state.builtin +); +export const selectResolvedCollections = createSelector( + selectState, + state => state.resolved +); + +export const selectMyUnpublishedCollections = createSelector( + selectState, + state => state.unpublished +); + +export const selectMyEditedCollections = createSelector( + selectState, + state => state.edited +); + +export const selectPendingCollections = createSelector( + selectState, + state => state.pending +); + +export const makeSelectEditedCollectionForId = (id: string) => + createSelector( + selectMyEditedCollections, + eLists => eLists[id] + ); + +export const makeSelectPendingCollectionForId = (id: string) => + createSelector( + selectPendingCollections, + pending => pending[id] + ); + +export const makeSelectPublishedCollectionForId = (id: string) => + createSelector( + selectResolvedCollections, + rLists => rLists[id] + ); + +export const makeSelectUnpublishedCollectionForId = (id: string) => + createSelector( + selectMyUnpublishedCollections, + rLists => rLists[id] + ); + +export const makeSelectCollectionIsMine = (id: string) => + createSelector( + selectMyCollectionIds, + selectMyUnpublishedCollections, + selectBuiltinCollections, + (publicIds, privateIds, builtinIds) => { + return Boolean(publicIds.includes(id) || privateIds[id] || builtinIds[id]); + } + ); + +export const selectMyPublishedCollections = createSelector( + selectResolvedCollections, + selectPendingCollections, + selectMyEditedCollections, + selectMyCollectionIds, + (resolved, pending, edited, myIds) => { + // all resolved in myIds, plus those in pending and edited + const myPublishedCollections = Object.fromEntries( + Object.entries(pending).concat( + Object.entries(resolved).filter( + ([key, val]) => + myIds.includes(key) && + // $FlowFixMe + !pending[key] + ) + ) + ); + // now add in edited: + Object.entries(edited).forEach(([id, item]) => { + myPublishedCollections[id] = item; + }); + return myPublishedCollections; + } +); + +export const selectMyPublishedMixedCollections = createSelector( + selectMyPublishedCollections, + published => { + const myCollections = Object.fromEntries( + // $FlowFixMe + Object.entries(published).filter(([key, collection]) => { + // $FlowFixMe + return collection.type === 'collection'; + }) + ); + return myCollections; + } +); + +export const selectMyPublishedPlaylistCollections = createSelector( + selectMyPublishedCollections, + published => { + const myCollections = Object.fromEntries( + // $FlowFixMe + Object.entries(published).filter(([key, collection]) => { + // $FlowFixMe + return collection.type === 'playlist'; + }) + ); + return myCollections; + } +); + +export const makeSelectMyPublishedCollectionForId = (id: string) => + createSelector( + selectMyPublishedCollections, + myPublishedCollections => myPublishedCollections[id] + ); + +// export const selectSavedCollections = createSelector( +// selectResolvedCollections, +// selectSavedCollectionIds, +// (resolved, myIds) => { +// const mySavedCollections = Object.fromEntries( +// Object.entries(resolved).filter(([key, val]) => myIds.includes(key)) +// ); +// return mySavedCollections; +// } +// ); + +export const makeSelectIsResolvingCollectionForId = (id: string) => + createSelector( + selectState, + state => { + return state.isResolvingCollectionById[id]; + } + ); + +export const makeSelectCollectionForId = (id: string) => + createSelector( + selectBuiltinCollections, + selectResolvedCollections, + selectMyUnpublishedCollections, + selectMyEditedCollections, + selectPendingCollections, + (bLists, rLists, uLists, eLists, pLists) => { + const collection = bLists[id] || uLists[id] || eLists[id] || rLists[id] || pLists[id]; + return collection; + } + ); + +export const makeSelectCollectionForIdHasClaimUrl = (id: string, url: string) => + createSelector( + makeSelectCollectionForId(id), + collection => collection && collection.items.includes(url) + ); + +export const makeSelectUrlsForCollectionId = (id: string) => + createSelector( + makeSelectCollectionForId(id), + collection => collection && collection.items + ); + +export const makeSelectClaimIdsForCollectionId = (id: string) => + createSelector( + makeSelectCollectionForId(id), + collection => { + const items = (collection && collection.items) || []; + const ids = items.map(item => { + const { claimId } = parseURI(item); + return claimId; + }); + return ids; + } + ); + +export const makeSelectIndexForUrlInCollection = (url: string, id: string) => + createSelector( + makeSelectUrlsForCollectionId(id), + urls => { + const index = urls && urls.findIndex(u => u === url); + if (index > -1) { + return index; + } + return null; + } + ); + +export const makeSelectNextUrlForCollectionAndUrl = (id: string, url: string) => + createSelector( + makeSelectIndexForUrlInCollection(url, id), + makeSelectUrlsForCollectionId(id), + (index, urls) => { + if (urls && index >= -1) { + const url = urls[index + 1]; + if (url) { + return url; + } + } + return null; + } + ); + +export const makeSelectNameForCollectionId = (id: string) => + createSelector( + makeSelectCollectionForId(id), + collection => { + return (collection && collection.name) || ''; + } + ); + +export const makeSelectCountForCollectionId = (id: string) => + createSelector( + makeSelectCollectionForId(id), + collection => { + if (collection) { + if (collection.itemCount !== undefined) { + return collection.itemCount; + } + return collection.items.length; + } + return null; + } + );