select tags before channels and filter channels by tag

moartags

CS tags followed category
continue button,
Remove card header on tags select

limitShow tags count

tags limit fix

debug cs tags highlighting

bugfix

yarnlock
This commit is contained in:
jessop 2020-03-11 21:43:52 -04:00
parent f8357c4ec6
commit d9e65e8328
22 changed files with 325 additions and 93 deletions

View file

@ -131,7 +131,7 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#8245b055746216f7e1a12744fe6fbda3e3e90705", "lbry-redux": "lbryio/lbry-redux#6ed0dde5cbd7c25aa02631d5fa31fb6a4de76876",
"lbryinc": "lbryio/lbryinc#275f35b31ec614e2b89689f860fe19e645deee68", "lbryinc": "lbryio/lbryinc#275f35b31ec614e2b89689f860fe19e645deee68",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -1034,6 +1034,8 @@
"Deleting or editing comments is not currently possible. Please be mindful of this when posting.": "Deleting or editing comments is not currently possible. Please be mindful of this when posting.", "Deleting or editing comments is not currently possible. Please be mindful of this when posting.": "Deleting or editing comments is not currently possible. Please be mindful of this when posting.",
"When the alpha ends, we will attempt to transition comments, but do not promise to do so.": "When the alpha ends, we will attempt to transition comments, but do not promise to do so.", "When the alpha ends, we will attempt to transition comments, but do not promise to do so.": "When the alpha ends, we will attempt to transition comments, but do not promise to do so.",
"More Channels": "More Channels", "More Channels": "More Channels",
"Known Tags": "Known Tags",
"More Channels": "More Channels",
"You arent blocking any channels": "You arent blocking any channels", "You arent blocking any channels": "You arent blocking any channels",
"When you block a channel, all content from that channel will be hidden.": "When you block a channel, all content from that channel will be hidden.", "When you block a channel, all content from that channel will be hidden.": "When you block a channel, all content from that channel will be hidden.",
"View top claims for %normalized_uri%": "View top claims for %normalized_uri%", "View top claims for %normalized_uri%": "View top claims for %normalized_uri%",

View file

@ -9,6 +9,7 @@ import {
selectUploadCount, selectUploadCount,
selectUnclaimedRewards, selectUnclaimedRewards,
doUserSetReferrer, doUserSetReferrer,
selectUserVerifiedEmail,
} from 'lbryinc'; } from 'lbryinc';
import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux'; import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux';
import { makeSelectClientSetting, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings';
@ -35,6 +36,7 @@ const select = state => ({
syncError: selectGetSyncErrorMessage(state), syncError: selectGetSyncErrorMessage(state),
uploadCount: selectUploadCount(state), uploadCount: selectUploadCount(state),
rewards: selectUnclaimedRewards(state), rewards: selectUnclaimedRewards(state),
isAuthenticated: selectUserVerifiedEmail(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
@ -50,9 +52,4 @@ const perform = dispatch => ({
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)), setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
}); });
export default hot( export default hot(connect(select, perform)(App));
connect(
select,
perform
)(App)
);

View file

@ -68,6 +68,7 @@ type Props = {
rewards: Array<Reward>, rewards: Array<Reward>,
setReferrer: (string, boolean) => void, setReferrer: (string, boolean) => void,
analyticsTagSync: () => void, analyticsTagSync: () => void,
isAuthenticated: boolean,
}; };
function App(props: Props) { function App(props: Props) {
@ -93,6 +94,7 @@ function App(props: Props) {
rewards, rewards,
setReferrer, setReferrer,
analyticsTagSync, analyticsTagSync,
isAuthenticated,
} = props; } = props;
const appRef = useRef(); const appRef = useRef();
@ -242,11 +244,11 @@ function App(props: Props) {
}, [hasVerifiedEmail, syncEnabled, checkSync]); }, [hasVerifiedEmail, syncEnabled, checkSync]);
useEffect(() => { useEffect(() => {
if (syncError) { if (syncError && isAuthenticated) {
history.push(`/$/${PAGES.AUTH}?redirect=${pathname}`); history.push(`/$/${PAGES.AUTH}?redirect=${pathname}`);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncError, pathname]); }, [syncError, pathname, isAuthenticated]);
// @if TARGET='web' // @if TARGET='web'
useEffect(() => { useEffect(() => {

View file

@ -5,12 +5,14 @@ import {
selectFetchingClaimSearch, selectFetchingClaimSearch,
selectBlockedChannels, selectBlockedChannels,
SETTINGS, SETTINGS,
selectFollowedTags,
} from 'lbry-redux'; } from 'lbry-redux';
import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import ClaimListDiscover from './view'; import ClaimListDiscover from './view';
const select = state => ({ const select = state => ({
followedTags: selectFollowedTags(state),
claimSearchByQuery: selectClaimSearchByQuery(state), claimSearchByQuery: selectClaimSearchByQuery(state),
loading: selectFetchingClaimSearch(state), loading: selectFetchingClaimSearch(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state), showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),

View file

@ -32,7 +32,8 @@ type Props = {
hiddenUris: Array<string>, hiddenUris: Array<string>,
hiddenNsfwMessage?: Node, hiddenNsfwMessage?: Node,
channelIds?: Array<string>, channelIds?: Array<string>,
tags: Array<string>, tags: string, // these are just going to be string. pass a CSV if you want multi
defaultTags: string,
orderBy?: Array<string>, orderBy?: Array<string>,
defaultOrderBy?: string, defaultOrderBy?: string,
freshness?: string, freshness?: string,
@ -49,6 +50,7 @@ type Props = {
renderProperties?: Claim => Node, renderProperties?: Claim => Node,
includeSupportAction?: boolean, includeSupportAction?: boolean,
pageSize?: number, pageSize?: number,
followedTags?: Array<Tag>,
}; };
function ClaimListDiscover(props: Props) { function ClaimListDiscover(props: Props) {
@ -56,11 +58,12 @@ function ClaimListDiscover(props: Props) {
doClaimSearch, doClaimSearch,
claimSearchByQuery, claimSearchByQuery,
tags, tags,
defaultTags,
loading, loading,
meta, meta,
channelIds, channelIds,
showNsfw, showNsfw,
showReposts, // showReposts,
history, history,
location, location,
hiddenUris, hiddenUris,
@ -81,6 +84,7 @@ function ClaimListDiscover(props: Props) {
renderProperties, renderProperties,
includeSupportAction, includeSupportAction,
hideFilter, hideFilter,
followedTags,
} = props; } = props;
const didNavigateForward = history.action === 'PUSH'; const didNavigateForward = history.action === 'PUSH';
const { search } = location; const { search } = location;
@ -88,9 +92,12 @@ function ClaimListDiscover(props: Props) {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [forceRefresh, setForceRefresh] = useState(); const [forceRefresh, setForceRefresh] = useState();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const followed = (followedTags && followedTags.map(t => t.name)) || [];
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const tagsParam = tags || urlParams.get(CS.TAGS_KEY) || null; const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
(tags && getParamFromTags(tags)) ||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
(defaultTags && getParamFromTags(defaultTags));
const orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy || CS.ORDER_BY_TRENDING; const orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy || CS.ORDER_BY_TRENDING;
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness; const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
const contentTypeParam = urlParams.get(CS.CONTENT_KEY); const contentTypeParam = urlParams.get(CS.CONTENT_KEY);
@ -102,7 +109,12 @@ function ClaimListDiscover(props: Props) {
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL); const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL);
const isFiltered = () => const isFiltered = () =>
Boolean(urlParams.get(CS.FRESH_KEY) || urlParams.get(CS.CONTENT_KEY) || urlParams.get(CS.DURATION_KEY)); Boolean(
urlParams.get(CS.FRESH_KEY) ||
urlParams.get(CS.CONTENT_KEY) ||
urlParams.get(CS.DURATION_KEY) ||
urlParams.get(CS.TAGS_KEY)
);
useEffect(() => { useEffect(() => {
if (isFiltered()) setExpanded(true); if (isFiltered()) setExpanded(true);
@ -113,7 +125,7 @@ function ClaimListDiscover(props: Props) {
page_size: number, page_size: number,
page: number, page: number,
no_totals: boolean, no_totals: boolean,
any_tags: Array<string>, any_tags?: Array<string>,
not_tags: Array<string>, not_tags: Array<string>,
channel_ids: Array<string>, channel_ids: Array<string>,
not_channel_ids: Array<string>, not_channel_ids: Array<string>,
@ -131,7 +143,6 @@ function ClaimListDiscover(props: Props) {
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination // 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 // it's faster, but we will need to remove it if we start using total_pages
no_totals: true, no_totals: true,
any_tags: tagsParam || [],
channel_ids: channelIds || [], channel_ids: channelIds || [],
not_channel_ids: not_channel_ids:
// If channelIds were passed in, we don't need not_channel_ids // If channelIds were passed in, we don't need not_channel_ids
@ -211,6 +222,17 @@ function ClaimListDiscover(props: Props) {
} }
} }
if (tagsParam) {
if (tagsParam !== CS.TAGS_ALL && tagsParam !== '') {
if (tagsParam === CS.TAGS_FOLLOWED) {
options.any_tags = followed;
} else if (Array.isArray(tagsParam)) {
options.any_tags = tagsParam;
} else {
options.any_tags = tagsParam.split(',');
}
}
}
// https://github.com/lbryio/lbry-desktop/issues/3774 // https://github.com/lbryio/lbry-desktop/issues/3774
// if (!showReposts) { // if (!showReposts) {
// if (Array.isArray(options.claim_type)) { // if (Array.isArray(options.claim_type)) {
@ -220,7 +242,7 @@ function ClaimListDiscover(props: Props) {
// } // }
// } // }
const hasMatureTags = tags && tags.some(t => MATURE_TAGS.includes(t)); const hasMatureTags = tagsParam && tagsParam.split(',').some(t => MATURE_TAGS.includes(t));
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options); const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
const uris = claimSearchByQuery[claimSearchCacheQuery] || []; const uris = claimSearchByQuery[claimSearchCacheQuery] || [];
const shouldPerformSearch = const shouldPerformSearch =
@ -265,6 +287,14 @@ function ClaimListDiscover(props: Props) {
history.push(url); history.push(url);
} }
function getParamFromTags(t) {
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
return t;
} else if (Array.isArray(t)) {
return t.join(',');
}
}
function buildUrl(delta) { function buildUrl(delta) {
const newUrlParams = new URLSearchParams(); const newUrlParams = new URLSearchParams();
CS.KEYS.forEach(k => { CS.KEYS.forEach(k => {
@ -300,6 +330,23 @@ function ClaimListDiscover(props: Props) {
newUrlParams.set(CS.DURATION_KEY, delta.value); newUrlParams.set(CS.DURATION_KEY, delta.value);
} }
break; break;
case CS.TAGS_KEY:
if (delta.value === CS.TAGS_ALL) {
if (defaultTags === CS.TAGS_ALL) {
newUrlParams.delete(CS.TAGS_KEY);
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value);
}
} else if (delta.value === CS.TAGS_FOLLOWED) {
if (defaultTags === CS.TAGS_FOLLOWED) {
newUrlParams.delete(CS.TAGS_KEY);
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value); // redundant but special
}
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value);
}
break;
} }
return `?${newUrlParams.toString()}`; return `?${newUrlParams.toString()}`;
} }
@ -393,46 +440,48 @@ function ClaimListDiscover(props: Props) {
)} )}
{/* CONTENT_TYPES FIELD */} {/* CONTENT_TYPES FIELD */}
<div {!claimType && (
className={classnames('claim-search__input-container', { <div
'claim-search__input-container--selected': contentTypeParam, className={classnames('claim-search__input-container', {
})} 'claim-search__input-container--selected': contentTypeParam,
>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected': contentTypeParam,
})} })}
type="select"
name="claimType"
label={__('Content Type')}
value={contentTypeParam || CS.CONTENT_ALL}
onChange={e =>
handleChange({
key: CS.CONTENT_KEY,
value: e.target.value,
})
}
> >
{CS.CONTENT_TYPES.map(type => { <FormField
if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIds)) { className={classnames('claim-search__dropdown', {
return ( 'claim-search__dropdown--selected': contentTypeParam,
<option key={type} value={type}> })}
{/* i18fixme */} type="select"
{type === CS.CLAIM_CHANNEL && __('Channel')} name="claimType"
{type === CS.CLAIM_REPOST && __('Repost')} label={__('Content Type')}
{type === CS.FILE_VIDEO && __('Video')} value={contentTypeParam || CS.CONTENT_ALL}
{type === CS.FILE_AUDIO && __('Audio')} onChange={e =>
{type === CS.FILE_IMAGE && __('Image')} handleChange({
{type === CS.FILE_MODEL && __('Model')} key: CS.CONTENT_KEY,
{type === CS.FILE_BINARY && __('Other')} value: e.target.value,
{type === CS.FILE_DOCUMENT && __('Document')} })
{type === CS.CONTENT_ALL && __('Any')}
</option>
);
} }
})} >
</FormField> {CS.CONTENT_TYPES.map(type => {
</div> if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIds)) {
return (
<option key={type} value={type}>
{/* i18fixme */}
{type === CS.CLAIM_CHANNEL && __('Channel')}
{type === CS.CLAIM_REPOST && __('Repost')}
{type === CS.FILE_VIDEO && __('Video')}
{type === CS.FILE_AUDIO && __('Audio')}
{type === CS.FILE_IMAGE && __('Image')}
{type === CS.FILE_MODEL && __('Model')}
{type === CS.FILE_BINARY && __('Other')}
{type === CS.FILE_DOCUMENT && __('Document')}
{type === CS.CONTENT_ALL && __('Any')}
</option>
);
}
})}
</FormField>
</div>
)}
{/* DURATIONS FIELD */} {/* DURATIONS FIELD */}
{showDuration && ( {showDuration && (
<div className={'claim-search__input-container'}> <div className={'claim-search__input-container'}>
@ -469,6 +518,50 @@ function ClaimListDiscover(props: Props) {
</FormField> </FormField>
</div> </div>
)} )}
{/* TAGS FIELD */}
{!tags && (
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected':
((!defaultTags || defaultTags === CS.TAGS_ALL) && tagsParam && tagsParam !== CS.TAGS_ALL) ||
(defaultTags === CS.TAGS_FOLLOWED && tagsParam !== CS.TAGS_FOLLOWED),
})}
label={__('Tags')}
type="select"
name="tags"
value={tagsParam || CS.TAGS_ALL}
onChange={e =>
handleChange({
key: CS.TAGS_KEY,
value: e.target.value,
})
}
>
{[
CS.TAGS_ALL,
CS.TAGS_FOLLOWED,
...followed,
...(followed.includes(tagsParam) || tagsParam === CS.TAGS_ALL || tagsParam === CS.TAGS_FOLLOWED
? []
: [tagsParam]), // if they unfollow while filtered, add Other
].map(tag => (
<option
key={tag}
value={tag}
className={classnames({
'claim-search__input-special': !followed.includes(tag),
})}
>
{followed.includes(tag) && typeof tag === 'string' && toCapitalCase(__(tag))}
{tag === CS.TAGS_ALL && __('Any')}
{tag === CS.TAGS_FOLLOWED && __('Following')}
{!followed.includes(tag) && tag !== CS.TAGS_ALL && tag !== CS.TAGS_FOLLOWED && __('Other')}
</option>
))}
</FormField>
</div>
)}
</div> </div>
</> </>
)} )}

View file

@ -6,7 +6,6 @@ import ClaimPreviewTile from 'component/claimPreviewTile';
type Props = { type Props = {
uris: Array<string>, uris: Array<string>,
doClaimSearch: ({}) => void, doClaimSearch: ({}) => void,
loading: boolean,
showNsfw: boolean, showNsfw: boolean,
showReposts: boolean, showReposts: boolean,
history: { action: string, push: string => void, replace: string => void }, history: { action: string, push: string => void, replace: string => void },
@ -29,9 +28,8 @@ function ClaimTilesDiscover(props: Props) {
const { const {
doClaimSearch, doClaimSearch,
claimSearchByQuery, claimSearchByQuery,
loading,
showNsfw, showNsfw,
showReposts, // showReposts,
hiddenUris, hiddenUris,
// Below are options to pass that are forwarded to claim_search // Below are options to pass that are forwarded to claim_search
tags, tags,
@ -95,7 +93,7 @@ function ClaimTilesDiscover(props: Props) {
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options); const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
const uris = claimSearchByQuery[claimSearchCacheQuery] || []; const uris = claimSearchByQuery[claimSearchCacheQuery] || [];
const shouldPerformSearch = !hasSearched || uris.length === 0 || (!loading && uris.length < pageSize); const shouldPerformSearch = !hasSearched || uris.length === 0;
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time // Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
const optionsStringForEffect = JSON.stringify(options); const optionsStringForEffect = JSON.stringify(options);

View file

@ -153,7 +153,7 @@ function PublishForm(props: Props) {
hideHeader hideHeader
label={__('Selected Tags')} label={__('Selected Tags')}
empty={__('No tags added')} empty={__('No tags added')}
limit={TAGS_LIMIT} limitSelect={TAGS_LIMIT}
help={__( help={__(
'Add tags that are relevant to your content. If mature content, ensure it is tagged mature. Tag abuse and missing mature tags will not be tolerated.' 'Add tags that are relevant to your content. If mature content, ensure it is tagged mature. Tag abuse and missing mature tags will not be tolerated.'
)} )}

View file

@ -18,7 +18,8 @@ type Props = {
placeholder?: string, placeholder?: string,
label?: string, label?: string,
disabled?: boolean, disabled?: boolean,
limit?: number, limitSelect?: number,
limitShow?: number,
}; };
/* /*
@ -42,7 +43,8 @@ export default function TagsSearch(props: Props) {
placeholder, placeholder,
label, label,
disabled, disabled,
limit, limitSelect,
limitShow = 5,
} = props; } = props;
const [newTag, setNewTag] = useState(''); const [newTag, setNewTag] = useState('');
const doesTagMatch = name => { const doesTagMatch = name => {
@ -60,10 +62,10 @@ export default function TagsSearch(props: Props) {
const suggestedTagsSet = setUnion(remainingFollowedTagsSet, unfollowedTagsSet); const suggestedTagsSet = setUnion(remainingFollowedTagsSet, unfollowedTagsSet);
const countWithoutMature = selectedTagsSet.has('mature') ? selectedTagsSet.size - 1 : selectedTagsSet.size; const countWithoutMature = selectedTagsSet.has('mature') ? selectedTagsSet.size - 1 : selectedTagsSet.size;
const maxed = Boolean(limit && countWithoutMature >= limit); const maxed = Boolean(limitSelect && countWithoutMature >= limitSelect);
const suggestedTags = Array.from(suggestedTagsSet) const suggestedTags = Array.from(suggestedTagsSet)
.filter(doesTagMatch) .filter(doesTagMatch)
.slice(0, 5); .slice(0, limitShow);
// tack 'mature' onto the end if it's not already in the list // tack 'mature' onto the end if it's not already in the list
if (!newTag && suggestMature && !suggestedTags.some(tag => tag === 'mature')) { if (!newTag && suggestMature && !suggestedTags.some(tag => tag === 'mature')) {
@ -116,7 +118,7 @@ export default function TagsSearch(props: Props) {
<React.Fragment> <React.Fragment>
<Form className="tags__input-wrapper" onSubmit={handleSubmit}> <Form className="tags__input-wrapper" onSubmit={handleSubmit}>
<label> <label>
{limit ? ( {limitSelect ? (
<I18nMessage <I18nMessage
tokens={{ tokens={{
number: 5 - countWithoutMature, number: 5 - countWithoutMature,

View file

@ -24,7 +24,8 @@ type Props = {
placeholder?: string, placeholder?: string,
disableAutoFocus?: boolean, disableAutoFocus?: boolean,
hideHeader?: boolean, hideHeader?: boolean,
limit?: number, limitShow?: number,
limitSelect?: number,
}; };
/* /*
@ -45,7 +46,8 @@ export default function TagsSelect(props: Props) {
placeholder, placeholder,
hideHeader, hideHeader,
label, label,
limit, limitShow,
limitSelect,
} = props; } = props;
const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false); const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false);
const tagsToDisplay = tagsChosen || followedTags; const tagsToDisplay = tagsChosen || followedTags;
@ -107,7 +109,8 @@ export default function TagsSelect(props: Props) {
disableAutoFocus={disableAutoFocus} disableAutoFocus={disableAutoFocus}
tagsPassedIn={tagsToDisplay} tagsPassedIn={tagsToDisplay}
placeholder={placeholder} placeholder={placeholder}
limit={limit} limitShow={limitShow}
limitSelect={limitSelect}
/> />
</React.Fragment> </React.Fragment>
} }

View file

@ -1,6 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux'; import { selectFollowedTags } from 'lbry-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { doChannelSubscribe } from 'redux/actions/subscriptions';
import UserChannelFollowIntro from './view'; import UserChannelFollowIntro from './view';
const select = state => ({ const select = state => ({
@ -8,4 +9,11 @@ const select = state => ({
subscribedChannels: selectSubscriptions(state), subscribedChannels: selectSubscriptions(state),
}); });
export default connect(select)(UserChannelFollowIntro); const perform = dispatch => ({
channelSubscribe: uri => dispatch(doChannelSubscribe(uri)),
});
export default connect(
select,
perform
)(UserChannelFollowIntro);

View file

@ -1,18 +1,32 @@
// @flow // @flow
import React from 'react'; import React, { useEffect } from 'react';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import * as CS from 'constants/claim_search'; import * as CS from 'constants/claim_search';
import Nag from 'component/common/nag'; import Nag from 'component/common/nag';
import { parseURI } from 'lbry-redux';
import Button from 'component/button';
import { Form } from 'component/common/form-components/form';
type Props = { type Props = {
subscribedChannels: Array<Subscription>, subscribedChannels: Array<Subscription>,
onContinue: () => void, onContinue: () => void,
onBack: () => void,
channelSubscribe: (sub: Subscription) => void,
}; };
const LBRYURI = 'lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a';
function UserChannelFollowIntro(props: Props) { function UserChannelFollowIntro(props: Props) {
const { subscribedChannels, onContinue } = props; const { subscribedChannels, channelSubscribe, onContinue, onBack } = props;
const followingCount = (subscribedChannels && subscribedChannels.length) || 0; const followingCount = (subscribedChannels && subscribedChannels.length) || 0;
// subscribe to lbry
useEffect(() => {
channelSubscribe({
channelName: parseURI(LBRYURI).claimName,
uri: LBRYURI,
});
}, []);
return ( return (
<React.Fragment> <React.Fragment>
<h1 className="section__title--large">{__('Find Channels to Follow')}</h1> <h1 className="section__title--large">{__('Find Channels to Follow')}</h1>
@ -21,13 +35,25 @@ function UserChannelFollowIntro(props: Props) {
'LBRY works better if you find and follow at least 5 creators you like. You can also block channels you never want to see.' 'LBRY works better if you find and follow at least 5 creators you like. You can also block channels you never want to see.'
)} )}
</p> </p>
<Form onSubmit={onContinue} className="section__body">
<div className="card__actions">
<Button button="secondary" onClick={onBack} label={__('Back')} />
<Button
button="primary"
type="Submit"
onClick={onContinue}
label={__('Continue')}
disabled={subscribedChannels.length < 2}
/>
</div>
</Form>
<div className="section__body"> <div className="section__body">
<ClaimListDiscover <ClaimListDiscover
defaultOrderBy={CS.ORDER_BY_TOP} defaultOrderBy={CS.ORDER_BY_TOP}
defaultFreshness={CS.FRESH_ALL} defaultFreshness={CS.FRESH_ALL}
claimType="channel" claimType="channel"
hideBlock hideBlock
hideFilter defaultTags={CS.TAGS_FOLLOWED}
/> />
{followingCount > 0 && ( {followingCount > 0 && (
<Nag <Nag

View file

@ -6,6 +6,7 @@ import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify'; import UserEmailVerify from 'component/userEmailVerify';
import UserFirstChannel from 'component/userFirstChannel'; import UserFirstChannel from 'component/userFirstChannel';
import UserChannelFollowIntro from 'component/userChannelFollowIntro'; import UserChannelFollowIntro from 'component/userChannelFollowIntro';
import UserTagFollowIntro from 'component/userTagFollowIntro';
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view'; import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
import { rewards as REWARDS, YOUTUBE_STATUSES } from 'lbryinc'; import { rewards as REWARDS, YOUTUBE_STATUSES } from 'lbryinc';
import UserVerify from 'component/userVerify'; import UserVerify from 'component/userVerify';
@ -59,10 +60,12 @@ function UserSignIn(props: Props) {
const { search } = location; const { search } = location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const redirect = urlParams.get('redirect'); const redirect = urlParams.get('redirect');
const step = urlParams.get('step');
const shouldRedirectImmediately = urlParams.get('immediate'); const shouldRedirectImmediately = urlParams.get('immediate');
const [initialSignInStep, setInitialSignInStep] = React.useState(); const [initialSignInStep, setInitialSignInStep] = React.useState();
const [hasSeenFollowList, setHasSeenFollowList] = usePersistedState('channel-follow-intro', false); const [hasSeenFollowList, setHasSeenFollowList] = usePersistedState('channel-follow-intro', false);
const [hasSkippedRewards, setHasSkippedRewards] = usePersistedState('skip-rewards-intro', false); const [hasSkippedRewards, setHasSkippedRewards] = usePersistedState('skip-rewards-intro', false);
const [hasSeenTagsList, setHasSeenTagsList] = usePersistedState('channel-follow-intro', false);
const hasVerifiedEmail = user && user.has_verified_email; const hasVerifiedEmail = user && user.has_verified_email;
const rewardsApproved = user && user.is_reward_approved; const rewardsApproved = user && user.is_reward_approved;
const isIdentityVerified = user && user.is_identity_verified; const isIdentityVerified = user && user.is_identity_verified;
@ -92,7 +95,8 @@ function UserSignIn(props: Props) {
channelCount === 0 && channelCount === 0 &&
!hasYoutubeChannels; !hasYoutubeChannels;
const showYoutubeTransfer = hasVerifiedEmail && hasYoutubeChannels && !isYoutubeTransferComplete; const showYoutubeTransfer = hasVerifiedEmail && hasYoutubeChannels && !isYoutubeTransferComplete;
const showFollowIntro = hasVerifiedEmail && !hasSeenFollowList; const showFollowIntro = step === 'channels' || (hasVerifiedEmail && !hasSeenFollowList);
const showTagsIntro = step === 'tags' || (hasVerifiedEmail && !hasSeenTagsList);
const canHijackSignInFlowWithSpinner = hasVerifiedEmail && !getSyncError && !showFollowIntro; const canHijackSignInFlowWithSpinner = hasVerifiedEmail && !getSyncError && !showFollowIntro;
const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel; const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel;
const isWaitingForSomethingToFinish = const isWaitingForSomethingToFinish =
@ -136,6 +140,34 @@ function UserSignIn(props: Props) {
history.replace(url); history.replace(url);
setHasSeenFollowList(true); setHasSeenFollowList(true);
}} }}
onBack={() => {
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=tags`;
if (redirect) {
url += `&redirect=${redirect}`;
}
if (shouldRedirectImmediately) {
url += `&immediate=true`;
}
history.replace(url);
setHasSeenFollowList(false);
}}
/>
),
showTagsIntro && (
<UserTagFollowIntro
onContinue={() => {
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=channels`;
if (redirect) {
url += `&redirect=${redirect}`;
}
if (shouldRedirectImmediately) {
url += `&immediate=true`;
}
history.replace(url);
setHasSeenTagsList(true);
}}
/> />
), ),
showYoutubeTransfer && ( showYoutubeTransfer && (

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux';
import UserTagFollowIntro from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
});
export default connect(select)(UserTagFollowIntro);

View file

@ -0,0 +1,52 @@
// @flow
import React from 'react';
import Nag from 'component/common/nag';
import TagsSelect from 'component/tagsSelect';
import Button from 'component/button';
import { Form } from 'component/common/form';
type Props = {
subscribedChannels: Array<Subscription>,
onContinue: () => void,
followedTags: Array<Tag>,
};
function UserChannelFollowIntro(props: Props) {
const { onContinue, followedTags } = props;
const followingCount = (followedTags && followedTags.length) || 0;
return (
<React.Fragment>
<h1 className="section__title--large">{__('Tag Selection')}</h1>
<p className="section__subtitle">{__('Select some tags to help us show you interesting things.')}</p>
<Form onSubmit={onContinue} className="section__body">
<div className="card__actions">
<Button
button="primary"
type="Submit"
onClick={onContinue}
label={__('Continue')}
disabled={followedTags.length < 1}
/>
</div>
</Form>
<div className="section__body">
<TagsSelect hideHeader limitShow={300} help={false} showClose={false} title={__('Follow New Tags')} />
{followingCount > 0 && (
<Nag
type="helpful"
message={
followingCount === 1
? __('You are currently following %followingCount% tag', { followingCount })
: __('You are currently following %followingCount% tags', { followingCount })
}
actionText={__('Continue')}
onClick={onContinue}
/>
)}
</div>
</React.Fragment>
);
}
export default UserChannelFollowIntro;

View file

@ -6,6 +6,9 @@ export const DURATION_KEY = 'duration';
export const TAGS_KEY = 't'; export const TAGS_KEY = 't';
export const CONTENT_KEY = 'content'; export const CONTENT_KEY = 'content';
export const TAGS_ALL = 'tags_any';
export const TAGS_FOLLOWED = 'tags_followed';
export const FRESH_DAY = 'day'; export const FRESH_DAY = 'day';
export const FRESH_WEEK = 'week'; export const FRESH_WEEK = 'week';
export const FRESH_MONTH = 'month'; export const FRESH_MONTH = 'month';

View file

@ -119,12 +119,7 @@ function ChannelsFollowingDiscover(props: Props) {
</div> </div>
))} ))}
<h1 className="claim-grid__title">{__('More Channels')}</h1> <h1 className="claim-grid__title">{__('More Channels')}</h1>
<ClaimListDiscover <ClaimListDiscover defaultOrderBy={CS.ORDER_BY_TOP} defaultFreshness={CS.FRESH_ALL} claimType="channel" />
defaultOrderBy={CS.ORDER_BY_TOP}
defaultFreshness={CS.FRESH_ALL}
claimType="channel"
hideFilter
/>
</Page> </Page>
); );
} }

View file

@ -8,6 +8,7 @@ import useHover from 'effects/use-hover';
import analytics from 'analytics'; import analytics from 'analytics';
import HiddenNsfw from 'component/common/hidden-nsfw'; import HiddenNsfw from 'component/common/hidden-nsfw';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import * as CS from 'constants/claim_search';
type Props = { type Props = {
location: { search: string }, location: { search: string },
@ -26,11 +27,11 @@ function TagsPage(props: Props) {
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const claimType = urlParams.get('claim_type'); const claimType = urlParams.get('claim_type');
const tagsQuery = urlParams.get('t') || ''; const tagsQuery = urlParams.get('t') || null;
const tags = tagsQuery.split(','); const tags = tagsQuery ? tagsQuery.split(',') : null;
// Eventually allow more than one tag on this page // Eventually allow more than one tag on this page
// Restricting to one to make follow/unfollow simpler // Restricting to one to make follow/unfollow simpler
const tag = tags[0]; const tag = (tags && tags[0]) || null;
const isFollowing = followedTags.map(({ name }) => name).includes(tag); const isFollowing = followedTags.map(({ name }) => name).includes(tag);
let label = isFollowing ? __('Following') : __('Follow'); let label = isFollowing ? __('Following') : __('Follow');
@ -39,10 +40,12 @@ function TagsPage(props: Props) {
} }
function handleFollowClick() { function handleFollowClick() {
doToggleTagFollowDesktop(tag); if (tag) {
doToggleTagFollowDesktop(tag);
const nowFollowing = !isFollowing; const nowFollowing = !isFollowing;
analytics.tagFollowEvent(tag, nowFollowing, 'tag-page'); analytics.tagFollowEvent(tag, nowFollowing, 'tag-page');
}
} }
return ( return (
@ -53,7 +56,9 @@ function TagsPage(props: Props) {
tag ? ( tag ? (
<span> <span>
<Icon icon={ICONS.TAG} size={10} /> <Icon icon={ICONS.TAG} size={10} />
{tag} {(tag === CS.TAGS_ALL && __('All Content')) ||
(tag === CS.TAGS_FOLLOWED && __('Followed Tags')) ||
__(tag)}
</span> </span>
) : ( ) : (
<span> <span>
@ -62,7 +67,7 @@ function TagsPage(props: Props) {
</span> </span>
) )
} }
tags={tags} defaultTags={CS.TAGS_ALL}
hiddenNsfwMessage={<HiddenNsfw type="page" />} hiddenNsfwMessage={<HiddenNsfw type="page" />}
meta={ meta={
tag && ( tag && (

View file

@ -7,18 +7,18 @@ import TagsSelect from 'component/tagsSelect';
import Page from 'component/page'; import Page from 'component/page';
import Button from 'component/button'; import Button from 'component/button';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import * as CS from 'constants/claim_search';
type Props = { type Props = {
followedTags: Array<Tag>,
email: string, email: string,
}; };
function DiscoverPage(props: Props) { function DiscoverPage(props: Props) {
const { followedTags, email } = props; const { email } = props;
return ( return (
<Page> <Page>
{(email || !IS_WEB) && <TagsSelect showClose title={__('Find New Tags To Follow')} />} {(email || !IS_WEB) && <TagsSelect showClose limitShow={300} title={__('Find New Tags To Follow')} />}
<ClaimListDiscover <ClaimListDiscover
headerLabel={ headerLabel={
<span> <span>
@ -28,7 +28,7 @@ function DiscoverPage(props: Props) {
} }
hideCustomization={IS_WEB && !email} hideCustomization={IS_WEB && !email}
personalView personalView
tags={followedTags.map(tag => tag.name)} defaultTags={CS.TAGS_FOLLOWED}
meta={ meta={
<Button <Button
button="link" button="link"

View file

@ -6,7 +6,7 @@ import TagsSelect from 'component/tagsSelect';
function FollowingPage() { function FollowingPage() {
return ( return (
<Page> <Page>
<TagsSelect showClose={false} title={__('Follow New Tags')} /> <TagsSelect limitShow={300} showClose={false} title={__('Follow New Tags')} />
</Page> </Page>
); );
} }

View file

@ -44,6 +44,9 @@
} }
} }
.claim-search__input-special {
font-weight: var(--font-weight-bold);
}
.claim-search__extra { .claim-search__extra {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -7081,9 +7081,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#8245b055746216f7e1a12744fe6fbda3e3e90705: lbry-redux@lbryio/lbry-redux#6ed0dde5cbd7c25aa02631d5fa31fb6a4de76876:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/8245b055746216f7e1a12744fe6fbda3e3e90705" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/6ed0dde5cbd7c25aa02631d5fa31fb6a4de76876"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"