lbry-desktop/ui/redux/selectors/collections.js
Rafael Saes 83dbe8ec7c
Playlists v2: Refactors, touch ups + Queue Mode (#1604)
* Playlists v2

* Style pass

* Change playlist items arrange icon

* Playlist card body open by default

* Refactor collectionEdit components

* Paginate & Refactor bid field

* Collection page changes

* Add Thumbnail optional

* Replace extra info for description on collection page

* Playlist card right below video on medium screen

* Allow editing private collections

* Add edit option to menus

* Allow deleting a public playlist but keeping a private version

* Add queue to Save menu, remove edit option from Builtin pages, show queue on playlists page

* Fix scroll to recent persisting on medium screen

* Fix adding to queue from menu

* Fixes for delete

* PublishList: delay mounting Items tab to prevent lock-up (#1783)

For a large list, the playlist publish form is unusable (super-slow typing) due to the entire list being mounted despite the tab is not active.
The full solution is still to paginate it, but for now, don't mount the tab until it is selected. Add a spinner to indicate something is loading. It's not prefect, but it's throwaway code anyway. At least we can fill in the fields properly now.

* Batch-resolve private collections (#1782)

* makeSelectClaimForClaimId --> selectClaimForClaimId

Move away from the problematic `makeSelect*`, especially in large loops.

* Batch-resolve private collections
1758

This alleviates the lock-up that is caused by large number of invidual resolves. There will still be some minor stutter due to the large DOM that React needs to handle -- that is logged in 1758 and will be handled separately.

At least the stutter is short (1-2s) and the app is still usable.
Private list items are being resolve individually, super slow if the list is large (>100). Published lists doesn't have this issue.
doFetchItemsInCollections contains most of the useful logic, but it isn't called for private/built-in lists because it's not an actual claim.
Tweaked doFetchItemsInCollections to handle private (UUID-based) collections.

* Use persisted state for floating player playlist card body
- I find it annoying being open everytime

* Fix removing edits from published playlist

* Fix scroll on mobile

* Allow going editing items from toast

* Fix ClaimShareButton

* Prevent edit/publish of builtin

* Fix async inside forEach

* Fix sync on queue edit

* Fix autoplayCountdown replay

* Fix deleting an item scrolling the playlist

* CreatedAt fixes

* Remove repost for now

* Anon publish fixes

* Fix mature case on floating

Co-authored-by: infinite-persistence <64950861+infinite-persistence@users.noreply.github.com>
2022-07-13 10:59:59 -03:00

459 lines
16 KiB
JavaScript

// @flow
import fromEntries from '@ungap/from-entries';
import { createSelector } from 'reselect';
import * as COLLECTIONS_CONSTS from 'constants/collections';
import moment from 'moment';
import {
selectMyCollectionIds,
selectClaimForUri,
selectClaimForClaimId,
selectChannelNameForId,
selectPermanentUrlForUri,
} from 'redux/selectors/claims';
import { parseURI } from 'util/lbryURI';
import { createCachedSelector } from 're-reselect';
import { selectUserCreationDate } from 'redux/selectors/user';
import { selectPlayingCollection } from 'redux/selectors/content';
import { selectCountForCollection } from 'util/collections';
import { isPermanentUrl } from 'util/claim';
type State = { collections: CollectionState };
const selectState = (state: State) => state.collections || {};
export const selectSavedCollectionIds = (state: State) => selectState(state).saved;
export const selectBuiltinCollections = (state: State) => selectState(state).builtin;
export const selectResolvedCollections = (state: State) => selectState(state).resolved;
export const selectMyUnpublishedCollections = (state: State) => selectState(state).unpublished;
export const selectMyEditedCollections = (state: State) => selectState(state).edited;
export const selectPendingCollections = (state: State) => selectState(state).pending;
export const selectIsResolvingCollectionById = (state: State) => selectState(state).isResolvingCollectionById;
export const selectQueueCollection = (state: State) => selectState(state).queue;
export const selectCurrentQueueList = createSelector(selectQueueCollection, (queue) => ({ queue }));
export const selectHasItemsInQueue = createSelector(selectQueueCollection, (queue) => queue.items.length > 0);
export const selectLastUsedCollection = createSelector(selectState, (state) => state.lastUsedCollection);
export const selectUnpublishedCollectionsList = createSelector(
selectMyUnpublishedCollections,
(unpublishedCollections) => Object.keys(unpublishedCollections || {})
);
export const selectHasCollections = createSelector(
selectUnpublishedCollectionsList,
selectMyCollectionIds,
(unpublished, publishedIds) => Boolean(unpublished.length > 0 || publishedIds.length > 0)
);
export const selectEditedCollectionForId = (state: State, id: string) => {
const editedCollections = selectMyEditedCollections(state);
return editedCollections[id];
};
export const selectCollectionHasEditsForId = (state: State, id: string) => {
const editedCollection = selectEditedCollectionForId(state, id);
return Boolean(editedCollection && !editedCollection.editsCleared);
};
export const selectPendingCollectionForId = (state: State, id: string) => {
const pendingCollections = selectPendingCollections(state);
return pendingCollections[id];
};
export const selectPublishedCollectionForId = (state: State, id: string) => {
const publishedCollections = selectResolvedCollections(state);
return publishedCollections[id];
};
export const selectPublishedCollectionClaimForId = (state: any, id: string) => {
const publishedCollection = selectPublishedCollectionForId(state, id);
if (publishedCollection) {
const claim = selectClaimForClaimId(state, id);
return claim;
}
return null;
};
export const selectPublishedCollectionChannelNameForId = (state: any, id: string) => {
const collectionClaim = selectPublishedCollectionClaimForId(state, id);
if (collectionClaim) {
const name = selectChannelNameForId(state, id);
return name;
}
return null;
};
export const selectUnpublishedCollectionForId = (state: State, id: string) => {
const unpublishedCollections = selectMyUnpublishedCollections(state);
return unpublishedCollections[id];
};
export const selectCollectionIsMine = createSelector(
(state, id) => id,
selectMyCollectionIds,
selectMyUnpublishedCollections,
selectBuiltinCollections,
selectCurrentQueueList,
(id, publicIds, privateIds, builtinIds, queue) =>
Boolean(publicIds.includes(id) || privateIds[id] || builtinIds[id] || queue[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 = 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]) => {
// $FlowFixMe
if (!item.editsCleared) {
myPublishedCollections[id] = item;
} else {
// $FlowFixMe
myPublishedCollections[id] = { ...myPublishedCollections[id], updatedAt: item.updatedAt };
}
});
return myPublishedCollections;
}
);
export const selectMyPublishedOnlyCollections = createSelector(
selectResolvedCollections,
selectPendingCollections,
selectMyCollectionIds,
(resolved, pending, myIds) => {
// all resolved in myIds, plus those in pending and edited
const myPublishedCollections = fromEntries(
Object.entries(pending).concat(
Object.entries(resolved).filter(
([key, val]) =>
myIds.includes(key) &&
// $FlowFixMe
!pending[key]
)
)
);
return myPublishedCollections;
}
);
export const selectCollectionValuesListForKey = createSelector(
(state, key) => key,
selectBuiltinCollections,
selectCurrentQueueList,
selectMyPublishedCollections,
selectMyUnpublishedCollections,
(key, builtin, queue, published, unpublished) => {
const myCollections = { builtin, queue, published, unpublished };
const collectionsForKey = myCollections[key];
// this is needed so Flow doesn't error saying it is mixed when this list is looped
const collectionValues: CollectionList = (Object.values(collectionsForKey): any);
return collectionValues;
}
);
export const selectIsMyCollectioPublishedForId = (state: State, id: string) => {
const publishedCollection = selectMyPublishedCollections(state);
return Boolean(publishedCollection[id]);
};
export const selectPublishedCollectionNotEditedForId = createSelector(
selectIsMyCollectioPublishedForId,
selectCollectionHasEditsForId,
(isPublished, hasEdits) => isPublished && !hasEdits
);
export const selectMyPublishedMixedCollections = createSelector(selectMyPublishedCollections, (published) => {
const myCollections = fromEntries(
// $FlowFixMe
Object.entries(published).filter(([key, collection]) => {
// $FlowFixMe
return collection.type === 'collection';
})
);
return myCollections;
});
export const selectMyPublishedPlaylistCollections = createSelector(selectMyPublishedCollections, (published) => {
const myCollections = fromEntries(
// $FlowFixMe
Object.entries(published).filter(([key, collection]) => {
// $FlowFixMe
return collection.type === 'playlist';
})
);
return myCollections;
});
export const selectMyPublishedCollectionForId = (state: State, id: string) => {
const myPublishedCollections = selectMyPublishedCollections(state);
return myPublishedCollections[id];
};
export const selectMyPublishedOnlyCollectionForId = (state: State, id: string) => {
const myPublishedCollections = selectMyPublishedOnlyCollections(state);
return myPublishedCollections[id];
};
export const selectMyPublishedCollectionCountForId = (state: State, id: string) => {
const publishedCollection = selectMyPublishedOnlyCollectionForId(state, id);
const count = selectCountForCollection(publishedCollection);
return count;
};
export const selectIsResolvingCollectionForId = (state: State, id: string) => {
const resolvingById = selectIsResolvingCollectionById(state);
return resolvingById[id];
};
export const selectCollectionForId = createSelector(
(state, id) => id,
selectBuiltinCollections,
selectResolvedCollections,
selectMyUnpublishedCollections,
selectMyEditedCollections,
selectPendingCollections,
selectCurrentQueueList,
(id, bLists, rLists, uLists, eLists, pLists, queue) => {
const edited = eLists[id] && !eLists[id].editsCleared ? eLists[id] : undefined;
const collection = bLists[id] || uLists[id] || edited || pLists[id] || rLists[id] || queue[id];
return collection;
}
);
export const selectIsCollectionBuiltInForId = (state: State, id: string) => {
const builtin = selectBuiltinCollections(state);
return builtin[id];
};
export const selectClaimSavedForUrl = (state: State, url: string) => {
const [bLists, myRLists, uLists, eLists, pLists] = [
selectBuiltinCollections(state),
selectMyPublishedCollections(state),
selectMyUnpublishedCollections(state),
selectMyEditedCollections(state),
selectPendingCollections(state),
];
const collections = [bLists, uLists, eLists, myRLists, pLists];
// $FlowFixMe
return collections.some((list) => Object.values(list).some(({ items }) => items?.some((item) => item === url)));
};
export const selectClaimInCollectionsForUrl = (state: State, url: string) => {
const queue = selectQueueCollection(state);
const claimInQueue = queue.items.some((item) => item === url);
const claimSaved = selectClaimSavedForUrl(state, url);
return claimSaved && claimInQueue;
};
export const selectCollectionForIdHasClaimUrl = (state: State, id: string, uri: string) => {
// $FlowFixMe
const url = isPermanentUrl(uri) ? uri : selectPermanentUrlForUri(state, uri);
const collection = selectCollectionForId(state, id);
return collection && collection.items.includes(url);
};
export const selectUrlsForCollectionId = (state: State, id: string) => {
const collection = selectCollectionForId(state, id);
// -- sanitize -- > in case non-urls got added into a collection: only select string types
// to avoid general app errors trying to use its uri
return collection && collection.items.filter((item) => typeof item === 'string');
};
export const selectBrokenUrlsForCollectionId = (state: State, id: string) => {
const collection = selectCollectionForId(state, id);
// Allows removing non-standard uris from a collection
return collection && collection.items.filter((item) => typeof item !== 'string');
};
export const selectFirstItemUrlForCollection = createSelector(
selectUrlsForCollectionId,
(collectionItemUrls) => collectionItemUrls?.length > 0 && collectionItemUrls[0]
);
export const selectCollectionLengthForId = (state: State, id: string) => {
const urls = selectUrlsForCollectionId(state, id);
return urls?.length || 0;
};
export const selectCollectionIsEmptyForId = (state: State, id: string) => {
const length = selectCollectionLengthForId(state, id);
return length === 0;
};
export const selectAreBuiltinCollectionsEmpty = (state: State) => {
const notEmpty = COLLECTIONS_CONSTS.BUILTIN_PLAYLISTS.some((collectionKey) => {
if (collectionKey !== COLLECTIONS_CONSTS.QUEUE_ID) {
const length = selectCollectionLengthForId(state, collectionKey);
return length > 0;
}
});
return !notEmpty;
};
export const selectClaimIdsForCollectionId = createSelector(selectCollectionForId, (collection) => {
const items = (collection && collection.items) || [];
const ids = items.map((item) => {
const { claimId } = parseURI(item);
return claimId;
});
return ids;
});
export const selectIndexForUrlInCollection = createSelector(
(state, url, id, ignoreShuffle) => ignoreShuffle,
(state, url, id) => id,
(state, url, id) => selectUrlsForCollectionId(state, id),
(state, url) => url,
(state) => selectPlayingCollection(state),
selectClaimForUri,
(ignoreShuffle, id, urls, url, playingCollection, claim) => {
const { collectionId: playingCollectionId, shuffle } = playingCollection;
const shuffleUrls = !ignoreShuffle && shuffle && playingCollectionId === id && shuffle.newUrls;
const listUrls = shuffleUrls || urls;
const index = listUrls && listUrls.findIndex((u) => u === url);
if (index > -1) {
return index;
} else if (claim) {
const index = listUrls && listUrls.findIndex((u) => u === claim.permanent_url);
if (index > -1) return index;
}
return null;
}
);
export const selectPreviousUrlForCollectionAndUrl = createCachedSelector(
(state, url, id) => id,
(state) => selectPlayingCollection(state),
(state, url, id) => selectIndexForUrlInCollection(state, url, id),
(state, url, id) => selectUrlsForCollectionId(state, id),
(id, playingCollection, index, urls) => {
const { collectionId: playingCollectionId, shuffle, loop } = playingCollection;
const loopList = loop && playingCollectionId === id;
const shuffleUrls = shuffle && playingCollectionId === id && shuffle.newUrls;
const listUrls = shuffleUrls || urls;
if (index > -1 && listUrls) {
let nextUrl;
if (index === 0 && loopList) {
nextUrl = listUrls[listUrls.length - 1];
} else {
nextUrl = listUrls[index - 1];
}
return nextUrl || null;
} else {
return null;
}
}
)((state, url, id) => `${String(url)}:${String(id)}`);
export const selectNextUrlForCollectionAndUrl = createCachedSelector(
(state, url, id) => id,
(state) => selectPlayingCollection(state),
(state, url, id) => selectIndexForUrlInCollection(state, url, id),
(state, url, id) => selectUrlsForCollectionId(state, id),
(id, playingCollection, index, urls) => {
const { collectionId: playingCollectionId, shuffle, loop } = playingCollection;
const loopList = loop && playingCollectionId === id;
const shuffleUrls = shuffle && playingCollectionId === id && shuffle.newUrls;
const listUrls = shuffleUrls || urls;
if (index > -1 && listUrls) {
// We'll get the next playble url
let remainingUrls = listUrls.slice(index + 1);
if (!remainingUrls.length && loopList) {
remainingUrls = listUrls.slice(0);
}
const nextUrl = remainingUrls && remainingUrls[0];
return nextUrl || null;
} else {
return null;
}
}
)((state, url, id) => `${String(url)}:${String(id)}`);
export const selectNameForCollectionId = createSelector(
selectCollectionForId,
(collection) => (collection && collection.name) || ''
);
export const selectThumbnailForCollectionId = (state: State, id: string) => {
const collection = selectCollectionForId(state, id);
return collection.thumbnail?.url;
};
export const selectUpdatedAtForCollectionId = createSelector(
selectCollectionForId,
selectUserCreationDate,
selectEditedCollectionForId,
(collection, userCreatedAt, edited) => {
const collectionUpdatedAt = (edited?.updatedAt || collection.updatedAt) * 1000;
const userCreationDate = moment(userCreatedAt).format('MMMM DD YYYY');
const collectionUpdatedDate = moment(collectionUpdatedAt).format('MMMM DD YYYY');
// Collection updated time can't be older than account creation date
if (moment(collectionUpdatedDate).diff(moment(userCreationDate)) < 0) {
return userCreatedAt;
}
return collectionUpdatedAt || '';
}
);
export const selectCreatedAtForCollectionId = (state: State, id: string) => {
const collection = selectCollectionForId(state, id);
const isBuiltin = COLLECTIONS_CONSTS.BUILTIN_PLAYLISTS.includes(id);
if (isBuiltin) {
const userCreatedAt = selectUserCreationDate(state);
return userCreatedAt;
}
if (collection.createdAt) return collection.createdAt * 1000;
const publishedClaim = selectPublishedCollectionClaimForId(state, id);
if (publishedClaim) return publishedClaim.meta?.creation_timestamp * 1000;
return null;
};
export const selectCountForCollectionId = createSelector(selectCollectionForId, (collection) =>
selectCountForCollection(collection)
);
export const selectIsCollectionPrivateForId = createSelector(
(state, id) => id,
selectBuiltinCollections,
selectMyUnpublishedCollections,
selectCurrentQueueList,
(id, builtinById, unpublishedById, queue) => Boolean(builtinById[id] || unpublishedById[id] || queue[id])
);