From 65902f6d5836b8e08831d64c685a7ff756f79c37 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 15 Sep 2021 15:41:57 +0800 Subject: [PATCH 01/67] Commentron err replacement: fix readability + added link support. - Hopefully this is easier to understand, and easier to add/remove entries. - The link support will be needed by the upcoming Appeal process. --- ui/redux/actions/comments.js | 121 ++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 44 deletions(-) diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 5eb9c02d6..0e5425ea0 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -22,19 +22,45 @@ import { doAlertWaitingForSync } from 'redux/actions/app'; const isDev = process.env.NODE_ENV !== 'production'; const FETCH_API_FAILED_TO_FETCH = 'Failed to fetch'; -const COMMENTRON_MSG_REMAP = { - // <-- Commentron msg --> : <-- App msg --> - 'channel is blocked by publisher': 'Unable to comment. This channel has blocked you.', - 'channel is not allowed to post comments': 'Unable to comment. Your channel has been blocked by an admin.', - 'comments are disabled by the creator': 'Unable to comment. The content owner has disabled comments.', - 'duplicate comment!': 'Please do not spam.', +declare type CommentronErrorMap = { + [string]: { + commentron: string | RegExp, + replacement: string, + linkText?: string, + linkTarget?: string, + }, }; -const COMMENTRON_REGEX_MAP = { - // <-- App msg --> : <-- Regex of Commentron msg --> - 'Your user name "%1%" is too close to the creator\'s user name "%2%" and may cause confusion. Please use another identity.': /^your user name (.*) is too close to the creator's user name (.*) and may cause confusion. Please use another identity.$/, - 'Slow mode is on. Please wait up to %1% seconds before commenting again.': /^Slow mode is on. Please wait at most (.*) seconds before commenting again.$/, - 'The comment contains contents that are blocked by %1%.': /^the comment contents are blocked by (.*)$/, +// prettier-ignore +const ERR_MAP: CommentronErrorMap = { + SIMILAR_NAME: { + commentron: /^your user name (.*) is too close to the creator's user name (.*) and may cause confusion. Please use another identity.$/, + replacement: 'Your user name "%1%" is too close to the creator\'s user name "%2%" and may cause confusion. Please use another identity.', + }, + SLOW_MODE_IS_ON: { + commentron: /^Slow mode is on. Please wait at most (.*) seconds before commenting again.$/, + replacement: 'Slow mode is on. Please wait up to %1% seconds before commenting again.', + }, + HAS_MUTED_WORDS: { + commentron: /^the comment contents are blocked by (.*)$/, + replacement: 'The comment contains contents that are blocked by %1%.', + }, + BLOCKED_BY_CREATOR: { + commentron: 'channel is blocked by publisher', + replacement: 'Unable to comment. This channel has blocked you.', + }, + BLOCKED_BY_ADMIN: { + commentron: 'channel is not allowed to post comments', + replacement: 'Unable to comment. Your channel has been blocked by an admin.', + }, + CREATOR_DISABLED: { + commentron: 'comments are disabled by the creator', + replacement: 'Unable to comment. The content owner has disabled comments.', + }, + STOP_SPAMMING: { + commentron: 'duplicate comment!', + replacement: 'Please do not spam.', + }, }; function devToast(dispatch, msg) { @@ -44,6 +70,45 @@ function devToast(dispatch, msg) { } } +function resolveCommentronError(commentronMsg: string) { + for (const key in ERR_MAP) { + // noinspection JSUnfilteredForInLoop + const data = ERR_MAP[key]; + if (typeof data.commentron === 'string') { + if (data.commentron === commentronMsg) { + return { + message: __(data.replacement), + linkText: data.linkText ? __(data.linkText) : undefined, + linkTarget: data.linkTarget, + isError: true, + }; + } + } else { + const match = commentronMsg.match(data.commentron); + if (match) { + const subs = {}; + for (let i = 1; i < match.length; ++i) { + subs[`${i}`] = match[i]; + } + + return { + message: __(data.replacement, subs), + linkText: data.linkText ? __(data.linkText) : undefined, + linkTarget: data.linkTarget, + isError: true, + }; + } + } + } + + return { + // Fallback to commentron original message. It will be in English + // only and most likely not capitalized correctly. + message: commentronMsg, + isError: true, + }; +} + export function doCommentList( uri: string, parentId: string, @@ -471,39 +536,7 @@ export function doCommentCreate( }) .catch((error) => { dispatch({ type: ACTIONS.COMMENT_CREATE_FAILED, data: error }); - - let toastMessage; - - for (const commentronMsg in COMMENTRON_MSG_REMAP) { - if (error.message === commentronMsg) { - toastMessage = __(COMMENTRON_MSG_REMAP[commentronMsg]); - break; - } - } - - if (!toastMessage) { - for (const i18nStr in COMMENTRON_REGEX_MAP) { - const regex = COMMENTRON_REGEX_MAP[i18nStr]; - const match = error.message.match(regex); - if (match) { - const subs = {}; - for (let i = 1; i < match.length; ++i) { - subs[`${i}`] = match[i]; - } - - toastMessage = __(i18nStr, subs); - break; - } - } - } - - if (!toastMessage) { - // Fallback to commentron original message. It will be in English - // only and most likely not capitalized correctly. - toastMessage = error.message; - } - - dispatch(doToast({ message: toastMessage, isError: true })); + dispatch(doToast(resolveCommentronError(error.message))); return Promise.reject(error); }); }; -- 2.45.2 From 21ba7840cae6ff073b4054ff0747186696106fd7 Mon Sep 17 00:00:00 2001 From: jessopb <36554050+jessopb@users.noreply.github.com> Date: Wed, 15 Sep 2021 10:11:01 -0400 Subject: [PATCH 02/67] refactor collection thumbs and fix list channel updates (#7095) * refactor collection thumbs * collection update handle channels * collection update handle channels more better * bugfix --- ui/component/collectionEdit/index.js | 3 + ui/component/collectionEdit/view.jsx | 92 ++++++++++++++++------- ui/component/collectionsListMine/index.js | 2 + ui/component/collectionsListMine/view.jsx | 14 +++- ui/component/selectThumbnail/view.jsx | 3 +- 5 files changed, 82 insertions(+), 32 deletions(-) diff --git a/ui/component/collectionEdit/index.js b/ui/component/collectionEdit/index.js index d95aa4a4e..3dd9383ea 100644 --- a/ui/component/collectionEdit/index.js +++ b/ui/component/collectionEdit/index.js @@ -20,6 +20,7 @@ import { import CollectionForm from './view'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; +import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), @@ -46,6 +47,8 @@ const perform = (dispatch) => ({ publishCollectionUpdate: (params) => dispatch(doCollectionPublishUpdate(params)), publishCollection: (params, collectionId) => dispatch(doCollectionPublish(params, collectionId)), clearCollectionErrors: () => dispatch({ type: LBRY_REDUX_ACTIONS.CLEAR_COLLECTION_ERRORS }), + setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)), + setIncognito: (incognito) => dispatch(doSetIncognito(incognito)), }); export default connect(select, perform)(CollectionForm); diff --git a/ui/component/collectionEdit/view.jsx b/ui/component/collectionEdit/view.jsx index 371d7df7f..99ccc6ccf 100644 --- a/ui/component/collectionEdit/view.jsx +++ b/ui/component/collectionEdit/view.jsx @@ -52,6 +52,8 @@ type Props = { publishCollection: (CollectionPublishParams, string) => Promise, clearCollectionErrors: () => void, onDone: (string) => void, + setActiveChannel: (string) => void, + setIncognito: (boolean) => void, }; function CollectionForm(props: Props) { @@ -82,6 +84,8 @@ function CollectionForm(props: Props) { publishCollectionUpdate, publishCollection, clearCollectionErrors, + setActiveChannel, + setIncognito, onDone, } = props; const activeChannelName = activeChannelClaim && activeChannelClaim.name; @@ -91,22 +95,26 @@ function CollectionForm(props: Props) { } const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id; const collectionName = (claim && claim.name) || (collection && collection.name); - + const collectionChannel = claim && claim.signing_channel ? claim.signing_channel.claim_id : undefined; + const hasClaim = !!claim; + const [initialized, setInitialized] = React.useState(false); const [nameError, setNameError] = React.useState(undefined); const [bidError, setBidError] = React.useState(''); - const [params, setParams]: [any, (any) => void] = React.useState(getCollectionParams()); + const [thumbStatus, setThumbStatus] = React.useState(''); + const [thumbError, setThumbError] = React.useState(''); + const [params, setParams]: [any, (any) => void] = React.useState({}); const name = params.name; const isNewCollection = !uri; const { replace } = useHistory(); - const languageParam = params.languages; + const languageParam = params.languages || []; const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0]; const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1]; - + const hasClaims = params.claims && params.claims.length; const collectionClaimIdsString = JSON.stringify(collectionClaimIds); - const itemError = !params.claims.length ? __('Cannot publish empty list') : ''; + const itemError = !hasClaims ? __('Cannot publish empty list') : ''; const thumbnailError = - (params.thumbnail_error && params.thumbnail_status !== THUMBNAIL_STATUSES.COMPLETE && __('Invalid thumbnail')) || - (params.thumbnail_status === THUMBNAIL_STATUSES.IN_PROGRESS && __('Please wait for thumbnail to finish uploading')); + (thumbError && thumbStatus !== THUMBNAIL_STATUSES.COMPLETE && __('Invalid thumbnail')) || + (thumbStatus === THUMBNAIL_STATUSES.IN_PROGRESS && __('Please wait for thumbnail to finish uploading')); const submitError = nameError || bidError || itemError || updateError || createError || thumbnailError; function parseName(newName) { @@ -118,6 +126,10 @@ function CollectionForm(props: Props) { setParams({ ...params, ...paramObj }); } + function updateParams(paramsObj) { + setParams({ ...params, ...paramsObj }); + } + // TODO remove this or better decide whether app should delete languages[2+] // This was added because a previous update setting was duplicating language codes function dedupeLanguages(languages) { @@ -140,11 +152,19 @@ function CollectionForm(props: Props) { } } + function handleUpdateThumbnail(update: { [string]: string }) { + if (update.thumbnail_url) { + setParam(update); + } else if (update.thumbnail_status) { + setThumbStatus(update.thumbnail_status); + } else { + setThumbError(update.thumbnail_error); + } + } + function getCollectionParams() { const collectionParams: { thumbnail_url?: string, - thumbnail_error?: boolean, - thumbnail_status?: string, name?: string, description?: string, title?: string, @@ -168,8 +188,8 @@ function CollectionForm(props: Props) { return { name: tag }; }) : [], - claim_id: String(claim && claim.claim_id), - channel_id: String(activeChannelId && parseName(collectionName)), + claim_id: claim ? claim.claim_id : undefined, + channel_id: claim ? collectionChannel : activeChannelId || undefined, claims: collectionClaimIds, }; @@ -235,16 +255,38 @@ function CollectionForm(props: Props) { setNameError(nameError); }, [name]); + // on mount, if we get a collectionChannel, set it. React.useEffect(() => { - if (incognito && params.channel_id) { - const newParams = Object.assign({}, params); - delete newParams.channel_id; - setParams(newParams); - } else if (activeChannelId && params.channel_id !== activeChannelId) { - setParams({ ...params, channel_id: activeChannelId }); + if (hasClaim && !initialized) { + if (collectionChannel) { + setActiveChannel(collectionChannel); + setIncognito(false); + } else if (!collectionChannel && hasClaim) { + setIncognito(true); + } + setInitialized(true); } - }, [activeChannelId, incognito, params, setParams]); + }, [setInitialized, setActiveChannel, collectionChannel, setIncognito, hasClaim, incognito, initialized]); + // every time activechannel or incognito changes, set it. + React.useEffect(() => { + if (initialized) { + if (activeChannelId) { + setParam({ channel_id: activeChannelId }); + } else if (incognito) { + setParam({ channel_id: undefined }); + } + } + }, [activeChannelId, incognito, initialized]); + + // setup initial params after we're sure if it's published or not + React.useEffect(() => { + if (!uri || (uri && hasClaim)) { + updateParams(getCollectionParams()); + } + }, [uri, hasClaim]); + + console.log('params', params); return ( <>
@@ -295,10 +337,9 @@ function CollectionForm(props: Props) { ({ publishedCollections: selectMyPublishedPlaylistCollections(state), unpublishedCollections: selectMyUnpublishedCollections(state), // savedCollections: selectSavedCollections(state), + fetchingCollections: selectFetchingMyCollections(state), }); export default connect(select)(CollectionsListMine); diff --git a/ui/component/collectionsListMine/view.jsx b/ui/component/collectionsListMine/view.jsx index d87c92f14..c57273b38 100644 --- a/ui/component/collectionsListMine/view.jsx +++ b/ui/component/collectionsListMine/view.jsx @@ -18,6 +18,7 @@ type Props = { publishedCollections: CollectionGroup, unpublishedCollections: CollectionGroup, // savedCollections: CollectionGroup, + fetchingCollections: boolean, }; const ALL = 'All'; @@ -31,6 +32,7 @@ export default function CollectionsListMine(props: Props) { publishedCollections, unpublishedCollections, // savedCollections, these are resolved on startup from sync'd claimIds or urls + fetchingCollections, } = props; const builtinCollectionsList = (Object.values(builtinCollections || {}): any); @@ -129,7 +131,10 @@ export default function CollectionsListMine(props: Props) {

{__('Playlists')} - {!hasCollections && ( + {!hasCollections && !fetchingCollections && ( +
{__('(Empty) --[indicates empty playlist]--')}
+ )} + {!hasCollections && fetchingCollections && (
{__('(Empty) --[indicates empty playlist]--')}
)}

@@ -174,11 +179,16 @@ export default function CollectionsListMine(props: Props) {
)} - {!hasCollections && ( + {!hasCollections && !fetchingCollections && (
)} + {!hasCollections && fetchingCollections && ( +
+

{__('Loading...')}

+
+ )} ); diff --git a/ui/component/selectThumbnail/view.jsx b/ui/component/selectThumbnail/view.jsx index 95b5f03f1..3dbd70fea 100644 --- a/ui/component/selectThumbnail/view.jsx +++ b/ui/component/selectThumbnail/view.jsx @@ -16,7 +16,6 @@ type Props = { thumbnail: ?string, formDisabled: boolean, uploadThumbnailStatus: string, - publishForm: boolean, thumbnailPath: ?string, thumbnailError: ?string, thumbnailParam: ?string, @@ -34,7 +33,6 @@ function SelectThumbnail(props: Props) { fileInfos, myClaimForUri, formDisabled, - publishForm = true, uploadThumbnailStatus: status, openModal, updatePublishForm, @@ -45,6 +43,7 @@ function SelectThumbnail(props: Props) { resetThumbnailStatus, } = props; + const publishForm = !updateThumbnailParams; const thumbnail = publishForm ? props.thumbnail : thumbnailParam; const thumbnailError = publishForm ? props.thumbnailError : props.thumbnailParamError; -- 2.45.2 From 91fbd615e212f4ba3583e9724d20d63ab23e10a9 Mon Sep 17 00:00:00 2001 From: jessopb <36554050+jessopb@users.noreply.github.com> Date: Wed, 15 Sep 2021 11:28:45 -0400 Subject: [PATCH 03/67] fix embed end logo (#7106) --- ui/scss/component/_file-render.scss | 4 ++++ web/component/fileViewerEmbeddedEnded/view.jsx | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/scss/component/_file-render.scss b/ui/scss/component/_file-render.scss index 60d64bc99..ab6020018 100644 --- a/ui/scss/component/_file-render.scss +++ b/ui/scss/component/_file-render.scss @@ -195,6 +195,10 @@ } } +.file-viewer_embed-ended-title { + white-space: pre-wrap; +} + .content__viewer--floating { .file-viewer__overlay-title, .file-viewer__overlay-secondary { diff --git a/web/component/fileViewerEmbeddedEnded/view.jsx b/web/component/fileViewerEmbeddedEnded/view.jsx index 2074289de..c46b0f5e4 100644 --- a/web/component/fileViewerEmbeddedEnded/view.jsx +++ b/web/component/fileViewerEmbeddedEnded/view.jsx @@ -38,10 +38,10 @@ function FileViewerEmbeddedEnded(props: Props) {
-
{prompt}
+
{prompt}
{isInApp && } diff --git a/ui/component/searchTopClaim/view.jsx b/ui/component/searchTopClaim/view.jsx index 6707a893f..fefc7cbd5 100644 --- a/ui/component/searchTopClaim/view.jsx +++ b/ui/component/searchTopClaim/view.jsx @@ -17,12 +17,13 @@ type Props = { winningUri: ?string, doResolveUris: (Array) => void, hideLink?: boolean, - setChannelActive: boolean => void, - beginPublish: string => void, + setChannelActive: (boolean) => void, + beginPublish: (string) => void, pendingIds: Array, isResolvingWinningUri: boolean, winningClaim: ?Claim, isSearching: boolean, + preferEmbed: boolean, }; export default function SearchTopClaim(props: Props) { @@ -36,6 +37,7 @@ export default function SearchTopClaim(props: Props) { beginPublish, isResolvingWinningUri, isSearching, + preferEmbed, } = props; const uriFromQuery = `lbry://${query}`; const { push } = useHistory(); @@ -88,17 +90,19 @@ export default function SearchTopClaim(props: Props) { )} {winningUri && winningClaim && (
- ( - - - - - )} - /> + {preferEmbed && ( + ( + + + + + )} + /> + )}
)} {!winningUri && (isSearching || isResolvingWinningUri) && ( diff --git a/ui/component/wunderbarTopSuggestion/index.js b/ui/component/wunderbarTopSuggestion/index.js index 32c46b4ef..7c8ec3348 100644 --- a/ui/component/wunderbarTopSuggestion/index.js +++ b/ui/component/wunderbarTopSuggestion/index.js @@ -1,7 +1,14 @@ import { connect } from 'react-redux'; -import { doResolveUris, makeSelectClaimForUri, makeSelectIsUriResolving, parseURI } from 'lbry-redux'; +import { + doResolveUris, + makeSelectClaimForUri, + makeSelectIsUriResolving, + makeSelectTagInClaimOrChannelForUri, + parseURI, +} from 'lbry-redux'; import { makeSelectWinningUriForQuery } from 'redux/selectors/search'; import WunderbarTopSuggestion from './view'; +import { PREFERENCE_EMBED } from 'constants/tags'; const select = (state, props) => { const uriFromQuery = `lbry://${props.query}`; @@ -16,11 +23,12 @@ const select = (state, props) => { } } catch (e) {} - const resolvingUris = uris.some(uri => makeSelectIsUriResolving(uri)(state)); + const resolvingUris = uris.some((uri) => makeSelectIsUriResolving(uri)(state)); const winningUri = makeSelectWinningUriForQuery(props.query)(state); const winningClaim = winningUri ? makeSelectClaimForUri(winningUri)(state) : undefined; + const preferEmbed = makeSelectTagInClaimOrChannelForUri(winningUri, PREFERENCE_EMBED)(state); - return { resolvingUris, winningUri, winningClaim, uris }; + return { resolvingUris, winningUri, winningClaim, uris, preferEmbed }; }; export default connect(select, { diff --git a/ui/component/wunderbarTopSuggestion/view.jsx b/ui/component/wunderbarTopSuggestion/view.jsx index 086fa3db8..b7b8815d4 100644 --- a/ui/component/wunderbarTopSuggestion/view.jsx +++ b/ui/component/wunderbarTopSuggestion/view.jsx @@ -8,10 +8,11 @@ type Props = { doResolveUris: (Array) => void, uris: Array, resolvingUris: boolean, + preferEmbed: boolean, }; export default function WunderbarTopSuggestion(props: Props) { - const { uris, resolvingUris, winningUri, doResolveUris } = props; + const { uris, resolvingUris, winningUri, doResolveUris, preferEmbed } = props; const stringifiedUris = JSON.stringify(uris); React.useEffect(() => { @@ -38,7 +39,7 @@ export default function WunderbarTopSuggestion(props: Props) { ); } - if (!winningUri) { + if (!winningUri || preferEmbed) { return null; } diff --git a/ui/constants/tags.js b/ui/constants/tags.js index 90ede2d84..b923f4491 100644 --- a/ui/constants/tags.js +++ b/ui/constants/tags.js @@ -14,8 +14,10 @@ export const DEFAULT_FOLLOWED_TAGS = [ ]; export const DISABLE_COMMENTS_TAG = 'disable-comments'; +export const DISABLE_SUPPORT_TAG = 'disable-support'; +export const PREFERENCE_EMBED = 'preference-embed'; -export const UTILITY_TAGS = [DISABLE_COMMENTS_TAG]; +export const UTILITY_TAGS = [DISABLE_COMMENTS_TAG, DISABLE_SUPPORT_TAG, PREFERENCE_EMBED]; export const MATURE_TAGS = [ 'porn', diff --git a/ui/scss/component/_embed-player.scss b/ui/scss/component/_embed-player.scss index 2198e8841..59ee8119f 100644 --- a/ui/scss/component/_embed-player.scss +++ b/ui/scss/component/_embed-player.scss @@ -62,6 +62,6 @@ } .embed__overlay-logo { - max-height: 3.5rem; - max-width: 12rem; + max-height: 2rem; + max-width: 7rem; } diff --git a/ui/scss/component/_file-render.scss b/ui/scss/component/_file-render.scss index ab6020018..83b8995ca 100644 --- a/ui/scss/component/_file-render.scss +++ b/ui/scss/component/_file-render.scss @@ -301,6 +301,10 @@ .file-viewer__embedded-title { max-width: 75%; z-index: 2; + display: flex; + align-items: center; + padding-left: var(--spacing-s); + color: white; } .file-viewer__embedded-info { diff --git a/web/component/fileViewerEmbeddedEnded/index.js b/web/component/fileViewerEmbeddedEnded/index.js index b80fcc9f4..1903aec38 100644 --- a/web/component/fileViewerEmbeddedEnded/index.js +++ b/web/component/fileViewerEmbeddedEnded/index.js @@ -1,7 +1,10 @@ import { connect } from 'react-redux'; import fileViewerEmbeddedEnded from './view'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; +import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux'; +import { PREFERENCE_EMBED } from 'constants/tags'; -export default connect(state => ({ +export default connect((state, props) => ({ isAuthenticated: selectUserVerifiedEmail(state), + preferEmbed: makeSelectTagInClaimOrChannelForUri(props.uri, PREFERENCE_EMBED)(state), }))(fileViewerEmbeddedEnded); diff --git a/web/component/fileViewerEmbeddedEnded/view.jsx b/web/component/fileViewerEmbeddedEnded/view.jsx index c46b0f5e4..d56a77f27 100644 --- a/web/component/fileViewerEmbeddedEnded/view.jsx +++ b/web/component/fileViewerEmbeddedEnded/view.jsx @@ -9,10 +9,11 @@ import Logo from 'component/logo'; type Props = { uri: string, isAuthenticated: boolean, + preferEmbed: boolean, }; function FileViewerEmbeddedEnded(props: Props) { - const { uri, isAuthenticated } = props; + const { uri, isAuthenticated, preferEmbed } = props; const prompts = isAuthenticated ? { @@ -37,21 +38,28 @@ function FileViewerEmbeddedEnded(props: Props) { return (
-
-
{prompt}
-
-
+ {!preferEmbed && ( + <> +
{prompt}
+
+ { /* add button to replay? */ } + <> +
+ + )}
); } diff --git a/web/scss/themes/odysee/component/_file-render.scss b/web/scss/themes/odysee/component/_file-render.scss index ae8feb4fb..20560bad1 100644 --- a/web/scss/themes/odysee/component/_file-render.scss +++ b/web/scss/themes/odysee/component/_file-render.scss @@ -230,7 +230,8 @@ } .file-viewer__overlay-logo { - height: 3.5rem; + height: 2rem; + max-height: 2rem; //embed logo height? width: 12rem; display: flex; align-items: center; @@ -313,6 +314,7 @@ } .file-viewer__embedded-title { + color: white; max-width: 75%; z-index: 2; } -- 2.45.2 From a199432b5c47be9c1920874104b889bf1e0425a2 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Mon, 20 Sep 2021 09:23:04 +0800 Subject: [PATCH 09/67] Fix view-count showing up in non-Channel pages ## Issue If you navigated to a Channel Page and returned to the homepage (or any page with ClaimPreview), the view-count is shown because we have that data. ## Fix Instead of passing props around through the long "list" component chain (`ChannelContent -> ClaimListDiscover -> ClaimList -> ClaimPreview`) to indicate whether we should display it , just check the pathname at the lowest component level; I believe eventually we would display it everywhere anyways, so this we'll be the easiest to clean up. --- ui/component/fileViewCountInline/view.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/component/fileViewCountInline/view.jsx b/ui/component/fileViewCountInline/view.jsx index 809fd7985..395195c3c 100644 --- a/ui/component/fileViewCountInline/view.jsx +++ b/ui/component/fileViewCountInline/view.jsx @@ -24,7 +24,12 @@ export default function FileViewCountInline(props: Props) { formattedViewCount = Number(viewCount).toLocaleString(); } - if (!viewCount || (claim && claim.repost_url) || isLivestream) { + // Limit the view-count visibility to Channel Pages for now. I believe we'll + // eventually show it everywhere, so this band-aid would be the easiest to + // clean up (only one place edit/remove). + const isChannelPage = window.location.pathname.startsWith('/@'); + + if (!viewCount || (claim && claim.repost_url) || isLivestream || !isChannelPage) { // (1) Currently, makeSelectViewCountForUri doesn't differentiate between // un-fetched view-count vs zero view-count. But since it's probably not // ideal to highlight that a view has 0 count, let's just not show anything. -- 2.45.2 From 0fc9cb9e73ef69a1814144899015719d936820cd Mon Sep 17 00:00:00 2001 From: infinite-persistence <64950861+infinite-persistence@users.noreply.github.com> Date: Mon, 20 Sep 2021 22:19:34 +0800 Subject: [PATCH 10/67] Disable adv filters in "Find channels to follow" (#7130) ## Issue 7118 advanced filtering adjustments for channel related context --- ui/component/userChannelFollowIntro/view.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/component/userChannelFollowIntro/view.jsx b/ui/component/userChannelFollowIntro/view.jsx index 88723f3a1..55f6245b4 100644 --- a/ui/component/userChannelFollowIntro/view.jsx +++ b/ui/component/userChannelFollowIntro/view.jsx @@ -59,6 +59,7 @@ function UserChannelFollowIntro(props: Props) {
Date: Mon, 20 Sep 2021 09:20:28 -0500 Subject: [PATCH 11/67] Fix theater mode layout on small and medium screens (#7108) * Fix theater mode layout on small and medium screens * Make comments expandable on medium screens --- ui/component/commentsList/view.jsx | 19 ++++++++++++++----- ui/scss/component/_main.scss | 9 +++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ui/component/commentsList/view.jsx b/ui/component/commentsList/view.jsx index 230619ab8..181f9bad6 100644 --- a/ui/component/commentsList/view.jsx +++ b/ui/component/commentsList/view.jsx @@ -14,7 +14,7 @@ import usePersistedState from 'effects/use-persisted-state'; import { ENABLE_COMMENT_REACTIONS } from 'config'; import Empty from 'component/common/empty'; import debounce from 'util/debounce'; -import { useIsMobile } from 'effects/use-screensize'; +import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import { getChannelIdFromClaim } from 'util/claim'; const DEBOUNCE_SCROLL_HANDLER_MS = 200; @@ -87,7 +87,8 @@ function CommentList(props: Props) { linkedCommentId ? 0 : MAX_LINKED_COMMENT_SCROLL_ATTEMPTS ); const isMobile = useIsMobile(); - const [expandedComments, setExpandedComments] = React.useState(!isMobile); + const isMediumScreen = useIsMediumScreen(); + const [expandedComments, setExpandedComments] = React.useState(!isMobile && !isMediumScreen); const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const channelId = getChannelIdFromClaim(claim); const channelSettings = channelId ? settingsByChannelId[channelId] : undefined; @@ -243,7 +244,7 @@ function CommentList(props: Props) { } const handleCommentScroll = debounce(() => { - if (!isMobile && shouldFetchNextPage(page, topLevelTotalPages, window, document)) { + if (!isMobile && !isMediumScreen && shouldFetchNextPage(page, topLevelTotalPages, window, document)) { setPage(page + 1); } }, DEBOUNCE_SCROLL_HANDLER_MS); @@ -258,6 +259,7 @@ function CommentList(props: Props) { } }, [ isMobile, + isMediumScreen, page, moreBelow, spinnerRef, @@ -267,6 +269,13 @@ function CommentList(props: Props) { topLevelTotalPages, ]); + // Expand comments + useEffect(() => { + if (!isMobile && !isMediumScreen && !expandedComments) { + setExpandedComments(true); + } + }, [isMobile, isMediumScreen, expandedComments]); + return ( - {isMobile && ( + {(isMobile || isMediumScreen) && (
{(!expandedComments || moreBelow) && (
)}
-- 2.45.2 From 63fd8677571dadb7da1f9363aa3bb032ff80697c Mon Sep 17 00:00:00 2001 From: infinite-persistence <64950861+infinite-persistence@users.noreply.github.com> Date: Tue, 21 Sep 2021 22:40:44 +0800 Subject: [PATCH 16/67] Fix moderator data misalignment. (#7139) ## Issue Closes 7121 Missing mod block option (large number of moderated channels?) ## Notes It was bad to assume `channelSignatures` would be the same length as the promise result -- any failure in signing would cause a misalignment. For my case, an accidentally-merged channel couldn't be signed due to a missing private key. I think it's the same for Drew. --- ui/redux/actions/comments.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 0e5425ea0..488bf8e94 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -21,6 +21,7 @@ import { doAlertWaitingForSync } from 'redux/actions/app'; const isDev = process.env.NODE_ENV !== 'production'; const FETCH_API_FAILED_TO_FETCH = 'Failed to fetch'; +const PROMISE_FULFILLED = 'fulfilled'; declare type CommentronErrorMap = { [string]: { @@ -1377,9 +1378,7 @@ export function doFetchCommentModAmIList(channelClaim: ChannelClaim) { const state = getState(); const myChannels = selectMyChannelClaims(state); - dispatch({ - type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_STARTED, - }); + dispatch({ type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_STARTED }); let channelSignatures = []; @@ -1399,13 +1398,13 @@ export function doFetchCommentModAmIList(channelClaim: ChannelClaim) { }) ) ) - .then((res) => { + .then((results) => { const delegatorsById = {}; - channelSignatures.forEach((chanSig, index) => { - if (chanSig && res[index]) { - const value = res[index].value; - delegatorsById[chanSig.claim_id] = { + results.forEach((result, index) => { + if (result.status === PROMISE_FULFILLED) { + const value = result.value; + delegatorsById[value.channel_id] = { global: value ? value.type === 'Global' : false, delegators: value && value.authorized_channels ? value.authorized_channels : {}, }; @@ -1418,15 +1417,12 @@ export function doFetchCommentModAmIList(channelClaim: ChannelClaim) { }); }) .catch((err) => { - dispatch({ - type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_FAILED, - }); + devToast(dispatch, `AmI: ${err}`); + dispatch({ type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_FAILED }); }); }) .catch(() => { - dispatch({ - type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_FAILED, - }); + dispatch({ type: ACTIONS.COMMENT_MODERATION_AM_I_LIST_FAILED }); }); }; } -- 2.45.2 From aefa889ee4a8e1b0dc4af178a93c9555bd4953b5 Mon Sep 17 00:00:00 2001 From: infinite-persistence <64950861+infinite-persistence@users.noreply.github.com> Date: Tue, 21 Sep 2021 23:48:05 +0800 Subject: [PATCH 17/67] Embed: add replay button + msg resizing (#7141) * Embed: add replay button Also, changed "Rewatch or Discuss" to "Discuss + external arrow" since there is a dedicated re-watch button. * Embed: resize "ended message" based on container height --- static/app-strings.json | 1 + ui/scss/component/_file-render.scss | 10 +++++++++- .../fileViewerEmbeddedEnded/view.jsx | 19 ++++++++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/static/app-strings.json b/static/app-strings.json index e1abc146d..b73b0aa35 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2169,5 +2169,6 @@ "Content Page": "Content Page", "Card Last 4": "Card Last 4", "Search blocked channel name": "Search blocked channel name", + "Discuss": "Discuss", "--end--": "--end--" } diff --git a/ui/scss/component/_file-render.scss b/ui/scss/component/_file-render.scss index 83b8995ca..d43615d91 100644 --- a/ui/scss/component/_file-render.scss +++ b/ui/scss/component/_file-render.scss @@ -196,7 +196,11 @@ } .file-viewer_embed-ended-title { - white-space: pre-wrap; + max-width: 100%; + p { + font-size: 6vh; + white-space: pre-wrap; + } } .content__viewer--floating { @@ -231,6 +235,10 @@ .button + .button { margin-left: var(--spacing-m); } + + .button--link { + vertical-align: middle; + } } .file-viewer__overlay-logo { diff --git a/web/component/fileViewerEmbeddedEnded/view.jsx b/web/component/fileViewerEmbeddedEnded/view.jsx index d56a77f27..d365146b5 100644 --- a/web/component/fileViewerEmbeddedEnded/view.jsx +++ b/web/component/fileViewerEmbeddedEnded/view.jsx @@ -1,6 +1,7 @@ // @flow import React from 'react'; import Button from 'component/button'; +import * as ICONS from 'constants/icons'; import { formatLbryUrlForWeb } from 'util/url'; import { withRouter } from 'react-router'; import { URL, SITE_NAME } from 'config'; @@ -34,6 +35,7 @@ function FileViewerEmbeddedEnded(props: Props) { // $FlowFixMe const prompt = prompts[promptKey]; const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=${promptKey}`; + const showReplay = Boolean(window.player); return (
@@ -44,11 +46,22 @@ function FileViewerEmbeddedEnded(props: Props) {
{!preferEmbed && ( <> -
{prompt}
+
+

{prompt}

+
- { /* add button to replay? */ } <> -
- {!preferEmbed && ( + +
+

{prompt}

+
+
<> -
-

{prompt}

-
-
+ {showReplay && ( +
+ )} - )} +
); } -- 2.45.2 From de6eb99d41ead74dd0de309f90e65f1083375628 Mon Sep 17 00:00:00 2001 From: saltrafael <76502841+saltrafael@users.noreply.github.com> Date: Wed, 22 Sep 2021 12:28:32 -0300 Subject: [PATCH 19/67] Fix channel name issue (#7153) --- ui/component/notification/view.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/component/notification/view.jsx b/ui/component/notification/view.jsx index e51fe4ebe..996978442 100644 --- a/ui/component/notification/view.jsx +++ b/ui/component/notification/view.jsx @@ -106,7 +106,11 @@ export default function Notification(props: Props) { urlParams.append('lc', notification_parameters.dynamic.hash); } - let channelName = channelUrl && '@' + channelUrl.split('@')[1].split('#')[0]; + let channelName; + try { + const { claimName } = parseURI(channelUrl); + channelName = claimName; + } catch (e) {} const notificationTitle = notification_parameters.device.title; const titleSplit = notificationTitle.split(' '); -- 2.45.2 From 5c8878353f10696e4e20dd86ca32a63eee048764 Mon Sep 17 00:00:00 2001 From: GG2015 Date: Thu, 23 Sep 2021 02:17:59 -0400 Subject: [PATCH 20/67] Stream Key Button (#7127) * Added streamkey button. * Added streamkey button. * Updated changes * Removed unused code. * Removed copyableStreamkeyUnmask component. * Rewrote StreamKeyMask to enableMask/enableMaskType * Updated changelog and bumped version to 0.51.3 * Reverted changes * Updated to correct area. * Renamed CopyableText to CopyableStreamKey * See commit notes. * Fixed Show/Hide button --- CHANGELOG.md | 3 +- ui/component/copyableStreamkey/index.js | 7 ++ ui/component/copyableStreamkey/view.jsx | 93 +++++++++++++++++++++++++ ui/page/livestreamSetup/view.jsx | 3 +- 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 ui/component/copyableStreamkey/index.js create mode 100644 ui/component/copyableStreamkey/view.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebd737be..143ac994f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Clicking on the title of a floating player will take you back to the list ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921)) - Fix floating player stopping on markdown or image files ([#7073](https://github.com/lbryio/lbry-desktop/pull/7073)) - Fix list thumbnail upload ([#7074](https://github.com/lbryio/lbry-desktop/pull/7074)) +- Stream Key is now hidden. ([#7127](https://github.com/lbryio/lbry-desktop/pull/7127)) ## [0.51.2] - [2021-08-20] @@ -1955,4 +1956,4 @@ This release includes a breaking change that will reset many of your settings. T - Use local file for publishing - Use local file and html5 for video playback -- Misc changes needed to make UI compatible with electron. +- Misc changes needed to make UI compatible with electron. \ No newline at end of file diff --git a/ui/component/copyableStreamkey/index.js b/ui/component/copyableStreamkey/index.js new file mode 100644 index 000000000..9646cfbfb --- /dev/null +++ b/ui/component/copyableStreamkey/index.js @@ -0,0 +1,7 @@ +import { connect } from 'react-redux'; +import { doToast } from 'redux/actions/notifications'; +import CopyableStreamkey from './view'; + +export default connect(null, { + doToast, +})(CopyableStreamkey); diff --git a/ui/component/copyableStreamkey/view.jsx b/ui/component/copyableStreamkey/view.jsx new file mode 100644 index 000000000..14164bf18 --- /dev/null +++ b/ui/component/copyableStreamkey/view.jsx @@ -0,0 +1,93 @@ +// @flow +import * as ICONS from 'constants/icons'; +import { FormField } from 'component/common/form'; +import Button from 'component/button'; +import React, { useRef, Fragment } from 'react'; + +type Props = { + copyable: string, + snackMessage: ?string, + doToast: ({ message: string }) => void, + primaryButton?: boolean, + name?: string, + onCopy?: (string) => string, + enableMask?: boolean, +}; + +export default function CopyableStreamkey(props: Props) { + const { copyable, doToast, snackMessage, primaryButton = false, name, onCopy, enableMask = true } = props; + + const input = useRef(); + + function copyToClipboard() { + const topRef = input.current; + if (topRef[1].type === 'password') { + navigator.clipboard.writeText(topRef[1].defaultValue); + } + if (topRef[1].type === 'text') { + topRef[1].select(); + if (onCopy) { + onCopy(topRef[1]); + } + } + + document.execCommand('copy'); + } + + function checkMaskType() { + if (enableMask === true) { + return 'password'; + } + if (enableMask === false) { + return 'text'; + } + } + function showStreamkeyFunc() { + const topRef = input.current; + if (topRef[1].type === 'password') { + topRef[1].type = 'text'; + topRef[0].innerText = 'Hide'; + return; + } + if (topRef[1].type === 'text') { + topRef[1].type = 'password'; + topRef[0].innerText = 'Show'; + } + } + + return ( + +
+
+ {' '} +
+ { + copyToClipboard(); + doToast({ + message: snackMessage || __('Text copied'), + }); + }} + /> + } + /> + +
+ ); +} diff --git a/ui/page/livestreamSetup/view.jsx b/ui/page/livestreamSetup/view.jsx index ab3e06f4f..e535751a0 100644 --- a/ui/page/livestreamSetup/view.jsx +++ b/ui/page/livestreamSetup/view.jsx @@ -13,6 +13,7 @@ import { Lbry } from 'lbry-redux'; import { toHex } from 'util/hex'; import { FormField } from 'component/common/form'; import CopyableText from 'component/copyableText'; +import CopyableStreamkey from 'component/copyableStreamkey'; import Card from 'component/common/card'; import ClaimList from 'component/claimList'; import usePersistedState from 'effects/use-persisted-state'; @@ -186,7 +187,7 @@ export default function LivestreamSetupPage(props: Props) { copyable={LIVESTREAM_RTMP_URL} snackMessage={__('Copied')} /> - Date: Thu, 23 Sep 2021 17:57:13 +0800 Subject: [PATCH 21/67] Revert "Stream Key Button (#7127)" I forgot to lint before merging. Reverting for now, will fix in a bit. This reverts commit 5c8878353f10696e4e20dd86ca32a63eee048764. --- CHANGELOG.md | 3 +- ui/component/copyableStreamkey/index.js | 7 -- ui/component/copyableStreamkey/view.jsx | 93 ------------------------- ui/page/livestreamSetup/view.jsx | 3 +- 4 files changed, 2 insertions(+), 104 deletions(-) delete mode 100644 ui/component/copyableStreamkey/index.js delete mode 100644 ui/component/copyableStreamkey/view.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 143ac994f..2ebd737be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Clicking on the title of a floating player will take you back to the list ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921)) - Fix floating player stopping on markdown or image files ([#7073](https://github.com/lbryio/lbry-desktop/pull/7073)) - Fix list thumbnail upload ([#7074](https://github.com/lbryio/lbry-desktop/pull/7074)) -- Stream Key is now hidden. ([#7127](https://github.com/lbryio/lbry-desktop/pull/7127)) ## [0.51.2] - [2021-08-20] @@ -1956,4 +1955,4 @@ This release includes a breaking change that will reset many of your settings. T - Use local file for publishing - Use local file and html5 for video playback -- Misc changes needed to make UI compatible with electron. \ No newline at end of file +- Misc changes needed to make UI compatible with electron. diff --git a/ui/component/copyableStreamkey/index.js b/ui/component/copyableStreamkey/index.js deleted file mode 100644 index 9646cfbfb..000000000 --- a/ui/component/copyableStreamkey/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import { connect } from 'react-redux'; -import { doToast } from 'redux/actions/notifications'; -import CopyableStreamkey from './view'; - -export default connect(null, { - doToast, -})(CopyableStreamkey); diff --git a/ui/component/copyableStreamkey/view.jsx b/ui/component/copyableStreamkey/view.jsx deleted file mode 100644 index 14164bf18..000000000 --- a/ui/component/copyableStreamkey/view.jsx +++ /dev/null @@ -1,93 +0,0 @@ -// @flow -import * as ICONS from 'constants/icons'; -import { FormField } from 'component/common/form'; -import Button from 'component/button'; -import React, { useRef, Fragment } from 'react'; - -type Props = { - copyable: string, - snackMessage: ?string, - doToast: ({ message: string }) => void, - primaryButton?: boolean, - name?: string, - onCopy?: (string) => string, - enableMask?: boolean, -}; - -export default function CopyableStreamkey(props: Props) { - const { copyable, doToast, snackMessage, primaryButton = false, name, onCopy, enableMask = true } = props; - - const input = useRef(); - - function copyToClipboard() { - const topRef = input.current; - if (topRef[1].type === 'password') { - navigator.clipboard.writeText(topRef[1].defaultValue); - } - if (topRef[1].type === 'text') { - topRef[1].select(); - if (onCopy) { - onCopy(topRef[1]); - } - } - - document.execCommand('copy'); - } - - function checkMaskType() { - if (enableMask === true) { - return 'password'; - } - if (enableMask === false) { - return 'text'; - } - } - function showStreamkeyFunc() { - const topRef = input.current; - if (topRef[1].type === 'password') { - topRef[1].type = 'text'; - topRef[0].innerText = 'Hide'; - return; - } - if (topRef[1].type === 'text') { - topRef[1].type = 'password'; - topRef[0].innerText = 'Show'; - } - } - - return ( - -
-
- {' '} -
- { - copyToClipboard(); - doToast({ - message: snackMessage || __('Text copied'), - }); - }} - /> - } - /> - -
- ); -} diff --git a/ui/page/livestreamSetup/view.jsx b/ui/page/livestreamSetup/view.jsx index e535751a0..ab3e06f4f 100644 --- a/ui/page/livestreamSetup/view.jsx +++ b/ui/page/livestreamSetup/view.jsx @@ -13,7 +13,6 @@ import { Lbry } from 'lbry-redux'; import { toHex } from 'util/hex'; import { FormField } from 'component/common/form'; import CopyableText from 'component/copyableText'; -import CopyableStreamkey from 'component/copyableStreamkey'; import Card from 'component/common/card'; import ClaimList from 'component/claimList'; import usePersistedState from 'effects/use-persisted-state'; @@ -187,7 +186,7 @@ export default function LivestreamSetupPage(props: Props) { copyable={LIVESTREAM_RTMP_URL} snackMessage={__('Copied')} /> - Date: Thu, 23 Sep 2021 17:50:43 +0800 Subject: [PATCH 22/67] Restore "Stream Key Button (#7127)" + lint and modifications - Consolidate functionality into existing component. - Use proper strings. --- CHANGELOG.md | 1 + static/app-strings.json | 3 ++ ui/component/copyableText/view.jsx | 51 ++++++++++++++++++------------ ui/page/livestreamSetup/view.jsx | 5 +-- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebd737be..f03f0d4b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Clicking on the title of a floating player will take you back to the list ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921)) - Fix floating player stopping on markdown or image files ([#7073](https://github.com/lbryio/lbry-desktop/pull/7073)) - Fix list thumbnail upload ([#7074](https://github.com/lbryio/lbry-desktop/pull/7074)) +- Stream Key is now hidden _community pr!_ ([#7127](https://github.com/lbryio/lbry-desktop/pull/7127)) ## [0.51.2] - [2021-08-20] diff --git a/static/app-strings.json b/static/app-strings.json index b73b0aa35..2449386a6 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -207,6 +207,9 @@ "View": "View", "Edit": "Edit", "Copied": "Copied", + "Copied stream key.": "Copied stream key.", + "Copied stream server URL.": "Copied stream server URL.", + "Failed to copy.": "Failed to copy.", "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.": "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.", "Connecting...": "Connecting...", "Comments": "Comments", diff --git a/ui/component/copyableText/view.jsx b/ui/component/copyableText/view.jsx index 195d8070b..a02ea44cf 100644 --- a/ui/component/copyableText/view.jsx +++ b/ui/component/copyableText/view.jsx @@ -12,23 +12,38 @@ type Props = { primaryButton?: boolean, name?: string, onCopy?: (string) => string, + enableInputMask?: boolean, }; export default function CopyableText(props: Props) { - const { copyable, doToast, snackMessage, label, primaryButton = false, name, onCopy } = props; + const { copyable, doToast, snackMessage, label, primaryButton = false, name, onCopy, enableInputMask } = props; + const [maskInput, setMaskInput] = React.useState(enableInputMask); const input = useRef(); - function copyToClipboard() { - const topRef = input.current; - if (topRef && topRef.input && topRef.input.current) { - topRef.input.current.select(); - if (onCopy) { - onCopy(topRef.input.current); + function handleCopyText() { + if (enableInputMask) { + navigator.clipboard + .writeText(copyable) + .then(() => { + doToast({ message: snackMessage || __('Text copied') }); + }) + .catch(() => { + doToast({ message: __('Failed to copy.'), isError: true }); + }); + } else { + const topRef = input.current; + if (topRef && topRef.input && topRef.input.current) { + topRef.input.current.select(); + if (onCopy) { + // Allow clients to change the selection before making the copy. + onCopy(topRef.input.current); + } } - } - document.execCommand('copy'); + document.execCommand('copy'); + doToast({ message: snackMessage || __('Text copied') }); + } } function onFocus() { @@ -41,7 +56,7 @@ export default function CopyableText(props: Props) { return ( { - copyToClipboard(); - doToast({ - message: snackMessage || __('Text copied'), - }); - }} - /> +
); + const renderUris = uris || claimSearchResult; + // ************************************************************************** // Helpers // ************************************************************************** @@ -514,41 +506,6 @@ function ClaimListDiscover(props: Props) { } } - function urisEqual(prev: Array, next: Array) { - if (!prev || !next) { - // From 'ClaimList', "null" and "undefined" have special meaning, - // so we can't just compare array length here. - // - null = "timed out" - // - undefined = "no result". - return prev === next; - } - return prev.length === next.length && prev.every((value, index) => value === next[index]); - } - - function getFinalUrisInitialState(isNavigatingBack, claimSearchResult) { - if (isNavigatingBack && claimSearchResult && claimSearchResult.length > 0) { - return claimSearchResult; - } else { - return []; - } - } - - function fetchViewCountForUris(uris) { - const claimIds = []; - - if (uris) { - uris.forEach((uri) => { - if (claimsByUri[uri]) { - claimIds.push(claimsByUri[uri].claim_id); - } - }); - } - - if (claimIds.length > 0) { - doFetchViewCount(claimIds.join(',')); - } - } - function resolveOrderByOption(orderBy: string | Array, sortBy: string | Array) { const order_by = orderBy === CS.ORDER_BY_TRENDING @@ -567,38 +524,15 @@ function ClaimListDiscover(props: Props) { // ************************************************************************** // ************************************************************************** + useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount); + React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); doClaimSearch(searchOptions); - - if (liveLivestreamsFirst && options.page === 1) { - doClaimSearch(getLivestreamOnlyOptions(searchOptions)); - } } }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]); - // Resolve 'finalUri' - React.useEffect(() => { - if (uris) { - if (!urisEqual(uris, finalUris)) { - setFinalUris(uris); - } - } else { - // Wait until all queries are done before updating the uris to avoid layout shifts. - const pending = claimSearchResult === undefined || (liveLivestreamsFirst && livestreamSearchResult === undefined); - if (!pending && !urisEqual(claimSearchResult, finalUris)) { - setFinalUris(claimSearchResult); - } - } - }, [uris, claimSearchResult, finalUris, setFinalUris, liveLivestreamsFirst, livestreamSearchResult]); - - React.useEffect(() => { - if (fetchViewCount) { - fetchViewCountForUris(finalUris); - } - }, [finalUris]); // eslint-disable-line react-hooks/exhaustive-deps - const headerToUse = header || ( { streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state), wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state), isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), + isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state), isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state), collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state), collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state), diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index 22f3812db..343424d5f 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -80,7 +80,7 @@ type Props = { repostUrl?: string, hideMenu?: boolean, isLivestream?: boolean, - live?: boolean, + isLivestreamActive: boolean, collectionId?: string, editCollection: (string, CollectionEditParams) => void, isCollectionMine: boolean, @@ -145,7 +145,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { hideMenu = false, // repostUrl, isLivestream, // need both? CHECK - live, + isLivestreamActive, collectionId, collectionIndex, editCollection, @@ -336,7 +336,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { } let liveProperty = null; - if (live === true) { + if (isLivestreamActive === true) { liveProperty = (claim) => <>LIVE; } @@ -349,7 +349,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { 'claim-preview__wrapper--channel': isChannelUri && type !== 'inline', 'claim-preview__wrapper--inline': type === 'inline', 'claim-preview__wrapper--small': type === 'small', - 'claim-preview__live': live, + 'claim-preview__live': isLivestreamActive, 'claim-preview__active': active, })} > @@ -386,7 +386,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { )} {/* @endif */} - {!isLivestream && ( + {(!isLivestream || isLivestreamActive) && (
diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index 6bfa3760d..e9372e4eb 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -13,6 +13,7 @@ import { } from 'lbry-redux'; import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; +import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; import { selectShowMatureContent } from 'redux/selectors/settings'; import ClaimPreviewTile from './view'; import formatMediaDuration from 'util/formatMediaDuration'; @@ -36,6 +37,7 @@ const select = (state, props) => { showMature: selectShowMatureContent(state), isMature: makeSelectClaimIsNsfw(props.uri)(state), isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), + isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state), }; }; diff --git a/ui/component/claimPreviewTile/view.jsx b/ui/component/claimPreviewTile/view.jsx index 1372e5918..235b86e4b 100644 --- a/ui/component/claimPreviewTile/view.jsx +++ b/ui/component/claimPreviewTile/view.jsx @@ -48,10 +48,10 @@ type Props = { showMature: boolean, showHiddenByUser?: boolean, properties?: (Claim) => void, - live?: boolean, collectionId?: string, showNoSourceClaims?: boolean, isLivestream: boolean, + isLivestreamActive: boolean, }; // preview image cards used in related video functionality @@ -75,9 +75,9 @@ function ClaimPreviewTile(props: Props) { showMature, showHiddenByUser, properties, - live, showNoSourceClaims, isLivestream, + isLivestreamActive, collectionId, mediaDuration, } = props; @@ -192,7 +192,7 @@ function ClaimPreviewTile(props: Props) { } let liveProperty = null; - if (live === true) { + if (isLivestreamActive === true) { liveProperty = (claim) => <>LIVE; } @@ -201,7 +201,7 @@ function ClaimPreviewTile(props: Props) { onClick={handleClick} className={classnames('card claim-preview--tile', { 'claim-preview__wrapper--channel': isChannel, - 'claim-preview__live': live, + 'claim-preview__live': isLivestreamActive, })} > diff --git a/ui/component/claimTilesDiscover/index.js b/ui/component/claimTilesDiscover/index.js index a12963202..ebdcd3acb 100644 --- a/ui/component/claimTilesDiscover/index.js +++ b/ui/component/claimTilesDiscover/index.js @@ -1,27 +1,36 @@ import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import { doClaimSearch, selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, SETTINGS, selectClaimsByUri, + splitBySeparator, + MATURE_TAGS, } from 'lbry-redux'; import { doFetchViewCount } from 'lbryinc'; import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings'; import { selectModerationBlockList } from 'redux/selectors/comments'; import { selectMutedChannels } from 'redux/selectors/blocked'; +import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config'; +import * as CS from 'constants/claim_search'; + import ClaimListDiscover from './view'; -const select = (state) => ({ - claimSearchByQuery: selectClaimSearchByQuery(state), - claimsByUri: selectClaimsByUri(state), - fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state), - showNsfw: selectShowMatureContent(state), - hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), - mutedUris: selectMutedChannels(state), - blockedUris: selectModerationBlockList(state), -}); +const select = (state, props) => { + return { + claimSearchByQuery: selectClaimSearchByQuery(state), + claimsByUri: selectClaimsByUri(state), + fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state), + showNsfw: selectShowMatureContent(state), + hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), + mutedUris: selectMutedChannels(state), + blockedUris: selectModerationBlockList(state), + options: resolveSearchOptions({ pageSize: 8, ...props }), + }; +}; const perform = { doClaimSearch, @@ -29,4 +38,102 @@ const perform = { doFetchViewCount, }; -export default connect(select, perform)(ClaimListDiscover); +export default withRouter(connect(select, perform)(ClaimListDiscover)); + +// **************************************************************************** +// **************************************************************************** + +function resolveSearchOptions(props) { + const { + pageSize, + claimType, + tags, + showNsfw, + languages, + channelIds, + mutedUris, + blockedUris, + orderBy, + streamTypes, + hasNoSource, + hasSource, + releaseTime, + feeAmount, + limitClaimsPerChannel, + hideReposts, + timestamp, + claimIds, + location, + } = props; + + const mutedAndBlockedChannelIds = Array.from( + new Set((mutedUris || []).concat(blockedUris || []).map((uri) => splitBySeparator(uri)[1])) + ); + + const urlParams = new URLSearchParams(location.search); + const feeAmountInUrl = urlParams.get('fee_amount'); + const feeAmountParam = feeAmountInUrl || feeAmount; + + let streamTypesParam; + if (streamTypes) { + streamTypesParam = streamTypes; + } else if (SIMPLE_SITE && !hasNoSource && streamTypes !== null) { + streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO]; + } + + const options = { + page_size: pageSize, + claim_type: claimType || ['stream', 'repost', 'channel'], + // no_totals makes it so the sdk doesn't have to calculate total number pages for pagination + // it's faster, but we will need to remove it if we start using total_pages + no_totals: true, + any_tags: tags || [], + not_tags: !showNsfw ? MATURE_TAGS : [], + any_languages: languages, + channel_ids: channelIds || [], + not_channel_ids: mutedAndBlockedChannelIds, + order_by: orderBy || ['trending_group', 'trending_mixed'], + stream_types: streamTypesParam, + }; + + if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) { + options.has_no_source = true; + } else if (hasSource || (!ENABLE_NO_SOURCE_CLAIMS && (!claimType || claimType === 'stream'))) { + options.has_source = true; + } + + if (releaseTime) { + options.release_time = releaseTime; + } + + if (feeAmountParam) { + options.fee_amount = feeAmountParam; + } + + if (limitClaimsPerChannel) { + options.limit_claims_per_channel = limitClaimsPerChannel; + } + + // https://github.com/lbryio/lbry-desktop/issues/3774 + if (hideReposts) { + if (Array.isArray(options.claim_type)) { + options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost'); + } else { + options.claim_type = ['stream', 'channel']; + } + } + + if (claimType) { + options.claim_type = claimType; + } + + if (timestamp) { + options.timestamp = timestamp; + } + + if (claimIds) { + options.claim_ids = claimIds; + } + + return options; +} diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index f28e98817..e67b5c234 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -1,83 +1,41 @@ // @flow -import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config'; -import * as CS from 'constants/claim_search'; import type { Node } from 'react'; import React from 'react'; -import { createNormalizedClaimSearchKey, MATURE_TAGS, splitBySeparator } from 'lbry-redux'; +import { createNormalizedClaimSearchKey } from 'lbry-redux'; import ClaimPreviewTile from 'component/claimPreviewTile'; -import { useHistory } from 'react-router'; -import { getLivestreamOnlyOptions } from 'util/search'; +import useFetchViewCount from 'effects/use-fetch-view-count'; -/** - * Updates 'uris' by adding and/or moving active livestreams to the front of - * list. - * 'liveUris' is also updated with any entries that were moved to the - * front, for convenience. - * - * @param uris [Ref] - * @param liveUris [Ref] - * @param livestreamMap - * @param claimsByUri - * @param claimSearchByQuery - * @param options - */ -export function prioritizeActiveLivestreams( - uris: Array, - liveUris: Array, - livestreamMap: { [string]: any }, - claimsByUri: { [string]: any }, - claimSearchByQuery: { [string]: Array }, - options: any -) { - if (!livestreamMap || !uris) return; +type SearchOptions = { + page_size: number, + no_totals: boolean, + any_tags: Array, + channel_ids: Array, + claim_ids?: Array, + not_channel_ids: Array, + not_tags: Array, + order_by: Array, + languages?: Array, + release_time?: string, + claim_type?: string | Array, + timestamp?: string, + fee_amount?: string, + limit_claims_per_channel?: number, + stream_types?: Array, + has_source?: boolean, + has_no_source?: boolean, +}; - const claimIsLive = (claim, liveChannelIds) => { - // This function relies on: - // 1. Only 1 actual livestream per channel (i.e. all other livestream-claims - // for that channel actually point to the same source). - // 2. 'liveChannelIds' needs to be pruned after being accounted for, - // otherwise all livestream-claims will be "live" (we'll only take the - // latest one as "live" ). - return ( - claim && - claim.value_type === 'stream' && - claim.value.source === undefined && - claim.signing_channel && - liveChannelIds.includes(claim.signing_channel.claim_id) - ); - }; - - let liveChannelIds = Object.keys(livestreamMap); - - // 1. Collect active livestreams from the primary search to put in front. - uris.forEach((uri) => { - const claim = claimsByUri[uri]; - if (claimIsLive(claim, liveChannelIds)) { - liveUris.push(uri); - // This live channel has been accounted for, so remove it. - liveChannelIds.splice(liveChannelIds.indexOf(claim.signing_channel.claim_id), 1); - } - }); - - // 2. Now, repeat on the secondary search. - if (options) { - const livestreamsOnlySearchCacheQuery = createNormalizedClaimSearchKey(getLivestreamOnlyOptions(options)); - const livestreamsOnlyUris = claimSearchByQuery[livestreamsOnlySearchCacheQuery]; - if (livestreamsOnlyUris) { - livestreamsOnlyUris.forEach((uri) => { - const claim = claimsByUri[uri]; - if (!uris.includes(uri) && claimIsLive(claim, liveChannelIds)) { - liveUris.push(uri); - // This live channel has been accounted for, so remove it. - liveChannelIds.splice(liveChannelIds.indexOf(claim.signing_channel.claim_id), 1); - } - }); - } +function urisEqual(prev: ?Array, next: ?Array) { + if (!prev || !next) { + // ClaimList: "null" and "undefined" have special meaning, + // so we can't just compare array length here. + // - null = "timed out" + // - undefined = "no result". + return prev === next; } - // 3. Finalize uris by putting live livestreams in front. - const newUris = liveUris.concat(uris.filter((uri) => !liveUris.includes(uri))); - uris.splice(0, uris.length, ...newUris); + // $FlowFixMe - already checked for null above. + return prev.length === next.length && prev.every((value, index) => value === next[index]); } // **************************************************************************** @@ -88,8 +46,6 @@ type Props = { prefixUris?: Array, pinUrls?: Array, uris: Array, - liveLivestreamsFirst?: boolean, - livestreamMap?: { [string]: any }, showNoSourceClaims?: boolean, renderProperties?: (Claim) => ?Node, fetchViewCount?: boolean, @@ -109,6 +65,7 @@ type Props = { hasSource?: boolean, hasNoSource?: boolean, // --- select --- + location: { search: string }, claimSearchByQuery: { [string]: Array }, claimsByUri: { [string]: any }, fetchingClaimSearchByQuery: { [string]: boolean }, @@ -116,6 +73,7 @@ type Props = { hideReposts: boolean, mutedUris: Array, blockedUris: Array, + options: SearchOptions, // --- perform --- doClaimSearch: ({}) => void, doFetchViewCount: (claimIdCsv: string) => void, @@ -126,234 +84,72 @@ function ClaimTilesDiscover(props: Props) { doClaimSearch, claimSearchByQuery, claimsByUri, - showNsfw, - hideReposts, fetchViewCount, - // Below are options to pass that are forwarded to claim_search - tags, - channelIds, - claimIds, - orderBy, - pageSize = 8, - releaseTime, - languages, - claimType, - streamTypes, - timestamp, - feeAmount, - limitClaimsPerChannel, fetchingClaimSearchByQuery, - hasSource, hasNoSource, renderProperties, - blockedUris, - mutedUris, - liveLivestreamsFirst, - livestreamMap, pinUrls, prefixUris, showNoSourceClaims, doFetchViewCount, + pageSize = 8, + options, } = props; - const { location } = useHistory(); - const urlParams = new URLSearchParams(location.search); - const feeAmountInUrl = urlParams.get('fee_amount'); - const feeAmountParam = feeAmountInUrl || feeAmount; - const mutedAndBlockedChannelIds = Array.from( - new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1])) - ); - const liveUris = []; - let streamTypesParam; - if (streamTypes) { - streamTypesParam = streamTypes; - } else if (SIMPLE_SITE && !hasNoSource && streamTypes !== null) { - streamTypesParam = [CS.FILE_VIDEO, CS.FILE_AUDIO]; - } - - const [prevUris, setPrevUris] = React.useState([]); - - const options: { - page_size: number, - no_totals: boolean, - any_tags: Array, - channel_ids: Array, - claim_ids?: Array, - not_channel_ids: Array, - not_tags: Array, - order_by: Array, - languages?: Array, - release_time?: string, - claim_type?: string | Array, - timestamp?: string, - fee_amount?: string, - limit_claims_per_channel?: number, - stream_types?: Array, - has_source?: boolean, - has_no_source?: boolean, - } = { - page_size: pageSize, - claim_type: claimType || ['stream', 'repost', 'channel'], - // no_totals makes it so the sdk doesn't have to calculate total number pages for pagination - // it's faster, but we will need to remove it if we start using total_pages - no_totals: true, - any_tags: tags || [], - not_tags: !showNsfw ? MATURE_TAGS : [], - any_languages: languages, - channel_ids: channelIds || [], - not_channel_ids: mutedAndBlockedChannelIds, - order_by: orderBy || ['trending_group', 'trending_mixed'], - stream_types: streamTypesParam, - }; - - if (ENABLE_NO_SOURCE_CLAIMS && hasNoSource) { - options.has_no_source = true; - } else if (hasSource || (!ENABLE_NO_SOURCE_CLAIMS && (!claimType || claimType === 'stream'))) { - options.has_source = true; - } - - if (releaseTime) { - options.release_time = releaseTime; - } - - if (feeAmountParam) { - options.fee_amount = feeAmountParam; - } - - if (limitClaimsPerChannel) { - options.limit_claims_per_channel = limitClaimsPerChannel; - } - - // https://github.com/lbryio/lbry-desktop/issues/3774 - if (hideReposts) { - if (Array.isArray(options.claim_type)) { - options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost'); - } else { - options.claim_type = ['stream', 'channel']; - } - } - - if (claimType) { - options.claim_type = claimType; - } - - if (timestamp) { - options.timestamp = timestamp; - } - - if (claimIds) { - options.claim_ids = claimIds; - } - - const mainSearchKey = createNormalizedClaimSearchKey(options); - const livestreamSearchKey = liveLivestreamsFirst - ? createNormalizedClaimSearchKey(getLivestreamOnlyOptions(options)) - : undefined; - - let uris = (prefixUris || []).concat(claimSearchByQuery[mainSearchKey] || []); - - const isLoading = fetchingClaimSearchByQuery[mainSearchKey]; - - if (liveLivestreamsFirst && livestreamMap && !isLoading) { - prioritizeActiveLivestreams(uris, liveUris, livestreamMap, claimsByUri, claimSearchByQuery, options); - } + const searchKey = createNormalizedClaimSearchKey(options); + const fetchingClaimSearch = fetchingClaimSearchByQuery[searchKey]; + const claimSearchUris = claimSearchByQuery[searchKey] || []; // Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time const optionsStringForEffect = JSON.stringify(options); - const shouldPerformSearch = !isLoading && uris.length === 0; + const shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0; - if ( - prefixUris === undefined && - (claimSearchByQuery[mainSearchKey] === undefined || - (livestreamSearchKey && claimSearchByQuery[livestreamSearchKey] === undefined)) - ) { - // This is a new query and we don't have results yet ... - if (prevUris.length !== 0) { - // ... but we have previous results. Use it until new results are here. - uris = prevUris; - } - } + const uris = (prefixUris || []).concat(claimSearchUris); - const modifiedUris = uris ? uris.slice() : []; - const fixUris = pinUrls || []; - - if (pinUrls && modifiedUris && modifiedUris.length > 2 && window.location.pathname === '/') { - fixUris.forEach((fixUri) => { - if (modifiedUris.indexOf(fixUri) !== -1) { - modifiedUris.splice(modifiedUris.indexOf(fixUri), 1); + if (pinUrls && uris && uris.length > 2 && window.location.pathname === '/') { + pinUrls.forEach((pin) => { + if (uris.indexOf(pin) !== -1) { + uris.splice(uris.indexOf(pin), 1); } else { - modifiedUris.pop(); + uris.pop(); } }); - modifiedUris.splice(2, 0, ...fixUris); + uris.splice(2, 0, ...pinUrls); } - // ************************************************************************** - // ************************************************************************** - - function resolveLive(index) { - if (liveLivestreamsFirst && livestreamMap && index < liveUris.length) { - return true; - } - return undefined; + if (uris.length > 0 && uris.length < pageSize && shouldPerformSearch) { + // prefixUri and pinUrls might already be present while waiting for the + // remaining claim_search results. Fill the space to prevent layout shifts. + uris.push(...Array(pageSize - uris.length).fill('')); } - function fetchViewCountForUris(uris) { - const claimIds = []; - - if (uris) { - uris.forEach((uri) => { - if (claimsByUri[uri]) { - claimIds.push(claimsByUri[uri].claim_id); - } - }); - } - - if (claimIds.length > 0) { - doFetchViewCount(claimIds.join(',')); - } - } - - // ************************************************************************** - // ************************************************************************** + useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount); + // Run `doClaimSearch` React.useEffect(() => { if (shouldPerformSearch) { const searchOptions = JSON.parse(optionsStringForEffect); doClaimSearch(searchOptions); - - if (liveLivestreamsFirst) { - doClaimSearch(getLivestreamOnlyOptions(searchOptions)); - } } - }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, liveLivestreamsFirst]); - - React.useEffect(() => { - if (JSON.stringify(prevUris) !== JSON.stringify(uris) && !shouldPerformSearch) { - // Stash new results for next render cycle: - setPrevUris(uris); - // Fetch view count: - if (fetchViewCount) { - fetchViewCountForUris(uris); - } - } - }, [shouldPerformSearch, prevUris, uris]); // eslint-disable-line react-hooks/exhaustive-deps - - // ************************************************************************** - // ************************************************************************** + }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]); return (
    - {modifiedUris && modifiedUris.length - ? modifiedUris.map((uri, index) => ( - - )) + {uris && uris.length + ? uris.map((uri, i) => { + if (uri) { + return ( + + ); + } else { + return ; + } + }) : new Array(pageSize) .fill(1) .map((x, i) => ( @@ -362,4 +158,75 @@ function ClaimTilesDiscover(props: Props) {
); } -export default ClaimTilesDiscover; + +export default React.memo(ClaimTilesDiscover, areEqual); + +function debug_trace(val) { + if (process.env.DEBUG_TRACE) console.log(`Render due to: ${val}`); +} + +function areEqual(prev: Props, next: Props) { + const prevOptions: SearchOptions = prev.options; + const nextOptions: SearchOptions = next.options; + + const prevSearchKey = createNormalizedClaimSearchKey(prevOptions); + const nextSearchKey = createNormalizedClaimSearchKey(nextOptions); + + // "Pause" render when fetching to solve the layout-shift problem in #5979 + // (previous solution used a stashed copy of the rendered uris while fetching + // to make it stay still). + // This version works as long as we are not doing anything during a fetch, + // such as showing a spinner. + const nextCsFetch = next.fetchingClaimSearchByQuery[nextSearchKey]; + if (nextCsFetch) { + return true; + } + + // --- Deep-compare --- + if (prev.claimSearchByQuery[prevSearchKey] !== next.claimSearchByQuery[nextSearchKey]) { + debug_trace('claimSearchByQuery'); + return false; + } + + if (prev.fetchingClaimSearchByQuery[prevSearchKey] !== next.fetchingClaimSearchByQuery[nextSearchKey]) { + debug_trace('fetchingClaimSearchByQuery'); + return false; + } + + const ARRAY_KEYS = ['prefixUris', 'channelIds', 'mutedUris', 'blockedUris']; + + for (let i = 0; i < ARRAY_KEYS.length; ++i) { + const key = ARRAY_KEYS[i]; + if (!urisEqual(prev[key], next[key])) { + debug_trace(`${key}`); + return false; + } + } + + // --- Default the rest(*) to shallow-compare --- + // (*) including new props introduced in the future, in case developer forgets + // to update this function. Better to render more than miss an important one. + const KEYS_TO_IGNORE = [ + ...ARRAY_KEYS, + 'claimSearchByQuery', + 'fetchingClaimSearchByQuery', + 'location', + 'history', + 'match', + 'claimsByUri', + 'options', + 'doClaimSearch', + 'doToggleTagFollowDesktop', + ]; + + const propKeys = Object.keys(next); + for (let i = 0; i < propKeys.length; ++i) { + const pk = propKeys[i]; + if (!KEYS_TO_IGNORE.includes(pk) && prev[pk] !== next[pk]) { + debug_trace(`${pk}`); + return false; + } + } + + return true; +} diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 4b02a2128..17748007e 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -352,3 +352,7 @@ export const FETCH_NO_SOURCE_CLAIMS_STARTED = 'FETCH_NO_SOURCE_CLAIMS_STARTED'; export const FETCH_NO_SOURCE_CLAIMS_COMPLETED = 'FETCH_NO_SOURCE_CLAIMS_COMPLETED'; export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED'; export const VIEWERS_RECEIVED = 'VIEWERS_RECEIVED'; +export const FETCH_ACTIVE_LIVESTREAMS_STARTED = 'FETCH_ACTIVE_LIVESTREAMS_STARTED'; +export const FETCH_ACTIVE_LIVESTREAMS_FAILED = 'FETCH_ACTIVE_LIVESTREAMS_FAILED'; +export const FETCH_ACTIVE_LIVESTREAMS_SKIPPED = 'FETCH_ACTIVE_LIVESTREAMS_SKIPPED'; +export const FETCH_ACTIVE_LIVESTREAMS_COMPLETED = 'FETCH_ACTIVE_LIVESTREAMS_COMPLETED'; diff --git a/ui/effects/use-fetch-view-count.js b/ui/effects/use-fetch-view-count.js new file mode 100644 index 000000000..b4f8b0892 --- /dev/null +++ b/ui/effects/use-fetch-view-count.js @@ -0,0 +1,24 @@ +// @flow +import { useState, useEffect } from 'react'; + +export default function useFetchViewCount( + shouldFetch: ?boolean, + uris: Array, + claimsByUri: any, + doFetchViewCount: (string) => void +) { + const [fetchedUris, setFetchedUris] = useState([]); + + useEffect(() => { + if (shouldFetch && uris && uris.length > 0) { + const urisToFetch = uris.filter((uri) => uri && !fetchedUris.includes(uri) && Boolean(claimsByUri[uri])); + + if (urisToFetch.length > 0) { + const claimIds = urisToFetch.map((uri) => claimsByUri[uri].claim_id); + doFetchViewCount(claimIds.join(',')); + setFetchedUris([...fetchedUris, ...urisToFetch]); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [uris]); +} diff --git a/ui/effects/use-get-livestreams.js b/ui/effects/use-get-livestreams.js deleted file mode 100644 index fb77c1395..000000000 --- a/ui/effects/use-get-livestreams.js +++ /dev/null @@ -1,55 +0,0 @@ -// @flow -import React from 'react'; -import { LIVESTREAM_LIVE_API } from 'constants/livestream'; - -/** - * Gets latest livestream info list. Returns null (instead of a blank object) - * when there are no active livestreams. - * - * @param minViewers - * @param refreshMs - * @returns {{livestreamMap: null, loading: boolean}} - */ -export default function useGetLivestreams(minViewers: number = 0, refreshMs: number = 0) { - const [loading, setLoading] = React.useState(true); - const [livestreamMap, setLivestreamMap] = React.useState(null); - - React.useEffect(() => { - function checkCurrentLivestreams() { - fetch(LIVESTREAM_LIVE_API) - .then((res) => res.json()) - .then((res) => { - setLoading(false); - if (!res.data) { - setLivestreamMap(null); - return; - } - - const livestreamMap = res.data.reduce((acc, curr) => { - if (curr.viewCount >= minViewers) { - acc[curr.claimId] = curr; - } - return acc; - }, {}); - - setLivestreamMap(livestreamMap); - }) - .catch((err) => { - setLoading(false); - }); - } - - checkCurrentLivestreams(); - - if (refreshMs > 0) { - let fetchInterval = setInterval(checkCurrentLivestreams, refreshMs); - return () => { - if (fetchInterval) { - clearInterval(fetchInterval); - } - }; - } - }, []); - - return { livestreamMap, loading }; -} diff --git a/ui/page/channelsFollowing/index.js b/ui/page/channelsFollowing/index.js index 599cbf841..c08a2107f 100644 --- a/ui/page/channelsFollowing/index.js +++ b/ui/page/channelsFollowing/index.js @@ -1,13 +1,18 @@ import { connect } from 'react-redux'; import { SETTINGS } from 'lbry-redux'; +import { doFetchActiveLivestreams } from 'redux/actions/livestream'; +import { selectActiveLivestreams } from 'redux/selectors/livestream'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import ChannelsFollowingPage from './view'; -const select = state => ({ +const select = (state) => ({ subscribedChannels: selectSubscriptions(state), tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state), + activeLivestreams: selectActiveLivestreams(state), }); -export default connect(select)(ChannelsFollowingPage); +export default connect(select, { + doFetchActiveLivestreams, +})(ChannelsFollowingPage); diff --git a/ui/page/channelsFollowing/view.jsx b/ui/page/channelsFollowing/view.jsx index 71e91d2b3..c7f9d7222 100644 --- a/ui/page/channelsFollowing/view.jsx +++ b/ui/page/channelsFollowing/view.jsx @@ -9,24 +9,32 @@ import ClaimListDiscover from 'component/claimListDiscover'; import Page from 'component/page'; import Button from 'component/button'; import Icon from 'component/common/icon'; -import useGetLivestreams from 'effects/use-get-livestreams'; import { splitBySeparator } from 'lbry-redux'; +import { getLivestreamUris } from 'util/livestream'; type Props = { subscribedChannels: Array, tileLayout: boolean, + activeLivestreams: ?LivestreamInfo, + doFetchActiveLivestreams: () => void, }; function ChannelsFollowingPage(props: Props) { - const { subscribedChannels, tileLayout } = props; + const { subscribedChannels, tileLayout, activeLivestreams, doFetchActiveLivestreams } = props; + const hasSubsribedChannels = subscribedChannels.length > 0; - const { livestreamMap } = useGetLivestreams(); + const channelIds = subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1]); + + React.useEffect(() => { + doFetchActiveLivestreams(); + }, []); return !hasSubsribedChannels ? ( ) : ( } defaultOrderBy={CS.ORDER_BY_NEW} - channelIds={subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1])} + channelIds={channelIds} meta={