Add ability to search through publishes. (#7535)

* Add ability to search through publishes.

* Small fix in allClaimListMine type.

* Small fix for search claims in uploads page.

* Add search term in uri when filtering uploads.

* ui/ux touchup

* no appstrings for you

* resolve conflicts

Co-authored-by: jessopb <36554050+jessopb@users.noreply.github.com>
This commit is contained in:
Franco Montenegro 2022-04-15 00:05:59 -03:00 committed by GitHub
parent 21204321c0
commit 50ae6e2869
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 37 deletions

View file

@ -2306,6 +2306,7 @@
"Privacy": "Privacy", "Privacy": "Privacy",
"LBRY takes privacy and choice seriously. Is it ok if we monitor performance and help creators track their views?": "LBRY takes privacy and choice seriously. Is it ok if we monitor performance and help creators track their views?", "LBRY takes privacy and choice seriously. Is it ok if we monitor performance and help creators track their views?": "LBRY takes privacy and choice seriously. Is it ok if we monitor performance and help creators track their views?",
"Yes, share with LBRY": "Yes, share with LBRY", "Yes, share with LBRY": "Yes, share with LBRY",
"Search Uploads": "Search Uploads",
"This refundable boost will improve the discoverability of this %claimTypeText% while active. ": "This refundable boost will improve the discoverability of this %claimTypeText% while active. ", "This refundable boost will improve the discoverability of this %claimTypeText% while active. ": "This refundable boost will improve the discoverability of this %claimTypeText% while active. ",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -128,6 +128,8 @@ export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED';
export const FETCH_CHANNEL_CLAIM_COUNT_STARTED = 'FETCH_CHANNEL_CLAIM_COUNT_STARTED'; export const FETCH_CHANNEL_CLAIM_COUNT_STARTED = 'FETCH_CHANNEL_CLAIM_COUNT_STARTED';
export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED'; export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED';
export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED'; export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED';
export const FETCH_ALL_CLAIM_LIST_MINE_STARTED = 'FETCH_ALL_CLAIM_LIST_MINE_STARTED';
export const FETCH_ALL_CLAIM_LIST_MINE_COMPLETED = 'FETCH_ALL_CLAIM_LIST_MINE_COMPLETED';
export const ABANDON_CLAIM_STARTED = 'ABANDON_CLAIM_STARTED'; export const ABANDON_CLAIM_STARTED = 'ABANDON_CLAIM_STARTED';
export const ABANDON_CLAIM_SUCCEEDED = 'ABANDON_CLAIM_SUCCEEDED'; 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';

View file

@ -1,11 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
selectIsFetchingClaimListMine, selectIsFetchingAllMyClaims,
selectMyClaimsPage,
selectMyClaimsPageItemCount,
selectFetchingMyClaimsPageError, selectFetchingMyClaimsPageError,
selectAllMyClaims,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { doFetchClaimListMine, doCheckPendingClaims } from 'redux/actions/claims'; import { doCheckPendingClaims, doFetchAllClaimListMine } from 'redux/actions/claims';
import { doClearPublish } from 'redux/actions/publish'; import { doClearPublish } from 'redux/actions/publish';
import FileListPublished from './view'; import FileListPublished from './view';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
@ -16,22 +15,22 @@ const select = (state, props) => {
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const page = Number(urlParams.get(PAGE_PARAM)) || '1'; const page = Number(urlParams.get(PAGE_PARAM)) || '1';
const pageSize = urlParams.get(PAGE_SIZE_PARAM) || String(MY_CLAIMS_PAGE_SIZE); const pageSize = urlParams.get(PAGE_SIZE_PARAM) || String(MY_CLAIMS_PAGE_SIZE);
const initialSearchTerm = urlParams.get('searchText') || '';
return { return {
page, page,
pageSize, pageSize,
fetching: selectIsFetchingClaimListMine(state), fetching: selectIsFetchingAllMyClaims(state),
urls: selectMyClaimsPage(state),
urlTotal: selectMyClaimsPageItemCount(state),
error: selectFetchingMyClaimsPageError(state), error: selectFetchingMyClaimsPageError(state),
myClaims: selectAllMyClaims(state),
initialSearchTerm,
}; };
}; };
const perform = (dispatch) => ({ const perform = (dispatch) => ({
checkPendingPublishes: () => dispatch(doCheckPendingClaims()), checkPendingPublishes: () => dispatch(doCheckPendingClaims()),
fetchClaimListMine: (page, pageSize, resolve, filterBy) =>
dispatch(doFetchClaimListMine(page, pageSize, resolve, filterBy)),
clearPublish: () => dispatch(doClearPublish()), clearPublish: () => dispatch(doClearPublish()),
fetchAllMyClaims: () => dispatch(doFetchAllClaimListMine()),
}); });
export default withRouter(connect(select, perform)(FileListPublished)); export default withRouter(connect(select, perform)(FileListPublished));

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useEffect } from 'react'; import React, { useEffect, useMemo } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
@ -10,28 +10,47 @@ import Paginate from 'component/common/paginate';
import { PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim'; import { PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
import { FormField, Form } from 'component/common/form';
import Icon from 'component/common/icon';
import debounce from 'util/debounce';
import classnames from 'classnames'; import classnames from 'classnames';
const FILTER_ALL = 'stream,repost'; const FILTER_ALL = 'stream,repost';
const FILTER_UPLOADS = 'stream'; const FILTER_UPLOADS = 'stream';
const FILTER_REPOSTS = 'repost'; const FILTER_REPOSTS = 'repost';
const PAGINATE_PARAM = 'page';
type Props = { type Props = {
checkPendingPublishes: () => void, checkPendingPublishes: () => void,
clearPublish: () => void, clearPublish: () => void,
fetchClaimListMine: (number, number, boolean, Array<string>) => void,
fetching: boolean, fetching: boolean,
urls: Array<string>,
urlTotal: number,
history: { replace: (string) => void, push: (string) => void }, history: { replace: (string) => void, push: (string) => void },
page: number, page: number,
pageSize: number, pageSize: number,
myClaims: any,
fetchAllMyClaims: () => void,
location: { search: string },
initialSearchTerm: string,
}; };
function FileListPublished(props: Props) { function FileListPublished(props: Props) {
const { checkPendingPublishes, clearPublish, fetchClaimListMine, fetching, urls, urlTotal, page, pageSize } = props; const {
checkPendingPublishes,
clearPublish,
fetching,
page,
pageSize,
myClaims,
fetchAllMyClaims,
location,
history,
initialSearchTerm,
} = props;
const [filterBy, setFilterBy] = React.useState(FILTER_ALL); const [filterBy, setFilterBy] = React.useState(FILTER_ALL);
const [searchText, setSearchText] = React.useState(initialSearchTerm);
const [filteredClaims, setFilteredClaims] = React.useState([]);
const { search } = location;
const params = {}; const params = {};
params[PAGE_PARAM] = Number(page); params[PAGE_PARAM] = Number(page);
@ -39,16 +58,71 @@ function FileListPublished(props: Props) {
const paramsString = JSON.stringify(params); const paramsString = JSON.stringify(params);
const doFilterClaims = () => {
if (fetching) {
return;
}
const filtered = myClaims.filter((claim) => {
const value = claim.value || {};
const src = value.source || {};
const title = (value.title || '').toLowerCase();
const description = (value.description || '').toLowerCase();
const tags = (value.tags || []).join('').toLowerCase();
const srcName = (src.name || '').toLowerCase();
const lowerCaseSearchText = searchText.toLowerCase();
const textMatches =
!searchText ||
title.indexOf(lowerCaseSearchText) !== -1 ||
description.indexOf(lowerCaseSearchText) !== -1 ||
tags.indexOf(lowerCaseSearchText) !== -1 ||
srcName.indexOf(lowerCaseSearchText) !== -1;
return textMatches && filterBy.includes(claim.value_type);
});
setFilteredClaims(filtered);
};
const debounceFilter = debounce(doFilterClaims, 200);
useEffect(() => { useEffect(() => {
checkPendingPublishes(); checkPendingPublishes();
}, [checkPendingPublishes]); }, [checkPendingPublishes]);
useEffect(() => { useEffect(() => {
if (paramsString && fetchClaimListMine) { const params = new URLSearchParams(search);
const params = JSON.parse(paramsString); params.set('searchText', searchText);
fetchClaimListMine(params.page, params.page_size, true, filterBy.split(',')); history.replace('?' + params.toString());
} debounceFilter();
}, [paramsString, filterBy, fetchClaimListMine]); }, [myClaims, searchText]);
useEffect(() => {
doFilterClaims();
}, [myClaims, filterBy]);
const urlTotal = filteredClaims.length;
const urls = useMemo(() => {
const params = JSON.parse(paramsString);
const zeroIndexPage = Math.max(0, params.page - 1);
const paginated = filteredClaims.slice(
zeroIndexPage * params.page_size,
zeroIndexPage * params.page_size + params.page_size
);
return paginated.map((claim) => claim.permanent_url);
}, [filteredClaims, paramsString]);
// Go back to the first page when the filtered claims change.
// This way, we avoid hiding results just because the
// user may be on a different page (page that was calculated
// using a different state, ie, different filtered claims)
useEffect(() => {
const params = new URLSearchParams(search);
params.set(PAGINATE_PARAM, '1');
history.replace('?' + params.toString());
}, [filteredClaims]);
useEffect(() => {
fetchAllMyClaims();
}, [fetchAllMyClaims]);
return ( return (
<Page> <Page>
@ -79,7 +153,10 @@ function FileListPublished(props: Props) {
<Button <Button
button="alt" button="alt"
label={__('Reposts')} label={__('Reposts')}
onClick={() => setFilterBy(FILTER_REPOSTS)} onClick={() => {
setFilterBy(FILTER_REPOSTS);
setSearchText('');
}}
className={classnames(`button-toggle`, { className={classnames(`button-toggle`, {
'button-toggle--active': filterBy === FILTER_REPOSTS, 'button-toggle--active': filterBy === FILTER_REPOSTS,
})} })}
@ -88,21 +165,24 @@ function FileListPublished(props: Props) {
} }
headerAltControls={ headerAltControls={
<div className="card__actions--inline"> <div className="card__actions--inline">
{!fetching && (
<Button
button="alt"
label={__('Refresh')}
icon={ICONS.REFRESH}
onClick={() => fetchClaimListMine(params.page, params.page_size, true, filterBy.split(','))}
/>
)}
<Button <Button
icon={ICONS.PUBLISH} button="alt"
button="secondary" label={__('Refresh')}
label={__('Upload')} icon={ICONS.REFRESH}
navigate={`/$/${PAGES.UPLOAD}`} disabled={fetching}
onClick={() => clearPublish()} onClick={fetchAllMyClaims}
/> />
<Form onSubmit={() => {}} className="wunderbar--inline">
<Icon icon={ICONS.SEARCH} />
<FormField
className="wunderbar__input--inline"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
type="text"
placeholder={__('Search Uploads')}
disabled={filterBy === FILTER_REPOSTS}
/>
</Form>
</div> </div>
} }
persistedStorageKey="claim-list-published" persistedStorageKey="claim-list-published"
@ -115,7 +195,7 @@ function FileListPublished(props: Props) {
</> </>
)} )}
</div> </div>
{!(urls && urls.length) && ( {!fetching && myClaims.length === 0 && (
<React.Fragment> <React.Fragment>
{!fetching ? ( {!fetching ? (
<section className="main--empty"> <section className="main--empty">

View file

@ -184,6 +184,29 @@ export function doFetchClaimListMine(
}; };
} }
export function doFetchAllClaimListMine() {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.FETCH_ALL_CLAIM_LIST_MINE_STARTED,
});
// $FlowFixMe
Lbry.claim_list({
page: 1,
page_size: 99999,
claim_type: ['stream', 'repost'],
resolve: true,
}).then((result: StreamListResponse) => {
dispatch({
type: ACTIONS.FETCH_ALL_CLAIM_LIST_MINE_COMPLETED,
data: {
result,
},
});
});
};
}
export function doAbandonTxo(txo: Txo, cb: (string) => void) { export function doAbandonTxo(txo: Txo, cb: (string) => void) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
if (cb) cb(ABANDON_STATES.PENDING); if (cb) cb(ABANDON_STATES.PENDING);

View file

@ -61,6 +61,8 @@ type State = {
isCheckingNameForPublish: boolean, isCheckingNameForPublish: boolean,
checkingPending: boolean, checkingPending: boolean,
checkingReflecting: boolean, checkingReflecting: boolean,
isFetchingAllClaimListMine: boolean,
allClaimListMine: Array<ChannelClaim | Claim>,
}; };
const reducers = {}; const reducers = {};
@ -109,6 +111,8 @@ const defaultState = {
isCheckingNameForPublish: false, isCheckingNameForPublish: false,
checkingPending: false, checkingPending: false,
checkingReflecting: false, checkingReflecting: false,
isFetchingAllClaimListMine: false,
allClaimListMine: [],
}; };
function handleClaimAction(state: State, action: any): State { function handleClaimAction(state: State, action: any): State {
@ -273,6 +277,17 @@ reducers[ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED] = (state: State, action: any):
}); });
}; };
reducers[ACTIONS.FETCH_ALL_CLAIM_LIST_MINE_STARTED] = (state: State): State =>
Object.assign({}, state, {
isFetchingAllClaimListMine: true,
});
reducers[ACTIONS.FETCH_ALL_CLAIM_LIST_MINE_COMPLETED] = (state: State, action: any): State =>
Object.assign({}, state, {
isFetchingAllClaimListMine: false,
allClaimListMine: action.data.result.items,
});
reducers[ACTIONS.FETCH_CHANNEL_LIST_STARTED] = (state: State): State => reducers[ACTIONS.FETCH_CHANNEL_LIST_STARTED] = (state: State): State =>
Object.assign({}, state, { fetchingMyChannels: true }); Object.assign({}, state, { fetchingMyChannels: true });

View file

@ -410,6 +410,10 @@ export const selectIsFetchingClaimListMine = (state: State) => selectState(state
export const selectMyClaimsPage = createSelector(selectState, (state) => state.myClaimsPageResults || []); export const selectMyClaimsPage = createSelector(selectState, (state) => state.myClaimsPageResults || []);
export const selectAllMyClaims = createSelector(selectState, (state) => state.allClaimListMine || []);
export const selectIsFetchingAllMyClaims = createSelector(selectState, (state) => state.isFetchingAllClaimListMine);
export const selectMyClaimsPageNumber = createSelector( export const selectMyClaimsPageNumber = createSelector(
selectState, selectState,
(state) => (state.claimListMinePage && state.claimListMinePage.items) || [], (state) => (state.claimListMinePage && state.claimListMinePage.items) || [],

View file

@ -98,6 +98,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
} }
border-radius: var(--border-radius);
} }
.wunderbar__suggestions { .wunderbar__suggestions {

View file

@ -2,10 +2,10 @@
// be triggered. The function will be called after it stops being called for // be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the // N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing. // leading edge, instead of the trailing.
export default function debouce(func, wait, immediate) { export default function debouce(func, waitInMs, immediate) {
let timeout; let timeout;
return function() { return function () {
const context = this; const context = this;
const args = arguments; const args = arguments;
const later = () => { const later = () => {
@ -15,7 +15,7 @@ export default function debouce(func, wait, immediate) {
const callNow = immediate && !timeout; const callNow = immediate && !timeout;
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(later, wait); timeout = setTimeout(later, waitInMs);
if (callNow) func.apply(context, args); if (callNow) func.apply(context, args);
}; };
} }