wip

clean

clean

review

wip

wallet sync

wip

collection publishing

build

refactor, publishing, pending, editing

wip

wip

fetch collections on resolve

select collections or playlists

build

return edit success

fix collection claimId selector

small rename

flow type fixes

collection edit params type param and flowtypes
This commit is contained in:
zeppi 2021-02-06 02:03:10 -05:00 committed by jessopb
parent 757e8c24ec
commit 8d0f9c18fd
17 changed files with 1650 additions and 18 deletions

View file

@ -1,11 +1,15 @@
// @flow // @flow
declare type Claim = StreamClaim | ChannelClaim; declare type Claim = StreamClaim | ChannelClaim | CollectionClaim;
declare type ChannelClaim = GenericClaim & { declare type ChannelClaim = GenericClaim & {
value: ChannelMetadata, value: ChannelMetadata,
}; };
declare type CollectionClaim = GenericClaim & {
value: CollectionMetadata,
};
declare type StreamClaim = GenericClaim & { declare type StreamClaim = GenericClaim & {
value: StreamMetadata, value: StreamMetadata,
}; };
@ -30,7 +34,7 @@ declare type GenericClaim = {
short_url: string, // permanent_url with short id, no channel short_url: string, // permanent_url with short id, no channel
txid: string, // unique tx id txid: string, // unique tx id
type: 'claim' | 'update' | 'support', type: 'claim' | 'update' | 'support',
value_type: 'stream' | 'channel', value_type: 'stream' | 'channel' | 'collection',
signing_channel?: ChannelClaim, signing_channel?: ChannelClaim,
reposted_claim?: GenericClaim, reposted_claim?: GenericClaim,
repost_channel_url?: string, repost_channel_url?: string,
@ -74,6 +78,10 @@ declare type ChannelMetadata = GenericMetadata & {
featured?: Array<string>, featured?: Array<string>,
}; };
declare type CollectionMetadata = GenericMetadata & {
claims: Array<string>,
}
declare type StreamMetadata = GenericMetadata & { declare type StreamMetadata = GenericMetadata & {
license?: string, // License "title" ex: Creative Commons, Custom copyright license?: string, // License "title" ex: Creative Commons, Custom copyright
license_url?: string, // Link to full license license_url?: string, // Link to full license
@ -136,3 +144,71 @@ declare type PurchaseReceipt = {
txid: string, txid: string,
type: 'purchase', 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<string>,
replace?: boolean,
languages?: Array<string>,
locations?: Array<string>,
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<string>,
languages?: Array<string>,
}
declare type CollectionUpdateParams = {
claim_id: string,
claim_ids?: Array<string>,
bid?: string,
title?: string,
cover_url?: string,
thumbnail_url?: string,
description?: string,
website_url?: string,
email?: string,
tags?: Array<string>,
replace?: boolean,
languages?: Array<string>,
locations?: Array<string>,
blocking?: boolean,
}
declare type CollectionPublishParams = {
name: string,
bid: string,
claim_ids: Array<string>,
blocking?: true,
title?: string,
thumbnail_url?: string,
description?: string,
tags?: Array<string>,
languages?: Array<string>,
}

40
dist/flow-typed/Collections.js vendored Normal file
View file

@ -0,0 +1,40 @@
declare type CollectionUpdateParams = {
remove?: boolean,
claims?: Array<Claim>,
name?: string,
order?: { from: number, to: number },
}
declare type Collection = {
id: string,
items: Array<?string>,
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<string>,
isResolvingCollectionById: { [string]: boolean },
error?: string | null,
};
declare type CollectionGroup = {
[string]: Collection,
}
declare type CollectionEditParams = {
claims?: Array<Claim>,
remove?: boolean,
claimIds?: Array<string>,
replace?: boolean,
order?: { from: number, to: number },
type?: string,
}

View file

@ -170,6 +170,37 @@ declare type ChannelSignResponse = {
signing_ts: string, signing_ts: string,
}; };
declare type CollectionCreateResponse = {
outputs: Array<Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
}
declare type CollectionListResponse = {
items: Array<Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type CollectionResolveResponse = {
items: Array<Claim>,
total_items: number,
};
declare type CollectionResolveOptions = {
claim_id: string,
};
declare type CollectionListOptions = {
page: number,
page_size: number,
resolve?: boolean,
};
declare type FileListResponse = { declare type FileListResponse = {
items: Array<FileListItem>, items: Array<FileListItem>,
page: number, page: number,
@ -288,6 +319,10 @@ declare type LbryTypes = {
support_abandon: (params: {}) => Promise<SupportAbandonResponse>, support_abandon: (params: {}) => Promise<SupportAbandonResponse>,
stream_repost: (params: StreamRepostOptions) => Promise<StreamRepostResponse>, stream_repost: (params: StreamRepostOptions) => Promise<StreamRepostResponse>,
purchase_list: (params: PurchaseListOptions) => Promise<PurchaseListResponse>, purchase_list: (params: PurchaseListOptions) => Promise<PurchaseListResponse>,
collection_resolve: (params: CollectionResolveOptions) => Promise<CollectionResolveResponse>,
collection_list: (params: CollectionListOptions) => Promise<CollectionListResponse>,
collection_create: (params: {}) => Promise<CollectionCreateResponse>,
collection_update: (params: {}) => Promise<CollectionCreateResponse>,
// File fetching and manipulation // File fetching and manipulation
file_list: (params: {}) => Promise<FileListResponse>, file_list: (params: {}) => Promise<FileListResponse>,

80
flow-typed/Claim.js vendored
View file

@ -1,11 +1,15 @@
// @flow // @flow
declare type Claim = StreamClaim | ChannelClaim; declare type Claim = StreamClaim | ChannelClaim | CollectionClaim;
declare type ChannelClaim = GenericClaim & { declare type ChannelClaim = GenericClaim & {
value: ChannelMetadata, value: ChannelMetadata,
}; };
declare type CollectionClaim = GenericClaim & {
value: CollectionMetadata,
};
declare type StreamClaim = GenericClaim & { declare type StreamClaim = GenericClaim & {
value: StreamMetadata, value: StreamMetadata,
}; };
@ -30,7 +34,7 @@ declare type GenericClaim = {
short_url: string, // permanent_url with short id, no channel short_url: string, // permanent_url with short id, no channel
txid: string, // unique tx id txid: string, // unique tx id
type: 'claim' | 'update' | 'support', type: 'claim' | 'update' | 'support',
value_type: 'stream' | 'channel', value_type: 'stream' | 'channel' | 'collection',
signing_channel?: ChannelClaim, signing_channel?: ChannelClaim,
reposted_claim?: GenericClaim, reposted_claim?: GenericClaim,
repost_channel_url?: string, repost_channel_url?: string,
@ -74,6 +78,10 @@ declare type ChannelMetadata = GenericMetadata & {
featured?: Array<string>, featured?: Array<string>,
}; };
declare type CollectionMetadata = GenericMetadata & {
claims: Array<string>,
}
declare type StreamMetadata = GenericMetadata & { declare type StreamMetadata = GenericMetadata & {
license?: string, // License "title" ex: Creative Commons, Custom copyright license?: string, // License "title" ex: Creative Commons, Custom copyright
license_url?: string, // Link to full license license_url?: string, // Link to full license
@ -136,3 +144,71 @@ declare type PurchaseReceipt = {
txid: string, txid: string,
type: 'purchase', 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<string>,
replace?: boolean,
languages?: Array<string>,
locations?: Array<string>,
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<string>,
languages?: Array<string>,
}
declare type CollectionUpdateParams = {
claim_id: string,
claim_ids?: Array<string>,
bid?: string,
title?: string,
cover_url?: string,
thumbnail_url?: string,
description?: string,
website_url?: string,
email?: string,
tags?: Array<string>,
replace?: boolean,
languages?: Array<string>,
locations?: Array<string>,
blocking?: boolean,
}
declare type CollectionPublishParams = {
name: string,
bid: string,
claim_ids: Array<string>,
blocking?: true,
title?: string,
thumbnail_url?: string,
description?: string,
tags?: Array<string>,
languages?: Array<string>,
}

41
flow-typed/Collections.js vendored Normal file
View file

@ -0,0 +1,41 @@
declare type CollectionUpdateParams = {
remove?: boolean,
claims?: Array<Claim>,
name?: string,
order?: { from: number, to: number },
}
declare type Collection = {
id: string,
items: Array<?string>,
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<string>,
isResolvingCollectionById: { [string]: boolean },
error?: string | null,
};
declare type CollectionGroup = {
[string]: Collection,
}
declare type CollectionEditParams = {
claims?: Array<Claim>,
remove?: boolean,
claimIds?: Array<string>,
replace?: boolean,
order?: { from: number, to: number },
type?: string,
name?: string,
}

35
flow-typed/Lbry.js vendored
View file

@ -170,6 +170,37 @@ declare type ChannelSignResponse = {
signing_ts: string, signing_ts: string,
}; };
declare type CollectionCreateResponse = {
outputs: Array<Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
}
declare type CollectionListResponse = {
items: Array<Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type CollectionResolveResponse = {
items: Array<Claim>,
total_items: number,
};
declare type CollectionResolveOptions = {
claim_id: string,
};
declare type CollectionListOptions = {
page: number,
page_size: number,
resolve?: boolean,
};
declare type FileListResponse = { declare type FileListResponse = {
items: Array<FileListItem>, items: Array<FileListItem>,
page: number, page: number,
@ -288,6 +319,10 @@ declare type LbryTypes = {
support_abandon: (params: {}) => Promise<SupportAbandonResponse>, support_abandon: (params: {}) => Promise<SupportAbandonResponse>,
stream_repost: (params: StreamRepostOptions) => Promise<StreamRepostResponse>, stream_repost: (params: StreamRepostOptions) => Promise<StreamRepostResponse>,
purchase_list: (params: PurchaseListOptions) => Promise<PurchaseListResponse>, purchase_list: (params: PurchaseListOptions) => Promise<PurchaseListResponse>,
collection_resolve: (params: CollectionResolveOptions) => Promise<CollectionResolveResponse>,
collection_list: (params: CollectionListOptions) => Promise<CollectionListResponse>,
collection_create: (params: {}) => Promise<CollectionCreateResponse>,
collection_update: (params: {}) => Promise<CollectionCreateResponse>,
// File fetching and manipulation // File fetching and manipulation
file_list: (params: {}) => Promise<FileListResponse>, file_list: (params: {}) => Promise<FileListResponse>,

View file

@ -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_STARTED = 'FETCH_CHANNEL_LIST_STARTED';
export const FETCH_CHANNEL_LIST_COMPLETED = 'FETCH_CHANNEL_LIST_COMPLETED'; export const FETCH_CHANNEL_LIST_COMPLETED = 'FETCH_CHANNEL_LIST_COMPLETED';
export const FETCH_CHANNEL_LIST_FAILED = 'FETCH_CHANNEL_LIST_FAILED'; 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_STARTED = 'CREATE_CHANNEL_STARTED';
export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED'; export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED';
export const CREATE_CHANNEL_FAILED = 'CREATE_CHANNEL_FAILED'; 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_STARTED = 'IMPORT_CHANNEL_STARTED';
export const IMPORT_CHANNEL_COMPLETED = 'IMPORT_CHANNEL_COMPLETED'; export const IMPORT_CHANNEL_COMPLETED = 'IMPORT_CHANNEL_COMPLETED';
export const IMPORT_CHANNEL_FAILED = 'IMPORT_CHANNEL_FAILED'; 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_STARTED = 'PUBLISH_STARTED';
export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED'; export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED';
export const PUBLISH_FAILED = 'PUBLISH_FAILED'; 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_COMPLETED = 'CLAIM_REPOST_COMPLETED';
export const CLAIM_REPOST_FAILED = 'CLAIM_REPOST_FAILED'; export const CLAIM_REPOST_FAILED = 'CLAIM_REPOST_FAILED';
export const CLEAR_REPOST_ERROR = 'CLEAR_REPOST_ERROR'; 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_STARTED = 'CHECK_PUBLISH_NAME_STARTED';
export const CHECK_PUBLISH_NAME_COMPLETED = 'CHECK_PUBLISH_NAME_COMPLETED'; export const CHECK_PUBLISH_NAME_COMPLETED = 'CHECK_PUBLISH_NAME_COMPLETED';
export const UPDATE_PENDING_CLAIMS = 'UPDATE_PENDING_CLAIMS'; 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_COMPLETED = 'PURCHASE_LIST_COMPLETED';
export const PURCHASE_LIST_FAILED = 'PURCHASE_LIST_FAILED'; 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 // Comments
export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED'; export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED';
export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED'; export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED';

View file

@ -0,0 +1,5 @@
export const COLLECTION_ID = 'colid';
export const COLLECTION_INDEX = 'colindex';
export const WATCH_LATER_ID = 'watchlater';
export const FAVORITES_ID = 'favorites';

View file

@ -12,6 +12,7 @@ import * as TXO_LIST from 'constants/txo_list';
import * as SPEECH_URLS from 'constants/speech_urls'; import * as SPEECH_URLS from 'constants/speech_urls';
import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import * as DAEMON_SETTINGS from 'constants/daemon_settings';
import * as SHARED_PREFERENCES from 'constants/shared_preferences'; 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 { DEFAULT_KNOWN_TAGS, DEFAULT_FOLLOWED_TAGS, MATURE_TAGS } from 'constants/tags';
import Lbry, { apiCall } from 'lbry'; import Lbry, { apiCall } from 'lbry';
import LbryFirst from 'lbry-first'; import LbryFirst from 'lbry-first';
@ -35,6 +36,7 @@ export {
MATURE_TAGS, MATURE_TAGS,
SPEECH_URLS, SPEECH_URLS,
SHARED_PREFERENCES, SHARED_PREFERENCES,
COLLECTIONS_CONSTS,
}; };
// common // common
@ -57,6 +59,13 @@ export { buildSharedStateMiddleware } from 'redux/middleware/shared-state';
// actions // actions
export { doToast, doDismissToast, doError, doDismissError } from 'redux/actions/notifications'; export { doToast, doDismissToast, doError, doDismissError } from 'redux/actions/notifications';
export {
doLocalCollectionCreate,
doFetchItemsInCollection,
doFetchItemsInCollections,
doCollectionEdit,
doLocalCollectionDelete,
} from 'redux/actions/collections';
export { export {
doFetchClaimsByChannel, doFetchClaimsByChannel,
@ -66,6 +75,7 @@ export {
doResolveUris, doResolveUris,
doResolveUri, doResolveUri,
doFetchChannelListMine, doFetchChannelListMine,
doFetchCollectionListMine,
doCreateChannel, doCreateChannel,
doUpdateChannel, doUpdateChannel,
doClaimSearch, doClaimSearch,
@ -76,6 +86,8 @@ export {
doCheckPublishNameAvailability, doCheckPublishNameAvailability,
doPurchaseList, doPurchaseList,
doCheckPendingClaims, doCheckPendingClaims,
doCollectionPublish,
doCollectionPublishUpdate,
} from 'redux/actions/claims'; } from 'redux/actions/claims';
export { doClearPurchasedUriSuccess, doPurchaseUri, doFileGet } from 'redux/actions/file'; export { doClearPurchasedUriSuccess, doPurchaseUri, doFileGet } from 'redux/actions/file';
@ -140,11 +152,34 @@ export { fileInfoReducer } from 'redux/reducers/file_info';
export { notificationsReducer } from 'redux/reducers/notifications'; export { notificationsReducer } from 'redux/reducers/notifications';
export { publishReducer } from 'redux/reducers/publish'; export { publishReducer } from 'redux/reducers/publish';
export { walletReducer } from 'redux/reducers/wallet'; export { walletReducer } from 'redux/reducers/wallet';
export { collectionsReducer } from 'redux/reducers/collections';
// selectors // selectors
export { makeSelectContentPositionForUri } from 'redux/selectors/content'; export { makeSelectContentPositionForUri } from 'redux/selectors/content';
export { selectToast, selectError } from 'redux/selectors/notifications'; 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,
makeSelectIsResolvingCollectionForId,
makeSelectNextUrlForCollection,
} from 'redux/selectors/collections';
export { export {
makeSelectClaimForUri, makeSelectClaimForUri,
@ -209,6 +244,8 @@ export {
selectAllMyClaimsByOutpoint, selectAllMyClaimsByOutpoint,
selectMyClaimsOutpoints, selectMyClaimsOutpoints,
selectFetchingMyChannels, selectFetchingMyChannels,
selectFetchingMyCollections,
selectMyCollectionIds,
selectMyChannelClaims, selectMyChannelClaims,
selectResolvingUris, selectResolvingUris,
selectPlayingUri, selectPlayingUri,
@ -237,6 +274,11 @@ export {
selectFetchingMyPurchasesError, selectFetchingMyPurchasesError,
selectMyPurchasesCount, selectMyPurchasesCount,
selectPurchaseUriSuccess, selectPurchaseUriSuccess,
makeSelectClaimIdForUri,
selectUpdatingCollection,
selectUpdateCollectionError,
selectCreatingCollection,
selectCreateCollectionError,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
export { export {

View file

@ -90,6 +90,10 @@ const Lbry: LbryTypes = {
support_create: params => daemonCallWithResult('support_create', params), support_create: params => daemonCallWithResult('support_create', params),
support_list: params => daemonCallWithResult('support_list', params), support_list: params => daemonCallWithResult('support_list', params),
stream_repost: params => daemonCallWithResult('stream_repost', 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 fetching and manipulation
file_list: (params = {}) => daemonCallWithResult('file_list', params), file_list: (params = {}) => daemonCallWithResult('file_list', params),

View file

@ -11,13 +11,20 @@ import {
selectMyChannelClaims, selectMyChannelClaims,
selectPendingIds, selectPendingIds,
selectClaimsById, selectClaimsById,
makeSelectClaimForClaimId,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { doFetchTxoPage } from 'redux/actions/wallet'; import { doFetchTxoPage } from 'redux/actions/wallet';
import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { creditsToString } from 'util/format-credits'; import { creditsToString } from 'util/format-credits';
import { batchActions } from 'util/batch-actions'; import { batchActions } from 'util/batch-actions';
import { createNormalizedClaimSearchKey } from 'util/claim'; import { createNormalizedClaimSearchKey } from 'util/claim';
import { PAGE_SIZE } from 'constants/claim'; import { PAGE_SIZE } from 'constants/claim';
import {
makeSelectEditedCollectionForId,
selectPendingCollections,
} from 'redux/selectors/collections';
import { doFetchItemsInCollection, doFetchItemsInCollections } from 'redux/actions/collections';
type ResolveEntries = Array<[string, GenericClaim]>; type ResolveEntries = Array<[string, GenericClaim]>;
@ -61,12 +68,16 @@ export function doResolveUris(
stream: ?StreamClaim, stream: ?StreamClaim,
channel: ?ChannelClaim, channel: ?ChannelClaim,
claimsInChannel: ?number, claimsInChannel: ?number,
collection: ?CollectionClaim,
}, },
} = {}; } = {};
const collectionIds: Array<string> = [];
return Lbry.resolve({ urls: urisToResolve, ...options }).then( return Lbry.resolve({ urls: urisToResolve, ...options }).then(
async(result: ResolveResponse) => { async(result: ResolveResponse) => {
let repostedResults = {}; let repostedResults = {};
const collectionClaimIdsToResolve = [];
const repostsToResolve = []; const repostsToResolve = [];
const fallbackResolveInfo = { const fallbackResolveInfo = {
stream: null, stream: null,
@ -80,6 +91,7 @@ export function doResolveUris(
// https://github.com/facebook/flow/issues/2221 // https://github.com/facebook/flow/issues/2221
if (uriResolveInfo) { if (uriResolveInfo) {
if (uriResolveInfo.error) { if (uriResolveInfo.error) {
// $FlowFixMe
resolveInfo[uri] = { ...fallbackResolveInfo }; resolveInfo[uri] = { ...fallbackResolveInfo };
} else { } else {
if (checkReposts) { if (checkReposts) {
@ -96,6 +108,10 @@ export function doResolveUris(
result.channel = uriResolveInfo; result.channel = uriResolveInfo;
// $FlowFixMe // $FlowFixMe
result.claimsInChannel = uriResolveInfo.meta.claims_in_channel; result.claimsInChannel = uriResolveInfo.meta.claims_in_channel;
} else if (uriResolveInfo.value_type === 'collection') {
result.collection = uriResolveInfo;
// $FlowFixMe
collectionIds.push(uriResolveInfo.claim_id);
} else { } else {
result.stream = uriResolveInfo; result.stream = uriResolveInfo;
if (uriResolveInfo.signing_channel) { if (uriResolveInfo.signing_channel) {
@ -127,6 +143,13 @@ export function doResolveUris(
type: ACTIONS.RESOLVE_URIS_COMPLETED, type: ACTIONS.RESOLVE_URIS_COMPLETED,
data: { resolveInfo }, data: { resolveInfo },
}); });
if (collectionIds.length) {
dispatch(doFetchItemsInCollections({ collectionIds: collectionIds, pageSize: 5 }));
}
// now collection claims are added, get their stuff
// if collections: doResolveCollections(claimIds)
return result; return result;
} }
); );
@ -573,11 +596,43 @@ 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,
})
);
// update or fetch collections?
};
const failure = error => {
dispatch({
type: ACTIONS.FETCH_COLLECTION_LIST_FAILED,
data: error,
});
};
Lbry.collection_list({ page, page_size: pageSize, resolve_claims: 1 }).then(callback, failure);
};
}
export function doClaimSearch( export function doClaimSearch(
options: { options: {
page_size: number, page_size: number,
page: number, page: number,
no_totals: boolean, no_totals?: boolean,
any_tags?: Array<string>, any_tags?: Array<string>,
claim_ids?: Array<string>, claim_ids?: Array<string>,
channel_ids?: Array<string>, channel_ids?: Array<string>,
@ -618,7 +673,8 @@ export function doClaimSearch(
pageSize: options.page_size, pageSize: options.page_size,
}, },
}); });
return true; // was return true
return resolveInfo;
}; };
const failure = err => { const failure = err => {
@ -679,6 +735,128 @@ 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<string>,
languages?: Array<string>,
claims: Array<string>,
},
localId: string
) {
return (dispatch: Dispatch) => {
// $FlowFixMe
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,
},
// shift unpublished collection to pending collection with new publish id
// recent publish won't resolve this second. handle it in checkPending
{
type: ACTIONS.COLLECTION_PENDING,
data: { localId: localId, claimId: collectionClaim.claim_id },
},
{
type: ACTIONS.UPDATE_PENDING_CLAIMS,
data: {
claims: [collectionClaim],
},
}
)
);
dispatch(doCheckPendingClaims());
dispatch(doFetchCollectionListMine(1, 10));
resolve(collectionClaim);
}
function failure(error) {
dispatch({
type: ACTIONS.COLLECTION_PUBLISH_FAILED,
data: {
error: error.message,
},
});
}
Lbry.collection_create(options).then(success, failure);
});
};
}
export function doCollectionPublishUpdate(options: {
bid?: string,
blocking?: true,
title?: string,
thumbnail_url?: string,
description?: string,
claim_id: string,
tags?: Array<string>,
languages?: Array<string>,
}) {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
// select claim forclaim_id
// get publish params from claim
// $FlowFixMe
const collectionClaim = makeSelectClaimForClaimId(options.claim_id)(state);
// TODO: add old claim entries to params
const editItems = makeSelectEditedCollectionForId(options.claim_id)(state);
const oldParams: CollectionUpdateParams = {
bid: collectionClaim.amount,
};
// $FlowFixMe
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.UPDATE_PENDING_CLAIMS,
data: {
claims: [collectionClaim],
},
});
dispatch(doFetchCollectionListMine(1, 10));
resolve(collectionClaim);
}
function failure(error) {
dispatch({
type: ACTIONS.COLLECTION_PUBLISH_UPDATE_FAILED,
data: {
error: error.message,
},
});
}
Lbry.collection_update(options).then(success, failure);
});
};
}
export function doCheckPublishNameAvailability(name: string) { export function doCheckPublishNameAvailability(name: string) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
dispatch({ dispatch({
@ -750,6 +928,7 @@ export const doCheckPendingClaims = (onConfirmed: Function) => (
const checkClaimList = () => { const checkClaimList = () => {
const state = getState(); const state = getState();
const pendingIdSet = new Set(selectPendingIds(state)); const pendingIdSet = new Set(selectPendingIds(state));
const pendingCollections = selectPendingCollections(state);
Lbry.claim_list({ page: 1, page_size: 10 }) Lbry.claim_list({ page: 1, page_size: 10 })
.then(result => { .then(result => {
const claims = result.items; const claims = result.items;
@ -758,6 +937,9 @@ export const doCheckPendingClaims = (onConfirmed: Function) => (
const { claim_id: claimId } = claim; const { claim_id: claimId } = claim;
if (claim.confirmations > 0 && pendingIdSet.has(claimId)) { if (claim.confirmations > 0 && pendingIdSet.has(claimId)) {
pendingIdSet.delete(claimId); pendingIdSet.delete(claimId);
if (Object.keys(pendingCollections).includes(claim.claim_id)) {
dispatch(doFetchItemsInCollection({ collectionId: claim.claim_id }));
}
claimsToConfirm.push(claim); claimsToConfirm.push(claim);
if (onConfirmed) { if (onConfirmed) {
onConfirmed(claim); onConfirmed(claim);

View file

@ -0,0 +1,459 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { v4 as uuid } from 'uuid';
import Lbry from 'lbry';
import { doClaimSearch } from 'redux/actions/claims';
import { makeSelectClaimForClaimId } from 'redux/selectors/claims';
import {
makeSelectCollectionForId,
// makeSelectPublishedCollectionForId, // for "save" or "copy" action
makeSelectMyPublishedCollectionForId,
makeSelectUnpublishedCollectionForId,
makeSelectEditedCollectionForId,
} from 'redux/selectors/collections';
const WATCH_LATER_ID = 'watchlater';
const FAVORITES_ID = 'favorites';
const BUILTIN_LISTS = [WATCH_LATER_ID, FAVORITES_ID];
const getTimestamp = () => {
return Math.floor(Date.now() / 1000);
};
// maybe take items param
export const doLocalCollectionCreate = (
name: string,
collectionItems: 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,
},
},
});
};
export const doLocalCollectionDelete = (id: string) => (dispatch: Dispatch) => {
return dispatch({
type: ACTIONS.COLLECTION_DELETE,
data: {
id: id,
},
});
};
// 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<string>,
pageSize?: number,
},
resolveStartedCallback?: () => void
) => async (dispatch: Dispatch, getState: GetState) => {
let state = getState();
// for each collection id,
// make sure the collection is resolved, the items are resolved, and build the collection objects
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) {
let claimSearchOptions = { claim_ids: collectionIdsToSearch, page: 1, page_size: 9999 };
await dispatch(doClaimSearch(claimSearchOptions));
}
const invalidCollectionIds = [];
const stateAfterClaimSearch = getState();
async function resolveCollectionItems(claimId, totalItems, pageSize) {
// take [ {}, {} ], return {}
// only need items [ Claim... ] and total_items
const mergeResults = (arrayOfResults: Array<{ items: any, total_items: number }>) => {
const mergedResults: { items: Array<?Claim>, total_items: number } = {
items: [],
total_items: 0,
};
arrayOfResults.forEach(result => {
mergedResults.items = mergedResults.items.concat(result.items);
mergedResults.total_items = result.total_items;
});
return mergedResults;
};
try {
const BATCH_SIZE = 10; // up batch size when sdk bug fixed
const batches = [];
let fetchResult;
if (!pageSize) {
// batch all
for (let i = 0; i < Math.ceil(totalItems / BATCH_SIZE); i++) {
batches[i] = Lbry.collection_resolve({
claim_id: claimId,
page: i + 1,
page_size: BATCH_SIZE,
});
}
const resultArray = await Promise.all(batches);
fetchResult = mergeResults(resultArray);
} else {
fetchResult = await Lbry.collection_resolve({
claim_id: claimId,
page: 1,
page_size: pageSize,
});
}
// $FlowFixMe
const itemsById: { claimId: string, items?: ?Array<GenericClaim> } = { claimId: claimId };
if (fetchResult.items) {
itemsById.items = fetchResult.items;
} else {
itemsById.items = null;
}
return itemsById;
} catch (e) {
return {
claimId: claimId,
items: null,
};
}
}
const promises = [];
collectionIds.forEach(collectionId => {
const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch);
if (!claim) {
invalidCollectionIds.push(collectionId);
} else {
const claimCount = claim.value.claims && claim.value.claims.length;
if (pageSize) {
promises.push(resolveCollectionItems(collectionId, claimCount, pageSize));
} else {
promises.push(resolveCollectionItems(collectionId, claimCount));
}
}
});
// $FlowFixMe
const resolvedCollectionItemsById: Array<{
claimId: string,
items: ?Array<GenericClaim>,
}> = await Promise.all(promises);
function processClaims(resultClaimsByUri) {
const processedClaims = {};
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 {
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
processedClaims[uri] = result;
}
});
return processedClaims;
}
const newCollectionItemsById = {};
const flatResolvedCollectionItems = {};
resolvedCollectionItemsById.forEach(entry => {
// $FlowFixMe
const collectionItems: Array<any> = entry.items;
const collectionId = entry.claimId;
if (collectionItems) {
const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch);
const editedCollection = makeSelectEditedCollectionForId(collectionId)(stateAfterClaimSearch);
const { name, timestamp } = claim || {};
const valueTypes = new Set();
const streamTypes = new Set();
let items = [];
collectionItems.forEach(collectionItem => {
// here's where we would just items.push(collectionItem.permanent_url
items.push(collectionItem.permanent_url);
valueTypes.add(collectionItem.value_type);
if (collectionItem.value.stream_type) {
streamTypes.add(collectionItem.value.stream_type);
}
flatResolvedCollectionItems[collectionItem.canonical_url] = collectionItem;
});
const 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'))));
newCollectionItemsById[collectionId] = {
items,
id: collectionId,
name: name,
itemCount: claim.value.claims.length,
type: isPlaylist ? 'playlist' : 'collection',
updatedAt: timestamp,
};
// clear any stale edits
if (editedCollection && timestamp > editedCollection['updatedAt']) {
dispatch({
type: ACTIONS.COLLECTION_DELETE,
data: {
id: collectionId,
collectionKey: 'edited',
},
});
}
} else {
// no collection items? probably in pending.
}
});
const processedClaimsByUri = processClaims(flatResolvedCollectionItems);
dispatch({
type: ACTIONS.RESOLVE_URIS_COMPLETED,
data: { resolveInfo: processedClaimsByUri },
});
dispatch({
type: ACTIONS.COLLECTION_ITEMS_RESOLVE_COMPLETED,
data: {
resolvedCollections: newCollectionItemsById,
failedCollectionIds: invalidCollectionIds,
},
});
};
export const doFetchItemsInCollection = (
options: { collectionId: string, pageSize?: number },
cb?: () => void
) => {
const { collectionId, pageSize } = options;
const newOptions: { collectionIds: Array<string>, 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 = makeSelectMyPublishedCollectionForId(collectionId)(state);
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';
let newItems: Array<?string> = 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);
}
if (editedCollection) {
if (publishedCollection.items.join(',') === newItems.join(',')) {
// delete edited if newItems are the same as publishedItems
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 (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;
};

View file

@ -13,6 +13,9 @@ type SharedData = {
settings?: any, settings?: any,
app_welcome_version?: number, app_welcome_version?: number,
sharing_3P?: boolean, sharing_3P?: boolean,
unpublishedCollectionTest: CollectionGroup,
builtinCollectionTest: CollectionGroup,
savedCollectionTest: Array<string>,
}, },
}; };
@ -27,6 +30,9 @@ function extractUserState(rawObj: SharedData) {
settings, settings,
app_welcome_version, app_welcome_version,
sharing_3P, sharing_3P,
unpublishedCollectionTest,
builtinCollectionTest,
savedCollectionTest,
} = rawObj.value; } = rawObj.value;
return { return {
@ -38,6 +44,9 @@ function extractUserState(rawObj: SharedData) {
...(settings ? { settings } : {}), ...(settings ? { settings } : {}),
...(app_welcome_version ? { app_welcome_version } : {}), ...(app_welcome_version ? { app_welcome_version } : {}),
...(sharing_3P ? { sharing_3P } : {}), ...(sharing_3P ? { sharing_3P } : {}),
...(unpublishedCollectionTest ? { unpublishedCollectionTest } : {}),
...(builtinCollectionTest ? { builtinCollectionTest } : {}),
...(savedCollectionTest ? { savedCollectionTest } : {}),
}; };
} }
@ -55,6 +64,9 @@ export function doPopulateSharedUserState(sharedSettings: any) {
settings, settings,
app_welcome_version, app_welcome_version,
sharing_3P, sharing_3P,
unpublishedCollectionTest,
builtinCollectionTest,
savedCollectionTest,
} = extractUserState(sharedSettings); } = extractUserState(sharedSettings);
dispatch({ dispatch({
type: ACTIONS.USER_STATE_POPULATE, type: ACTIONS.USER_STATE_POPULATE,
@ -67,6 +79,9 @@ export function doPopulateSharedUserState(sharedSettings: any) {
settings, settings,
welcomeVersion: app_welcome_version, welcomeVersion: app_welcome_version,
allowAnalytics: sharing_3P, allowAnalytics: sharing_3P,
unpublishedCollectionTest,
builtinCollectionTest,
savedCollectionTest,
}, },
}); });
}; };

View file

@ -13,6 +13,7 @@ import mergeClaim from 'util/merge-claim';
type State = { type State = {
createChannelError: ?string, createChannelError: ?string,
createCollectionError: ?string,
channelClaimCounts: { [string]: number }, channelClaimCounts: { [string]: number },
claimsByUri: { [string]: string }, claimsByUri: { [string]: string },
byId: { [string]: Claim }, byId: { [string]: Claim },
@ -21,9 +22,11 @@ type State = {
reflectingById: { [string]: ReflectingUpdate }, reflectingById: { [string]: ReflectingUpdate },
myClaims: ?Array<string>, myClaims: ?Array<string>,
myChannelClaims: ?Array<string>, myChannelClaims: ?Array<string>,
myCollectionClaims: ?Array<string>,
abandoningById: { [string]: boolean }, abandoningById: { [string]: boolean },
fetchingChannelClaims: { [string]: number }, fetchingChannelClaims: { [string]: number },
fetchingMyChannels: boolean, fetchingMyChannels: boolean,
fetchingMyCollections: boolean,
fetchingClaimSearchByQuery: { [string]: boolean }, fetchingClaimSearchByQuery: { [string]: boolean },
purchaseUriSuccess: boolean, purchaseUriSuccess: boolean,
myPurchases: ?Array<string>, myPurchases: ?Array<string>,
@ -34,6 +37,7 @@ type State = {
claimSearchByQuery: { [string]: Array<string> }, claimSearchByQuery: { [string]: Array<string> },
claimSearchByQueryLastPageReached: { [string]: Array<boolean> }, claimSearchByQueryLastPageReached: { [string]: Array<boolean> },
creatingChannel: boolean, creatingChannel: boolean,
creatingCollection: boolean,
paginatedClaimsByChannel: { paginatedClaimsByChannel: {
[string]: { [string]: {
all: Array<string>, all: Array<string>,
@ -43,7 +47,9 @@ type State = {
}, },
}, },
updateChannelError: ?string, updateChannelError: ?string,
updateCollectionError: ?string,
updatingChannel: boolean, updatingChannel: boolean,
updatingCollection: boolean,
pendingChannelImport: string | boolean, pendingChannelImport: string | boolean,
repostLoading: boolean, repostLoading: boolean,
repostError: ?string, repostError: ?string,
@ -66,6 +72,7 @@ const defaultState = {
fetchingChannelClaims: {}, fetchingChannelClaims: {},
resolvingUris: [], resolvingUris: [],
myChannelClaims: undefined, myChannelClaims: undefined,
myCollectionClaims: [],
myClaims: undefined, myClaims: undefined,
myPurchases: undefined, myPurchases: undefined,
myPurchasesPageNumber: undefined, myPurchasesPageNumber: undefined,
@ -74,6 +81,7 @@ const defaultState = {
fetchingMyPurchases: false, fetchingMyPurchases: false,
fetchingMyPurchasesError: undefined, fetchingMyPurchasesError: undefined,
fetchingMyChannels: false, fetchingMyChannels: false,
fetchingMyCollections: false,
abandoningById: {}, abandoningById: {},
pendingIds: [], pendingIds: [],
reflectingById: {}, reflectingById: {},
@ -82,9 +90,13 @@ const defaultState = {
claimSearchByQueryLastPageReached: {}, claimSearchByQueryLastPageReached: {},
fetchingClaimSearchByQuery: {}, fetchingClaimSearchByQuery: {},
updateChannelError: '', updateChannelError: '',
updateCollectionError: '',
updatingChannel: false, updatingChannel: false,
creatingChannel: false, creatingChannel: false,
createChannelError: undefined, createChannelError: undefined,
updatingCollection: false,
creatingCollection: false,
createCollectionError: undefined,
pendingChannelImport: false, pendingChannelImport: false,
repostLoading: false, repostLoading: false,
repostError: undefined, repostError: undefined,
@ -100,15 +112,7 @@ const defaultState = {
}; };
function handleClaimAction(state: State, action: any): State { function handleClaimAction(state: State, action: any): State {
const { const { resolveInfo }: ClaimActionResolveInfo = action.data;
resolveInfo,
}: {
[string]: {
stream: ?StreamClaim,
channel: ?ChannelClaim,
claimsInChannel: ?number,
},
} = action.data;
const byUri = Object.assign({}, state.claimsByUri); const byUri = Object.assign({}, state.claimsByUri);
const byId = Object.assign({}, state.byId); 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]) => { Object.entries(resolveInfo).forEach(([url: string, resolveResponse: ResolveResponse]) => {
// $FlowFixMe // $FlowFixMe
const { claimsInChannel, stream, channel: channelFromResolve } = resolveResponse; const { claimsInChannel, stream, channel: channelFromResolve, collection } = resolveResponse;
const channel = channelFromResolve || (stream && stream.signing_channel); const channel = channelFromResolve || (stream && stream.signing_channel);
if (stream) { if (stream) {
@ -165,8 +169,29 @@ function handleClaimAction(state: State, action: any): State {
newResolvingUrls.delete(channel.permanent_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;
// If url isn't a canonical_url, make sure that is added too
byUri[collection.canonical_url] = collection.claim_id;
// Also add the permanent_url here until lighthouse returns canonical_url for search results
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); newResolvingUrls.delete(url);
if (!stream && !channel && !pendingIds.includes(byUri[url])) { if (!stream && !channel && !collection && !pendingIds.includes(byUri[url])) {
byUri[url] = null; byUri[url] = null;
} }
}); });
@ -306,6 +331,57 @@ 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<CollectionClaim> } = action.data;
const myClaims = state.myClaims || [];
let myClaimIds = new Set(myClaims);
const pendingIds = state.pendingIds || [];
let myCollectionClaims;
const byId = Object.assign({}, state.byId);
const byUri = Object.assign({}, state.claimsByUri);
if (!claims.length) {
// $FlowFixMe
myCollectionClaims = null;
} else {
myCollectionClaims = new Set(state.myCollectionClaims);
claims.forEach(claim => {
const { meta } = claim;
const { canonical_url: canonicalUrl, permanent_url: permanentUrl, claim_id: claimId } = claim;
// maybe add info about items in collection
byUri[canonicalUrl] = claimId;
byUri[permanentUrl] = claimId;
// $FlowFixMe
myCollectionClaims.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: myCollectionClaims ? Array.from(myCollectionClaims) : null,
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 => { reducers[ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED] = (state: State, action: any): State => {
const { uri, page } = action.data; const { uri, page } = action.data;
const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims); const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims);
@ -520,6 +596,54 @@ 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 => {
return Object.assign({}, state, {
creatingCollection: false,
});
};
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,
});
};
// COLLECTION_PUBLISH_ABANDON_...
reducers[ACTIONS.IMPORT_CHANNEL_STARTED] = (state: State): State => reducers[ACTIONS.IMPORT_CHANNEL_STARTED] = (state: State): State =>
Object.assign({}, state, { pendingChannelImports: true }); Object.assign({}, state, { pendingChannelImports: true });

View file

@ -0,0 +1,235 @@
// @flow
import { handleActions } from 'util/redux-utils';
import * as ACTIONS from 'constants/action_types';
import * as COLLECTION_CONSTS from 'constants/collections';
const WATCH_LATER_ID = 'watchlater';
const FAVORITES_ID = 'favorites';
const BUILTIN_LISTS = [WATCH_LATER_ID, FAVORITES_ID];
const getTimestamp = () => {
return Math.floor(Date.now() / 1000);
};
const defaultState: CollectionState = {
builtin: {
watchlater: {
items: ['lbry://seriouspublish#c1b740eb88f96b465f65e5f1542564539df1c62e'],
id: WATCH_LATER_ID,
name: 'Watch Later',
updatedAt: getTimestamp(),
type: 'playlist',
},
favorites: {
items: ['lbry://seriouspublish#c1b740eb88f96b465f65e5f1542564539df1c62e'],
id: FAVORITES_ID,
name: 'Favorites',
type: 'collection',
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<string>}
// entry
const newListTemplate = {
id: params.id,
name: params.name,
items: [],
updatedAt: getTimestamp(),
type: params.type || 'mixed',
};
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]) {
delete state[collectionKey][id];
} 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);
const isEdit = editList[localId];
if (localId) {
// pending from unpublished -> published
// delete from local
newPendingList[claimId] = Object.assign(
{},
newEditList[localId] || newUnpublishedList[localId] || {}
);
if (isEdit) {
delete newEditList[localId];
} else {
delete newUnpublishedList[localId];
}
} else {
// pending from edited published -> published
if (isEdit) {
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 (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 { builtinCollectionTest, savedCollectionTest, unpublishedCollectionTest } = action.data;
return {
...state,
unpublished: unpublishedCollectionTest || state.unpublished,
builtin: builtinCollectionTest || state.builtin,
saved: savedCollectionTest || state.saved,
};
},
[ACTIONS.COLLECTION_ITEMS_RESOLVE_COMPLETED]: (state, action) => {
const { resolvedCollections, failedCollectionIds } = action.data;
const {
pending: pendingList,
edited: editList,
isResolvingCollectionById,
resolved: lists,
} = state;
const resolvedIds = Object.keys(resolvedCollections);
const newResolving = Object.assign({}, isResolvingCollectionById);
if (resolvedCollections && resolvedCollections.length) {
resolvedIds.forEach(resolvedId => {
if (editList[resolvedId]) {
if (editList[resolvedId]['updatedAt'] < resolvedCollections[resolvedId]['updatedAt']) {
delete editList[resolvedId];
}
}
delete newResolving[resolvedId];
if (pendingList[resolvedId]) {
delete pendingList[resolvedId];
}
});
}
if (failedCollectionIds && failedCollectionIds.length) {
failedCollectionIds.forEach(failedId => {
delete newResolving[failedId];
});
}
const newLists = Object.assign({}, lists, resolvedCollections);
return Object.assign({}, state, {
...state,
pending: pendingList,
resolved: newLists,
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 };

View file

@ -95,6 +95,12 @@ export const makeSelectClaimIsPending = (uri: string) =>
} }
); );
export const makeSelectClaimIdForUri = (uri: string) =>
createSelector(
selectClaimIdsByUri,
claimIds => claimIds[uri]
);
export const selectReflectingById = createSelector( export const selectReflectingById = createSelector(
selectState, selectState,
state => state.reflectingById state => state.reflectingById
@ -531,6 +537,11 @@ export const selectFetchingMyChannels = createSelector(
state => state.fetchingMyChannels state => state.fetchingMyChannels
); );
export const selectFetchingMyCollections = createSelector(
selectState,
state => state.fetchingMyCollections
);
export const selectMyChannelClaims = createSelector( export const selectMyChannelClaims = createSelector(
selectState, selectState,
selectClaimsById, selectClaimsById,
@ -557,6 +568,11 @@ export const selectMyChannelUrls = createSelector(
claims => (claims ? claims.map(claim => claim.canonical_url || claim.permanent_url) : undefined) claims => (claims ? claims.map(claim => claim.canonical_url || claim.permanent_url) : undefined)
); );
export const selectMyCollectionIds = createSelector(
selectState,
state => state.myCollectionClaims
);
export const selectResolvingUris = createSelector( export const selectResolvingUris = createSelector(
selectState, selectState,
state => state.resolvingUris || [] state => state.resolvingUris || []
@ -917,3 +933,23 @@ export const makeSelectStakedLevelForChannelUri = (uri: string) =>
return level; 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
);

View file

@ -0,0 +1,203 @@
// @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]);
}
);
// for library page, we want all published
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(edited)
.filter(
([key, val]) => myIds.includes(key)
// $FlowFixMe
)
.concat(
Object.entries(resolved).filter(
([key, val]) =>
myIds.includes(key) &&
// $FlowFixMe
(!pending[key] && !edited[key])
)
)
)
);
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 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 makeSelectNextUrlForCollection = (id: string, index: number) =>
createSelector(
makeSelectUrlsForCollectionId(id),
urls => {
const url = urls[index + 1];
if (url) {
return url;
}
return null;
}
);
export const makeSelectNameForCollectionId = (id: string) =>
createSelector(
makeSelectCollectionForId(id),
collection => {
return (collection && collection.name) || '';
}
);