Merge pull request #3844 from lbryio/feat-selectTagsFirstRun
select tags before channels and filter channels by tag
This commit is contained in:
commit
b5bb4b46d7
22 changed files with 325 additions and 93 deletions
|
@ -131,7 +131,7 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"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",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -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.",
|
||||
"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",
|
||||
"Known Tags": "Known Tags",
|
||||
"More Channels": "More Channels",
|
||||
"You aren’t blocking any channels": "You aren’t 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.",
|
||||
"View top claims for %normalized_uri%": "View top claims for %normalized_uri%",
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
selectUploadCount,
|
||||
selectUnclaimedRewards,
|
||||
doUserSetReferrer,
|
||||
selectUserVerifiedEmail,
|
||||
} from 'lbryinc';
|
||||
import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux';
|
||||
import { makeSelectClientSetting, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings';
|
||||
|
@ -35,6 +36,7 @@ const select = state => ({
|
|||
syncError: selectGetSyncErrorMessage(state),
|
||||
uploadCount: selectUploadCount(state),
|
||||
rewards: selectUnclaimedRewards(state),
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -50,9 +52,4 @@ const perform = dispatch => ({
|
|||
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
|
||||
});
|
||||
|
||||
export default hot(
|
||||
connect(
|
||||
select,
|
||||
perform
|
||||
)(App)
|
||||
);
|
||||
export default hot(connect(select, perform)(App));
|
||||
|
|
|
@ -68,6 +68,7 @@ type Props = {
|
|||
rewards: Array<Reward>,
|
||||
setReferrer: (string, boolean) => void,
|
||||
analyticsTagSync: () => void,
|
||||
isAuthenticated: boolean,
|
||||
};
|
||||
|
||||
function App(props: Props) {
|
||||
|
@ -93,6 +94,7 @@ function App(props: Props) {
|
|||
rewards,
|
||||
setReferrer,
|
||||
analyticsTagSync,
|
||||
isAuthenticated,
|
||||
} = props;
|
||||
|
||||
const appRef = useRef();
|
||||
|
@ -242,11 +244,11 @@ function App(props: Props) {
|
|||
}, [hasVerifiedEmail, syncEnabled, checkSync]);
|
||||
|
||||
useEffect(() => {
|
||||
if (syncError) {
|
||||
if (syncError && isAuthenticated) {
|
||||
history.push(`/$/${PAGES.AUTH}?redirect=${pathname}`);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [syncError, pathname]);
|
||||
}, [syncError, pathname, isAuthenticated]);
|
||||
|
||||
// @if TARGET='web'
|
||||
useEffect(() => {
|
||||
|
|
|
@ -5,12 +5,14 @@ import {
|
|||
selectFetchingClaimSearch,
|
||||
selectBlockedChannels,
|
||||
SETTINGS,
|
||||
selectFollowedTags,
|
||||
} from 'lbry-redux';
|
||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import ClaimListDiscover from './view';
|
||||
|
||||
const select = state => ({
|
||||
followedTags: selectFollowedTags(state),
|
||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
||||
loading: selectFetchingClaimSearch(state),
|
||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
||||
|
|
|
@ -32,7 +32,8 @@ type Props = {
|
|||
hiddenUris: Array<string>,
|
||||
hiddenNsfwMessage?: Node,
|
||||
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>,
|
||||
defaultOrderBy?: string,
|
||||
freshness?: string,
|
||||
|
@ -49,6 +50,7 @@ type Props = {
|
|||
renderProperties?: Claim => Node,
|
||||
includeSupportAction?: boolean,
|
||||
pageSize?: number,
|
||||
followedTags?: Array<Tag>,
|
||||
};
|
||||
|
||||
function ClaimListDiscover(props: Props) {
|
||||
|
@ -56,11 +58,12 @@ function ClaimListDiscover(props: Props) {
|
|||
doClaimSearch,
|
||||
claimSearchByQuery,
|
||||
tags,
|
||||
defaultTags,
|
||||
loading,
|
||||
meta,
|
||||
channelIds,
|
||||
showNsfw,
|
||||
showReposts,
|
||||
// showReposts,
|
||||
history,
|
||||
location,
|
||||
hiddenUris,
|
||||
|
@ -81,6 +84,7 @@ function ClaimListDiscover(props: Props) {
|
|||
renderProperties,
|
||||
includeSupportAction,
|
||||
hideFilter,
|
||||
followedTags,
|
||||
} = props;
|
||||
const didNavigateForward = history.action === 'PUSH';
|
||||
const { search } = location;
|
||||
|
@ -88,9 +92,12 @@ function ClaimListDiscover(props: Props) {
|
|||
const [page, setPage] = useState(1);
|
||||
const [forceRefresh, setForceRefresh] = useState();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const followed = (followedTags && followedTags.map(t => t.name)) || [];
|
||||
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 freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
|
||||
const contentTypeParam = urlParams.get(CS.CONTENT_KEY);
|
||||
|
@ -102,7 +109,12 @@ function ClaimListDiscover(props: Props) {
|
|||
|
||||
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL);
|
||||
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(() => {
|
||||
if (isFiltered()) setExpanded(true);
|
||||
|
@ -113,7 +125,7 @@ function ClaimListDiscover(props: Props) {
|
|||
page_size: number,
|
||||
page: number,
|
||||
no_totals: boolean,
|
||||
any_tags: Array<string>,
|
||||
any_tags?: Array<string>,
|
||||
not_tags: Array<string>,
|
||||
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
|
||||
// it's faster, but we will need to remove it if we start using total_pages
|
||||
no_totals: true,
|
||||
any_tags: tagsParam || [],
|
||||
channel_ids: channelIds || [],
|
||||
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
|
||||
// if (!showReposts) {
|
||||
// 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 uris = claimSearchByQuery[claimSearchCacheQuery] || [];
|
||||
const shouldPerformSearch =
|
||||
|
@ -265,6 +287,14 @@ function ClaimListDiscover(props: Props) {
|
|||
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) {
|
||||
const newUrlParams = new URLSearchParams();
|
||||
CS.KEYS.forEach(k => {
|
||||
|
@ -300,6 +330,23 @@ function ClaimListDiscover(props: Props) {
|
|||
newUrlParams.set(CS.DURATION_KEY, delta.value);
|
||||
}
|
||||
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()}`;
|
||||
}
|
||||
|
@ -393,46 +440,48 @@ function ClaimListDiscover(props: Props) {
|
|||
)}
|
||||
|
||||
{/* CONTENT_TYPES FIELD */}
|
||||
<div
|
||||
className={classnames('claim-search__input-container', {
|
||||
'claim-search__input-container--selected': contentTypeParam,
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
className={classnames('claim-search__dropdown', {
|
||||
'claim-search__dropdown--selected': contentTypeParam,
|
||||
{!claimType && (
|
||||
<div
|
||||
className={classnames('claim-search__input-container', {
|
||||
'claim-search__input-container--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 => {
|
||||
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
|
||||
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,
|
||||
})
|
||||
}
|
||||
})}
|
||||
</FormField>
|
||||
</div>
|
||||
>
|
||||
{CS.CONTENT_TYPES.map(type => {
|
||||
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 */}
|
||||
{showDuration && (
|
||||
<div className={'claim-search__input-container'}>
|
||||
|
@ -469,6 +518,50 @@ function ClaimListDiscover(props: Props) {
|
|||
</FormField>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -6,7 +6,6 @@ import ClaimPreviewTile from 'component/claimPreviewTile';
|
|||
type Props = {
|
||||
uris: Array<string>,
|
||||
doClaimSearch: ({}) => void,
|
||||
loading: boolean,
|
||||
showNsfw: boolean,
|
||||
showReposts: boolean,
|
||||
history: { action: string, push: string => void, replace: string => void },
|
||||
|
@ -29,9 +28,8 @@ function ClaimTilesDiscover(props: Props) {
|
|||
const {
|
||||
doClaimSearch,
|
||||
claimSearchByQuery,
|
||||
loading,
|
||||
showNsfw,
|
||||
showReposts,
|
||||
// showReposts,
|
||||
hiddenUris,
|
||||
// Below are options to pass that are forwarded to claim_search
|
||||
tags,
|
||||
|
@ -95,7 +93,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
|
||||
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
|
||||
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
|
||||
const optionsStringForEffect = JSON.stringify(options);
|
||||
|
||||
|
|
|
@ -153,7 +153,7 @@ function PublishForm(props: Props) {
|
|||
hideHeader
|
||||
label={__('Selected Tags')}
|
||||
empty={__('No tags added')}
|
||||
limit={TAGS_LIMIT}
|
||||
limitSelect={TAGS_LIMIT}
|
||||
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.'
|
||||
)}
|
||||
|
|
|
@ -18,7 +18,8 @@ type Props = {
|
|||
placeholder?: string,
|
||||
label?: string,
|
||||
disabled?: boolean,
|
||||
limit?: number,
|
||||
limitSelect?: number,
|
||||
limitShow?: number,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -42,7 +43,8 @@ export default function TagsSearch(props: Props) {
|
|||
placeholder,
|
||||
label,
|
||||
disabled,
|
||||
limit,
|
||||
limitSelect,
|
||||
limitShow = 5,
|
||||
} = props;
|
||||
const [newTag, setNewTag] = useState('');
|
||||
const doesTagMatch = name => {
|
||||
|
@ -60,10 +62,10 @@ export default function TagsSearch(props: Props) {
|
|||
const suggestedTagsSet = setUnion(remainingFollowedTagsSet, unfollowedTagsSet);
|
||||
|
||||
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)
|
||||
.filter(doesTagMatch)
|
||||
.slice(0, 5);
|
||||
.slice(0, limitShow);
|
||||
|
||||
// tack 'mature' onto the end if it's not already in the list
|
||||
if (!newTag && suggestMature && !suggestedTags.some(tag => tag === 'mature')) {
|
||||
|
@ -116,7 +118,7 @@ export default function TagsSearch(props: Props) {
|
|||
<React.Fragment>
|
||||
<Form className="tags__input-wrapper" onSubmit={handleSubmit}>
|
||||
<label>
|
||||
{limit ? (
|
||||
{limitSelect ? (
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
number: 5 - countWithoutMature,
|
||||
|
|
|
@ -24,7 +24,8 @@ type Props = {
|
|||
placeholder?: string,
|
||||
disableAutoFocus?: boolean,
|
||||
hideHeader?: boolean,
|
||||
limit?: number,
|
||||
limitShow?: number,
|
||||
limitSelect?: number,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -45,7 +46,8 @@ export default function TagsSelect(props: Props) {
|
|||
placeholder,
|
||||
hideHeader,
|
||||
label,
|
||||
limit,
|
||||
limitShow,
|
||||
limitSelect,
|
||||
} = props;
|
||||
const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false);
|
||||
const tagsToDisplay = tagsChosen || followedTags;
|
||||
|
@ -107,7 +109,8 @@ export default function TagsSelect(props: Props) {
|
|||
disableAutoFocus={disableAutoFocus}
|
||||
tagsPassedIn={tagsToDisplay}
|
||||
placeholder={placeholder}
|
||||
limit={limit}
|
||||
limitShow={limitShow}
|
||||
limitSelect={limitSelect}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectFollowedTags } from 'lbry-redux';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { doChannelSubscribe } from 'redux/actions/subscriptions';
|
||||
import UserChannelFollowIntro from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
@ -8,4 +9,11 @@ const select = state => ({
|
|||
subscribedChannels: selectSubscriptions(state),
|
||||
});
|
||||
|
||||
export default connect(select)(UserChannelFollowIntro);
|
||||
const perform = dispatch => ({
|
||||
channelSubscribe: uri => dispatch(doChannelSubscribe(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(UserChannelFollowIntro);
|
||||
|
|
|
@ -1,18 +1,32 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import ClaimListDiscover from 'component/claimListDiscover';
|
||||
import * as CS from 'constants/claim_search';
|
||||
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 = {
|
||||
subscribedChannels: Array<Subscription>,
|
||||
onContinue: () => void,
|
||||
onBack: () => void,
|
||||
channelSubscribe: (sub: Subscription) => void,
|
||||
};
|
||||
|
||||
const LBRYURI = 'lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a';
|
||||
function UserChannelFollowIntro(props: Props) {
|
||||
const { subscribedChannels, onContinue } = props;
|
||||
const { subscribedChannels, channelSubscribe, onContinue, onBack } = props;
|
||||
const followingCount = (subscribedChannels && subscribedChannels.length) || 0;
|
||||
|
||||
// subscribe to lbry
|
||||
useEffect(() => {
|
||||
channelSubscribe({
|
||||
channelName: parseURI(LBRYURI).claimName,
|
||||
uri: LBRYURI,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<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.'
|
||||
)}
|
||||
</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">
|
||||
<ClaimListDiscover
|
||||
defaultOrderBy={CS.ORDER_BY_TOP}
|
||||
defaultFreshness={CS.FRESH_ALL}
|
||||
claimType="channel"
|
||||
hideBlock
|
||||
hideFilter
|
||||
defaultTags={CS.TAGS_FOLLOWED}
|
||||
/>
|
||||
{followingCount > 0 && (
|
||||
<Nag
|
||||
|
|
|
@ -6,6 +6,7 @@ import UserEmailNew from 'component/userEmailNew';
|
|||
import UserEmailVerify from 'component/userEmailVerify';
|
||||
import UserFirstChannel from 'component/userFirstChannel';
|
||||
import UserChannelFollowIntro from 'component/userChannelFollowIntro';
|
||||
import UserTagFollowIntro from 'component/userTagFollowIntro';
|
||||
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
|
||||
import { rewards as REWARDS, YOUTUBE_STATUSES } from 'lbryinc';
|
||||
import UserVerify from 'component/userVerify';
|
||||
|
@ -59,10 +60,12 @@ function UserSignIn(props: Props) {
|
|||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const redirect = urlParams.get('redirect');
|
||||
const step = urlParams.get('step');
|
||||
const shouldRedirectImmediately = urlParams.get('immediate');
|
||||
const [initialSignInStep, setInitialSignInStep] = React.useState();
|
||||
const [hasSeenFollowList, setHasSeenFollowList] = usePersistedState('channel-follow-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 rewardsApproved = user && user.is_reward_approved;
|
||||
const isIdentityVerified = user && user.is_identity_verified;
|
||||
|
@ -92,7 +95,8 @@ function UserSignIn(props: Props) {
|
|||
channelCount === 0 &&
|
||||
!hasYoutubeChannels;
|
||||
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 isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel;
|
||||
const isWaitingForSomethingToFinish =
|
||||
|
@ -136,6 +140,34 @@ function UserSignIn(props: Props) {
|
|||
history.replace(url);
|
||||
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 && (
|
||||
|
|
9
ui/component/userTagFollowIntro/index.js
Normal file
9
ui/component/userTagFollowIntro/index.js
Normal 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);
|
52
ui/component/userTagFollowIntro/view.jsx
Normal file
52
ui/component/userTagFollowIntro/view.jsx
Normal 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;
|
|
@ -6,6 +6,9 @@ export const DURATION_KEY = 'duration';
|
|||
export const TAGS_KEY = 't';
|
||||
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_WEEK = 'week';
|
||||
export const FRESH_MONTH = 'month';
|
||||
|
|
|
@ -119,12 +119,7 @@ function ChannelsFollowingDiscover(props: Props) {
|
|||
</div>
|
||||
))}
|
||||
<h1 className="claim-grid__title">{__('More Channels')}</h1>
|
||||
<ClaimListDiscover
|
||||
defaultOrderBy={CS.ORDER_BY_TOP}
|
||||
defaultFreshness={CS.FRESH_ALL}
|
||||
claimType="channel"
|
||||
hideFilter
|
||||
/>
|
||||
<ClaimListDiscover defaultOrderBy={CS.ORDER_BY_TOP} defaultFreshness={CS.FRESH_ALL} claimType="channel" />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import useHover from 'effects/use-hover';
|
|||
import analytics from 'analytics';
|
||||
import HiddenNsfw from 'component/common/hidden-nsfw';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as CS from 'constants/claim_search';
|
||||
|
||||
type Props = {
|
||||
location: { search: string },
|
||||
|
@ -26,11 +27,11 @@ function TagsPage(props: Props) {
|
|||
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const claimType = urlParams.get('claim_type');
|
||||
const tagsQuery = urlParams.get('t') || '';
|
||||
const tags = tagsQuery.split(',');
|
||||
const tagsQuery = urlParams.get('t') || null;
|
||||
const tags = tagsQuery ? tagsQuery.split(',') : null;
|
||||
// Eventually allow more than one tag on this page
|
||||
// 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);
|
||||
let label = isFollowing ? __('Following') : __('Follow');
|
||||
|
@ -39,10 +40,12 @@ function TagsPage(props: Props) {
|
|||
}
|
||||
|
||||
function handleFollowClick() {
|
||||
doToggleTagFollowDesktop(tag);
|
||||
if (tag) {
|
||||
doToggleTagFollowDesktop(tag);
|
||||
|
||||
const nowFollowing = !isFollowing;
|
||||
analytics.tagFollowEvent(tag, nowFollowing, 'tag-page');
|
||||
const nowFollowing = !isFollowing;
|
||||
analytics.tagFollowEvent(tag, nowFollowing, 'tag-page');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -53,7 +56,9 @@ function TagsPage(props: Props) {
|
|||
tag ? (
|
||||
<span>
|
||||
<Icon icon={ICONS.TAG} size={10} />
|
||||
{tag}
|
||||
{(tag === CS.TAGS_ALL && __('All Content')) ||
|
||||
(tag === CS.TAGS_FOLLOWED && __('Followed Tags')) ||
|
||||
__(tag)}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
|
@ -62,7 +67,7 @@ function TagsPage(props: Props) {
|
|||
</span>
|
||||
)
|
||||
}
|
||||
tags={tags}
|
||||
defaultTags={CS.TAGS_ALL}
|
||||
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
||||
meta={
|
||||
tag && (
|
||||
|
|
|
@ -7,18 +7,18 @@ import TagsSelect from 'component/tagsSelect';
|
|||
import Page from 'component/page';
|
||||
import Button from 'component/button';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as CS from 'constants/claim_search';
|
||||
|
||||
type Props = {
|
||||
followedTags: Array<Tag>,
|
||||
email: string,
|
||||
};
|
||||
|
||||
function DiscoverPage(props: Props) {
|
||||
const { followedTags, email } = props;
|
||||
const { email } = props;
|
||||
|
||||
return (
|
||||
<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
|
||||
headerLabel={
|
||||
<span>
|
||||
|
@ -28,7 +28,7 @@ function DiscoverPage(props: Props) {
|
|||
}
|
||||
hideCustomization={IS_WEB && !email}
|
||||
personalView
|
||||
tags={followedTags.map(tag => tag.name)}
|
||||
defaultTags={CS.TAGS_FOLLOWED}
|
||||
meta={
|
||||
<Button
|
||||
button="link"
|
||||
|
|
|
@ -6,7 +6,7 @@ import TagsSelect from 'component/tagsSelect';
|
|||
function FollowingPage() {
|
||||
return (
|
||||
<Page>
|
||||
<TagsSelect showClose={false} title={__('Follow New Tags')} />
|
||||
<TagsSelect limitShow={300} showClose={false} title={__('Follow New Tags')} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.claim-search__input-special {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.claim-search__extra {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -7081,9 +7081,9 @@ lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#8245b055746216f7e1a12744fe6fbda3e3e90705:
|
||||
lbry-redux@lbryio/lbry-redux#6ed0dde5cbd7c25aa02631d5fa31fb6a4de76876:
|
||||
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:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
|
Loading…
Reference in a new issue