refactor 'active' channel usage across the app

This commit is contained in:
Sean Yesmunt 2021-02-09 11:05:56 -05:00
parent bce86ae8a3
commit db87125dc8
53 changed files with 726 additions and 944 deletions

View file

@ -740,7 +740,6 @@
"Repost": "Repost", "Repost": "Repost",
"Repost your favorite claims to help more people discover them!": "Repost your favorite claims to help more people discover them!", "Repost your favorite claims to help more people discover them!": "Repost your favorite claims to help more people discover them!",
"Repost %title%": "Repost %title%", "Repost %title%": "Repost %title%",
"Channel to repost on": "Channel to repost on",
"Advanced": "Advanced", "Advanced": "Advanced",
"community name": "community name", "community name": "community name",
"Change this to repost to a different %lbry_naming_link%.": "Change this to repost to a different %lbry_naming_link%.", "Change this to repost to a different %lbry_naming_link%.": "Change this to repost to a different %lbry_naming_link%.",

View file

@ -5,17 +5,28 @@ import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors
import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user'; import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user';
import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectUnclaimedRewards } from 'redux/selectors/rewards'; import { selectUnclaimedRewards } from 'redux/selectors/rewards';
import { doFetchChannelListMine, SETTINGS } from 'lbry-redux'; import { doFetchChannelListMine, selectMyChannelUrls, SETTINGS } from 'lbry-redux';
import { import {
makeSelectClientSetting, makeSelectClientSetting,
selectLanguage, selectLanguage,
selectLoadedLanguages, selectLoadedLanguages,
selectThemePath, selectThemePath,
} from 'redux/selectors/settings'; } from 'redux/selectors/settings';
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded, selectModal } from 'redux/selectors/app'; import {
selectIsUpgradeAvailable,
selectAutoUpdateDownloaded,
selectModal,
selectActiveChannelId,
} from 'redux/selectors/app';
import { doGetWalletSyncPreference, doSetLanguage } from 'redux/actions/settings'; import { doGetWalletSyncPreference, doSetLanguage } from 'redux/actions/settings';
import { doSyncLoop } from 'redux/actions/sync'; import { doSyncLoop } from 'redux/actions/sync';
import { doDownloadUpgradeRequested, doSignIn, doGetAndPopulatePreferences } from 'redux/actions/app'; import {
doDownloadUpgradeRequested,
doSignIn,
doGetAndPopulatePreferences,
doSetActiveChannel,
doSetIncognito,
} from 'redux/actions/app';
import App from './view'; import App from './view';
const select = state => ({ const select = state => ({
@ -33,6 +44,8 @@ const select = state => ({
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
currentModal: selectModal(state), currentModal: selectModal(state),
syncFatalError: selectSyncFatalError(state), syncFatalError: selectSyncFatalError(state),
activeChannelId: selectActiveChannelId(state),
myChannelUrls: selectMyChannelUrls(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
@ -45,6 +58,8 @@ const perform = dispatch => ({
getWalletSyncPref: () => dispatch(doGetWalletSyncPreference()), getWalletSyncPref: () => dispatch(doGetWalletSyncPreference()),
syncLoop: noInterval => dispatch(doSyncLoop(noInterval)), syncLoop: noInterval => dispatch(doSyncLoop(noInterval)),
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)), setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()),
setIncognito: () => dispatch(doSetIncognito()),
}); });
export default hot(connect(select, perform)(App)); export default hot(connect(select, perform)(App));

View file

@ -80,6 +80,10 @@ type Props = {
syncEnabled: boolean, syncEnabled: boolean,
currentModal: any, currentModal: any,
syncFatalError: boolean, syncFatalError: boolean,
activeChannelId: ?string,
myChannelUrls: ?Array<string>,
setActiveChannelIfNotSet: (?string) => void,
setIncognito: boolean => void,
}; };
function App(props: Props) { function App(props: Props) {
@ -106,6 +110,10 @@ function App(props: Props) {
syncLoop, syncLoop,
currentModal, currentModal,
syncFatalError, syncFatalError,
activeChannelId,
myChannelUrls,
setActiveChannelIfNotSet,
setIncognito,
} = props; } = props;
const appRef = useRef(); const appRef = useRef();
@ -133,7 +141,8 @@ function App(props: Props) {
const shouldHideNag = pathname.startsWith(`/$/${PAGES.EMBED}`) || pathname.startsWith(`/$/${PAGES.AUTH_VERIFY}`); const shouldHideNag = pathname.startsWith(`/$/${PAGES.EMBED}`) || pathname.startsWith(`/$/${PAGES.AUTH_VERIFY}`);
const userId = user && user.id; const userId = user && user.id;
const useCustomScrollbar = !IS_MAC; const useCustomScrollbar = !IS_MAC;
const hasMyChannels = myChannelUrls && myChannelUrls.length > 0;
const hasNoChannels = myChannelUrls && myChannelUrls.length === 0;
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language]; const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
let uri; let uri;
@ -228,6 +237,14 @@ function App(props: Props) {
document.documentElement.setAttribute('theme', theme); document.documentElement.setAttribute('theme', theme);
}, [theme]); }, [theme]);
useEffect(() => {
if (hasMyChannels && !activeChannelId) {
setActiveChannelIfNotSet();
} else if (hasNoChannels) {
setIncognito(true);
}
}, [hasMyChannels, activeChannelId, setActiveChannelIfNotSet]);
useEffect(() => { useEffect(() => {
if (!languages.includes(language)) { if (!languages.includes(language)) {
setLanguage(language); setLanguage(language);

View file

@ -1,9 +1,16 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SelectChannel from './view';
import { selectMyChannelClaims } from 'lbry-redux'; import { selectMyChannelClaims } from 'lbry-redux';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
import SelectChannel from './view';
const select = state => ({ const select = state => ({
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
}); });
export default connect(select)(SelectChannel); export default connect(select, {
doSetActiveChannel,
doSetIncognito,
})(SelectChannel);

View file

@ -1,16 +1,23 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import classnames from 'classnames'; import classnames from 'classnames';
import React from 'react'; import React from 'react';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import ChannelTitle from 'component/channelTitle'; import ChannelTitle from 'component/channelTitle';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import { useHistory } from 'react-router';
type Props = { type Props = {
selectedChannelUrl: string, // currently selected channel selectedChannelUrl: string, // currently selected channel
channels: ?Array<ChannelClaim>, channels: ?Array<ChannelClaim>,
onChannelSelect: (url: string) => void, onChannelSelect: (url: string) => void,
hideAnon?: boolean,
activeChannelClaim: ?ChannelClaim,
doSetActiveChannel: string => void,
incognito: boolean,
doSetIncognito: boolean => void,
}; };
type ListItemProps = { type ListItemProps = {
@ -30,34 +37,64 @@ function ChannelListItem(props: ListItemProps) {
); );
} }
function ChannelSelector(props: Props) { type IncognitoSelectorProps = {
const { channels, selectedChannelUrl, onChannelSelect } = props; isSelected?: boolean,
};
if (!channels || !selectedChannelUrl) { function IncognitoSelector(props: IncognitoSelectorProps) {
return null; return (
<div className={classnames('channel__list-item', { 'channel__list-item--selected': props.isSelected })}>
<Icon sectionIcon icon={ICONS.ANONYMOUS} />
<h2 className="channel__list-text">{__('Anonymous')}</h2>
{props.isSelected && <Icon icon={ICONS.DOWN} />}
</div>
);
}
function ChannelSelector(props: Props) {
const { channels, activeChannelClaim, doSetActiveChannel, hideAnon = false, incognito, doSetIncognito } = props;
const {
push,
location: { pathname },
} = useHistory();
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
function handleChannelSelect(channelClaim) {
doSetIncognito(false);
doSetActiveChannel(channelClaim.claim_id);
} }
return ( return (
<div className="channel__selector">
<Menu> <Menu>
<MenuButton className=""> <MenuButton>
<ChannelListItem uri={selectedChannelUrl} isSelected /> {(incognito && !hideAnon) || !activeChannelUrl ? (
<IncognitoSelector isSelected />
) : (
<ChannelListItem uri={activeChannelUrl} isSelected />
)}
</MenuButton> </MenuButton>
<MenuList className="menu__list channel__list"> <MenuList className="menu__list channel__list">
{channels && {channels &&
channels.map(channel => ( channels.map(channel => (
<MenuItem <MenuItem key={channel.permanent_url} onSelect={() => handleChannelSelect(channel)}>
key={channel.canonical_url} <ChannelListItem uri={channel.permanent_url} />
onSelect={() => {
if (selectedChannelUrl !== channel.canonical_url) {
onChannelSelect(channel.canonical_url);
}
}}
>
<ChannelListItem uri={channel.canonical_url} />
</MenuItem> </MenuItem>
))} ))}
{!hideAnon && (
<MenuItem onSelect={() => doSetIncognito(true)}>
<IncognitoSelector />
</MenuItem>
)}
<MenuItem onSelect={() => push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`)}>
<div className="channel__list-item">
<Icon sectionIcon icon={ICONS.CHANNEL} />
<h2 className="channel__list-text">Create a new channel</h2>
</div>
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
</div>
); );
} }

View file

@ -1,49 +1,28 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { makeSelectThumbnailForUri, selectMyChannelClaims, makeSelectChannelPermUrlForClaimUri } from 'lbry-redux';
doResolveUri,
makeSelectClaimIsPending,
makeSelectClaimForUri,
makeSelectThumbnailForUri,
makeSelectIsUriResolving,
selectMyChannelClaims,
makeSelectMyChannelPermUrlForName,
makeSelectChannelPermUrlForClaimUri,
} from 'lbry-redux';
import { doCommentAbandon, doCommentUpdate, doCommentPin, doCommentList } from 'redux/actions/comments'; import { doCommentAbandon, doCommentUpdate, doCommentPin, doCommentList } from 'redux/actions/comments';
import { doToggleBlockChannel } from 'redux/actions/blocked'; import { doToggleBlockChannel } from 'redux/actions/blocked';
import { selectChannelIsBlocked } from 'redux/selectors/blocked'; import { selectChannelIsBlocked } from 'redux/selectors/blocked';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { doSetPlayingUri } from 'redux/actions/content'; import { doSetPlayingUri } from 'redux/actions/content';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { import { selectIsFetchingComments, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
selectIsFetchingComments, import { selectActiveChannelClaim } from 'redux/selectors/app';
makeSelectOthersReactionsForComment,
selectCommentChannel,
} from 'redux/selectors/comments';
import Comment from './view'; import Comment from './view';
const select = (state, props) => { const select = (state, props) => ({
const channel = selectCommentChannel(state);
return {
activeChannel: channel,
pending: props.authorUri && makeSelectClaimIsPending(props.authorUri)(state),
channel: props.authorUri && makeSelectClaimForUri(props.authorUri)(state),
isResolvingUri: props.authorUri && makeSelectIsUriResolving(props.authorUri)(state),
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state), thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state), channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state),
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
isFetchingComments: selectIsFetchingComments(state), isFetchingComments: selectIsFetchingComments(state),
myChannels: selectMyChannelClaims(state), myChannels: selectMyChannelClaims(state),
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state), othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
commentIdentityChannel: makeSelectMyChannelPermUrlForName(channel)(state), contentChannelPermanentUrl: makeSelectChannelPermUrlForClaimUri(props.uri)(state),
contentChannel: makeSelectChannelPermUrlForClaimUri(props.uri)(state), activeChannelClaim: selectActiveChannelClaim(state),
}; });
};
const perform = dispatch => ({ const perform = dispatch => ({
closeInlinePlayer: () => dispatch(doSetPlayingUri({ uri: null })), closeInlinePlayer: () => dispatch(doSetPlayingUri({ uri: null })),
resolveUri: uri => dispatch(doResolveUri(uri)),
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)), updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
deleteComment: commentId => dispatch(doCommentAbandon(commentId)), deleteComment: commentId => dispatch(doCommentAbandon(commentId)),
blockChannel: channelUri => dispatch(doToggleBlockChannel(channelUri)), blockChannel: channelUri => dispatch(doToggleBlockChannel(channelUri)),

View file

@ -5,7 +5,6 @@ import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config'; import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import { isEmpty } from 'util/object';
import DateTime from 'component/dateTime'; import DateTime from 'component/dateTime';
import Button from 'component/button'; import Button from 'component/button';
import Expandable from 'component/expandable'; import Expandable from 'component/expandable';
@ -29,10 +28,6 @@ type Props = {
commentId: string, // sha256 digest identifying the comment commentId: string, // sha256 digest identifying the comment
message: string, // comment body message: string, // comment body
timePosted: number, // Comment timestamp timePosted: number, // Comment timestamp
channel: ?Claim, // Channel Claim, retrieved to obtain thumbnail
pending?: boolean,
resolveUri: string => void, // resolves the URI
isResolvingUri: boolean, // if the URI is currently being resolved
channelIsBlocked: boolean, // if the channel is blacklisted in the app channelIsBlocked: boolean, // if the channel is blacklisted in the app
claimIsMine: boolean, // if you control the claim which this comment was posted on claimIsMine: boolean, // if you control the claim which this comment was posted on
commentIsMine: boolean, // if this comment was signed by an owned channel commentIsMine: boolean, // if this comment was signed by an owned channel
@ -53,7 +48,8 @@ type Props = {
pinComment: (string, boolean) => Promise<any>, pinComment: (string, boolean) => Promise<any>,
fetchComments: string => void, fetchComments: string => void,
commentIdentityChannel: any, commentIdentityChannel: any,
contentChannel: any, contentChannelPermanentUrl: any,
activeChannelClaim: ?ChannelClaim,
}; };
const LENGTH_TO_COLLAPSE = 300; const LENGTH_TO_COLLAPSE = 300;
@ -67,10 +63,6 @@ function Comment(props: Props) {
authorUri, authorUri,
timePosted, timePosted,
message, message,
pending,
channel,
isResolvingUri,
resolveUri,
channelIsBlocked, channelIsBlocked,
commentIsMine, commentIsMine,
commentId, commentId,
@ -87,8 +79,8 @@ function Comment(props: Props) {
pinComment, pinComment,
fetchComments, fetchComments,
othersReacts, othersReacts,
commentIdentityChannel, contentChannelPermanentUrl,
contentChannel, activeChannelClaim,
} = props; } = props;
const { const {
push, push,
@ -108,10 +100,7 @@ function Comment(props: Props) {
const dislikesCount = (othersReacts && othersReacts.dislike) || 0; const dislikesCount = (othersReacts && othersReacts.dislike) || 0;
const totalLikesAndDislikes = likesCount + dislikesCount; const totalLikesAndDislikes = likesCount + dislikesCount;
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8; const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
// to debounce subsequent requests
const shouldFetch =
channel === undefined ||
(channel !== null && channel.value_type === 'channel' && isEmpty(channel.meta) && !pending);
let channelOwnerOfContent; let channelOwnerOfContent;
try { try {
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
@ -121,11 +110,6 @@ function Comment(props: Props) {
} catch (e) {} } catch (e) {}
useEffect(() => { useEffect(() => {
// If author was extracted from the URI, then it must be valid.
if (authorUri && author && !isResolvingUri && shouldFetch) {
resolveUri(authorUri);
}
if (isEditing) { if (isEditing) {
setCharCount(editedMessage.length); setCharCount(editedMessage.length);
@ -143,7 +127,7 @@ function Comment(props: Props) {
window.removeEventListener('keydown', handleEscape); window.removeEventListener('keydown', handleEscape);
}; };
} }
}, [isResolvingUri, shouldFetch, author, authorUri, resolveUri, editedMessage, isEditing, setEditing]); }, [author, authorUri, editedMessage, isEditing, setEditing]);
function handleEditMessageChanged(event) { function handleEditMessageChanged(event) {
setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value); setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value);
@ -262,7 +246,7 @@ function Comment(props: Props) {
{__('Block Channel')} {__('Block Channel')}
</MenuItem> </MenuItem>
)} )}
{commentIdentityChannel === contentChannel && isTopLevel && ( {activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl && isTopLevel && (
<MenuItem <MenuItem
className="comment__menu-option menu__link" className="comment__menu-option menu__link"
onSelect={ onSelect={

View file

@ -1,9 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimForUri, selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; import { makeSelectClaimForUri, selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux';
import { selectIsPostingComment, selectCommentChannel } from 'redux/selectors/comments'; import { selectIsPostingComment } from 'redux/selectors/comments';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
import { doCommentCreate, doSetCommentChannel } from 'redux/actions/comments'; import { doCommentCreate } from 'redux/actions/comments';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { CommentCreate } from './view'; import { CommentCreate } from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -12,14 +13,13 @@ const select = (state, props) => ({
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),
isFetchingChannels: selectFetchingMyChannels(state), isFetchingChannels: selectFetchingMyChannels(state),
isPostingComment: selectIsPostingComment(state), isPostingComment: selectIsPostingComment(state),
activeChannel: selectCommentChannel(state), activeChannelClaim: selectActiveChannelClaim(state),
}); });
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({
createComment: (comment, claimId, channel, parentId) => createComment: (comment, claimId, parentId) => dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri)),
dispatch(doCommentCreate(comment, claimId, channel, parentId, ownProps.uri)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
setCommentChannel: name => dispatch(doSetCommentChannel(name)), setActiveChannel: claimId => dispatch(doSetActiveChannel(claimId)),
}); });
export default connect(select, perform)(CommentCreate); export default connect(select, perform)(CommentCreate);

View file

@ -1,12 +1,11 @@
// @flow // @flow
import { SIMPLE_SITE } from 'config'; import { SIMPLE_SITE } from 'config';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import { CHANNEL_NEW } from 'constants/claim';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import ChannelSelection from 'component/selectChannel'; import SelectChannel from 'component/selectChannel';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
@ -25,7 +24,7 @@ type Props = {
isReply: boolean, isReply: boolean,
isPostingComment: boolean, isPostingComment: boolean,
activeChannel: string, activeChannel: string,
setCommentChannel: string => void, activeChannelClaim: ?ChannelClaim,
}; };
export function CommentCreate(props: Props) { export function CommentCreate(props: Props) {
@ -40,8 +39,7 @@ export function CommentCreate(props: Props) {
isReply, isReply,
parentId, parentId,
isPostingComment, isPostingComment,
activeChannel, activeChannelClaim,
setCommentChannel,
} = props; } = props;
const buttonref: ElementRef<any> = React.useRef(); const buttonref: ElementRef<any> = React.useRef();
const { push } = useHistory(); const { push } = useHistory();
@ -50,21 +48,7 @@ export function CommentCreate(props: Props) {
const [charCount, setCharCount] = useState(commentValue.length); const [charCount, setCharCount] = useState(commentValue.length);
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false); const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
const hasChannels = channels && channels.length; const hasChannels = channels && channels.length;
const disabled = isPostingComment || activeChannel === CHANNEL_NEW || !commentValue.length; const disabled = isPostingComment || !activeChannelClaim || !commentValue.length;
const topChannel =
channels &&
channels.reduce((top, channel) => {
const topClaimCount = (top && top.meta && top.meta.claims_in_channel) || 0;
const currentClaimCount = (activeChannel && channel.meta && channel.meta.claims_in_channel) || 0;
return topClaimCount >= currentClaimCount ? top : channel;
});
useEffect(() => {
// set default channel
if ((activeChannel === '' || activeChannel === 'anonymous') && topChannel) {
setCommentChannel(topChannel.name);
}
}, [activeChannel, topChannel, setCommentChannel]);
function handleCommentChange(event) { function handleCommentChange(event) {
let commentValue; let commentValue;
@ -94,8 +78,8 @@ export function CommentCreate(props: Props) {
} }
function handleSubmit() { function handleSubmit() {
if (activeChannel !== CHANNEL_NEW && commentValue.length) { if (activeChannelClaim && commentValue.length) {
createComment(commentValue, claimId, activeChannel, parentId).then(res => { createComment(commentValue, claimId, parentId).then(res => {
if (res && res.signature) { if (res && res.signature) {
setCommentValue(''); setCommentValue('');
@ -138,13 +122,13 @@ export function CommentCreate(props: Props) {
})} })}
> >
<FormField <FormField
disabled={activeChannel === CHANNEL_NEW} disabled={!activeChannelClaim}
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'} type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
name={isReply ? 'content_reply' : 'content_description'} name={isReply ? 'content_reply' : 'content_description'}
label={ label={
<span className="comment-new__label-wrapper"> <span className="comment-new__label-wrapper">
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div> <div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
<ChannelSelection channel={activeChannel} hideAnon tiny hideNew onChannelChange={setCommentChannel} /> <SelectChannel tiny />
</span> </span>
} }
quickActionLabel={ quickActionLabel={

View file

@ -2,19 +2,16 @@ import { connect } from 'react-redux';
import Comment from './view'; import Comment from './view';
import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux'; import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
makeSelectMyReactionsForComment,
makeSelectOthersReactionsForComment,
selectCommentChannel,
} from 'redux/selectors/comments';
import { doCommentReact } from 'redux/actions/comments'; import { doCommentReact } from 'redux/actions/comments';
import { selectActiveChannelId } from 'redux/selectors/app';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
myReacts: makeSelectMyReactionsForComment(props.commentId)(state), myReacts: makeSelectMyReactionsForComment(props.commentId)(state),
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state), othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
activeChannel: selectCommentChannel(state), activeChannelId: selectActiveChannelId(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -16,13 +16,13 @@ type Props = {
commentId: string, commentId: string,
pendingCommentReacts: Array<string>, pendingCommentReacts: Array<string>,
claimIsMine: boolean, claimIsMine: boolean,
activeChannel: string, activeChannelId: ?string,
claim: ?ChannelClaim, claim: ?ChannelClaim,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
}; };
export default function CommentReactions(props: Props) { export default function CommentReactions(props: Props) {
const { myReacts, othersReacts, commentId, react, claimIsMine, claim, activeChannel, doToast } = props; const { myReacts, othersReacts, commentId, react, claimIsMine, claim, activeChannelId, doToast } = props;
const { const {
push, push,
location: { pathname }, location: { pathname },
@ -31,8 +31,8 @@ export default function CommentReactions(props: Props) {
claim && claim &&
claimIsMine && claimIsMine &&
(claim.value_type === 'channel' (claim.value_type === 'channel'
? claim.name === activeChannel ? claim.claim_id === activeChannelId
: claim.signing_channel && claim.signing_channel.name === activeChannel); : claim.signing_channel && claim.signing_channel.claim_id === activeChannelId);
const authorUri = const authorUri =
claim && claim.value_type === 'channel' claim && claim.value_type === 'channel'
? claim.canonical_url ? claim.canonical_url
@ -52,7 +52,7 @@ export default function CommentReactions(props: Props) {
const creatorLiked = getCountForReact(REACTION_TYPES.CREATOR_LIKE) > 0; const creatorLiked = getCountForReact(REACTION_TYPES.CREATOR_LIKE) > 0;
function handleCommentLike() { function handleCommentLike() {
if (activeChannel) { if (activeChannelId) {
react(commentId, REACTION_TYPES.LIKE); react(commentId, REACTION_TYPES.LIKE);
} else { } else {
promptForChannel(); promptForChannel();
@ -60,7 +60,7 @@ export default function CommentReactions(props: Props) {
} }
function handleCommentDislike() { function handleCommentDislike() {
if (activeChannel) { if (activeChannelId) {
react(commentId, REACTION_TYPES.DISLIKE); react(commentId, REACTION_TYPES.DISLIKE);
} else { } else {
promptForChannel(); promptForChannel();

View file

@ -5,10 +5,10 @@ import {
selectIsFetchingComments, selectIsFetchingComments,
makeSelectTotalCommentsCountForUri, makeSelectTotalCommentsCountForUri,
selectOthersReactsById, selectOthersReactsById,
selectCommentChannel,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { doCommentList, doCommentReactList } from 'redux/actions/comments'; import { doCommentList, doCommentReactList } from 'redux/actions/comments';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelId } from 'redux/selectors/app';
import CommentsList from './view'; import CommentsList from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -20,7 +20,7 @@ const select = (state, props) => ({
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
reactionsById: selectOthersReactsById(state), reactionsById: selectOthersReactsById(state),
activeChannel: selectCommentChannel(state), activeChannelId: selectActiveChannelId(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -26,7 +26,7 @@ type Props = {
totalComments: number, totalComments: number,
fetchingChannels: boolean, fetchingChannels: boolean,
reactionsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } }, reactionsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
activeChannel: string, activeChannelId: ?string,
}; };
function CommentList(props: Props) { function CommentList(props: Props) {
@ -42,7 +42,7 @@ function CommentList(props: Props) {
totalComments, totalComments,
fetchingChannels, fetchingChannels,
reactionsById, reactionsById,
activeChannel, activeChannelId,
} = props; } = props;
const commentRef = React.useRef(); const commentRef = React.useRef();
const spinnerRef = React.useRef(); const spinnerRef = React.useRef();
@ -90,7 +90,7 @@ function CommentList(props: Props) {
}) })
.catch(() => setReadyToDisplayComments(true)); .catch(() => setReadyToDisplayComments(true));
} }
}, [fetchReacts, uri, totalComments, activeChannel, fetchingChannels]); }, [fetchReacts, uri, totalComments, activeChannelId, fetchingChannels]);
useEffect(() => { useEffect(() => {
if (readyToDisplayComments && linkedCommentId && commentRef && commentRef.current) { if (readyToDisplayComments && linkedCommentId && commentRef && commentRef.current) {

View file

@ -1015,4 +1015,9 @@ export const icons = {
<path d="m 18.089706,2.6673324 c -0.458672,0 -0.914415,0.081053 -1.342833,0.2381801 -0.726068,-1.5175206 -2.625165,-2.67785413 -4.515474,-2.67785413 -1.902023,0 -3.8128297,1.16033353 -4.5408481,2.67785413 C 7.2621303,2.7483855 6.8063878,2.6673324 6.348691,2.6673324 c -2.1528256,0 -3.9045598,1.7507491 -3.9045598,3.9035835 0,2.0230385 1.4648199,3.6410591 3.4146614,3.8752841 v 8.262918 h 2.9276892 v -3.415632 c 0.00968,-0.26944 0.2273915,-0.487951 0.4977084,-0.487951 0.2693563,0 0.4879486,0.218539 0.4879486,0.487951 v 3.415632 h 1.9420352 v -4.391535 c 0,-0.269439 0.217626,-0.487951 0.487948,-0.487951 0.269357,0 0.487946,0.218539 0.487946,0.487951 v 4.391535 h 1.951795 v -3.415632 c 0.01964,-0.26944 0.238125,-0.487951 0.507465,-0.487951 0.270325,0 0.487949,0.218539 0.468432,0.487951 v 3.415632 h 2.927689 V 10.4462 c 1.980095,-0.234307 3.445891,-1.8522456 3.445891,-3.8752841 0,-2.1528344 -1.750758,-3.9035835 -3.901634,-3.9035835" /> <path d="m 18.089706,2.6673324 c -0.458672,0 -0.914415,0.081053 -1.342833,0.2381801 -0.726068,-1.5175206 -2.625165,-2.67785413 -4.515474,-2.67785413 -1.902023,0 -3.8128297,1.16033353 -4.5408481,2.67785413 C 7.2621303,2.7483855 6.8063878,2.6673324 6.348691,2.6673324 c -2.1528256,0 -3.9045598,1.7507491 -3.9045598,3.9035835 0,2.0230385 1.4648199,3.6410591 3.4146614,3.8752841 v 8.262918 h 2.9276892 v -3.415632 c 0.00968,-0.26944 0.2273915,-0.487951 0.4977084,-0.487951 0.2693563,0 0.4879486,0.218539 0.4879486,0.487951 v 3.415632 h 1.9420352 v -4.391535 c 0,-0.269439 0.217626,-0.487951 0.487948,-0.487951 0.269357,0 0.487946,0.218539 0.487946,0.487951 v 4.391535 h 1.951795 v -3.415632 c 0.01964,-0.26944 0.238125,-0.487951 0.507465,-0.487951 0.270325,0 0.487949,0.218539 0.468432,0.487951 v 3.415632 h 2.927689 V 10.4462 c 1.980095,-0.234307 3.445891,-1.8522456 3.445891,-3.8752841 0,-2.1528344 -1.750758,-3.9035835 -3.901634,-3.9035835" />
</svg> </svg>
), ),
[ICONS.ANONYMOUS]: buildIcon(
<g>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</g>
),
}; };

View file

@ -1,13 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimForUri, doPrepareEdit } from 'lbry-redux'; import { makeSelectClaimForUri } from 'lbry-redux';
import CreatorAnalytics from './view'; import CreatorAnalytics from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
}); });
const perform = dispatch => ({ export default connect(select)(CreatorAnalytics);
prepareEdit: channelName => dispatch(doPrepareEdit({ signing_channel: { name: channelName } })),
});
export default connect(select, perform)(CreatorAnalytics);

View file

@ -26,7 +26,7 @@ export default function CreatorAnalytics(props: Props) {
const history = useHistory(); const history = useHistory();
const [stats, setStats] = React.useState(); const [stats, setStats] = React.useState();
const [error, setError] = React.useState(); const [error, setError] = React.useState();
const [fetchingStats, setFetchingStats] = React.useState(true); const [fetchingStats, setFetchingStats] = React.useState(false);
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const channelHasClaims = claim && claim.meta && claim.meta.claims_in_channel && claim.meta.claims_in_channel > 0; const channelHasClaims = claim && claim.meta && claim.meta.claims_in_channel && claim.meta.claims_in_channel > 0;
@ -81,7 +81,24 @@ export default function CreatorAnalytics(props: Props) {
/> />
)} )}
{!error && ( {!error && !channelHasClaims ? (
<Yrbl
type="sad"
title={__("You haven't uploaded anything")}
subtitle={__('Upload something to start tracking your stats!')}
actions={
<div className="section__actions">
<Button
button="primary"
label={__('Upload Something')}
onClick={() => {
history.push(`/$/${PAGES.UPLOAD}`);
}}
/>
</div>
}
/>
) : (
<Yrbl <Yrbl
title={ title={
channelHasClaims channelHasClaims
@ -93,12 +110,7 @@ export default function CreatorAnalytics(props: Props) {
<Button <Button
button="primary" button="primary"
label={__('Upload Something')} label={__('Upload Something')}
onClick={() => { onClick={() => history.push(`/$/${PAGES.UPLOAD}`)}
if (claim) {
prepareEdit(claim.name);
history.push(`/$/${PAGES.UPLOAD}`);
}
}}
/> />
</div> </div>
} }

View file

@ -1,15 +1,14 @@
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectTotalBalance, selectBalance, formatCredits, selectMyChannelClaims, SETTINGS } from 'lbry-redux'; import { selectTotalBalance, selectBalance, formatCredits, SETTINGS } from 'lbry-redux';
import { selectGetSyncErrorMessage } from 'redux/selectors/sync'; import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
import { selectUserVerifiedEmail, selectUserEmail, selectEmailToVerify, selectUser } from 'redux/selectors/user'; import { selectUserVerifiedEmail, selectUserEmail, selectEmailToVerify, selectUser } from 'redux/selectors/user';
import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user'; import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { doSignOut, doOpenModal } from 'redux/actions/app'; import { doSignOut, doOpenModal } from 'redux/actions/app';
import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings';
import { selectCommentChannel } from 'redux/selectors/comments'; import { selectHasNavigated, selectActiveChannelClaim } from 'redux/selectors/app';
import Header from './view'; import Header from './view';
import { selectHasNavigated } from 'redux/selectors/app';
const select = state => ({ const select = state => ({
language: selectLanguage(state), language: selectLanguage(state),
@ -25,8 +24,7 @@ const select = state => ({
emailToVerify: selectEmailToVerify(state), emailToVerify: selectEmailToVerify(state),
hasNavigated: selectHasNavigated(state), hasNavigated: selectHasNavigated(state),
user: selectUser(state), user: selectUser(state),
myChannels: selectMyChannelClaims(state), activeChannelClaim: selectActiveChannelClaim(state),
commentChannel: selectCommentChannel(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -61,8 +61,7 @@ type Props = {
setSidebarOpen: boolean => void, setSidebarOpen: boolean => void,
isAbsoluteSideNavHidden: boolean, isAbsoluteSideNavHidden: boolean,
hideCancel: boolean, hideCancel: boolean,
myChannels: ?Array<ChannelClaim>, activeChannelClaim: ?ChannelClaim,
commentChannel: string,
}; };
const Header = (props: Props) => { const Header = (props: Props) => {
@ -90,8 +89,7 @@ const Header = (props: Props) => {
isAbsoluteSideNavHidden, isAbsoluteSideNavHidden,
user, user,
hideCancel, hideCancel,
myChannels, activeChannelClaim,
commentChannel,
} = props; } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent // on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
@ -102,18 +100,8 @@ const Header = (props: Props) => {
const hasBackout = Boolean(backout); const hasBackout = Boolean(backout);
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {}; const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
const notificationsEnabled = (user && user.experimental_ui) || false; const notificationsEnabled = (user && user.experimental_ui) || false;
let channelUrl; const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
let identityChannel;
if (myChannels && myChannels.length >= 1) {
if (myChannels.length === 1) {
identityChannel = myChannels[0];
} else if (commentChannel) {
identityChannel = myChannels.find(chan => chan.name === commentChannel);
} else {
identityChannel = myChannels[0];
}
channelUrl = identityChannel && (identityChannel.permanent_url || identityChannel.canonical_url);
}
// Sign out if they click the "x" when they are on the password prompt // Sign out if they click the "x" when they are on the password prompt
const authHeaderAction = syncError ? { onClick: signOut } : { navigate: '/' }; const authHeaderAction = syncError ? { onClick: signOut } : { navigate: '/' };
const homeButtonNavigationProps = isVerifyPage ? {} : authHeader ? authHeaderAction : { navigate: '/' }; const homeButtonNavigationProps = isVerifyPage ? {} : authHeader ? authHeaderAction : { navigate: '/' };
@ -288,7 +276,7 @@ const Header = (props: Props) => {
history={history} history={history}
handleThemeToggle={handleThemeToggle} handleThemeToggle={handleThemeToggle}
currentTheme={currentTheme} currentTheme={currentTheme}
channelUrl={channelUrl} activeChannelUrl={activeChannelUrl}
openSignOutModal={openSignOutModal} openSignOutModal={openSignOutModal}
email={email} email={email}
signOut={signOut} signOut={signOut}
@ -341,7 +329,7 @@ type HeaderMenuButtonProps = {
history: { push: string => void }, history: { push: string => void },
handleThemeToggle: string => void, handleThemeToggle: string => void,
currentTheme: string, currentTheme: string,
channelUrl: ?string, activeChannelUrl: ?string,
openSignOutModal: () => void, openSignOutModal: () => void,
email: ?string, email: ?string,
signOut: () => void, signOut: () => void,
@ -354,7 +342,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
history, history,
handleThemeToggle, handleThemeToggle,
currentTheme, currentTheme,
channelUrl, activeChannelUrl,
openSignOutModal, openSignOutModal,
email, email,
signOut, signOut,
@ -427,8 +415,8 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
aria-label={__('Your account')} aria-label={__('Your account')}
title={__('Your account')} title={__('Your account')}
className={classnames('header__navigation-item mobile-hidden', { className={classnames('header__navigation-item mobile-hidden', {
'menu__title header__navigation-item--icon': !channelUrl, 'menu__title header__navigation-item--icon': !activeChannelUrl,
'header__navigation-item--profile-pic': channelUrl, 'header__navigation-item--profile-pic': activeChannelUrl,
})} })}
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={e => {
@ -436,7 +424,11 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
}} }}
// @endif // @endif
> >
{channelUrl ? <ChannelThumbnail uri={channelUrl} /> : <Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />} {activeChannelUrl ? (
<ChannelThumbnail uri={activeChannelUrl} />
) : (
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
)}
</MenuButton> </MenuButton>
<MenuList className="menu__list--header"> <MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOADS}`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOADS}`)}>

View file

@ -5,7 +5,6 @@ import Button from 'component/button';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import CopyableText from 'component/copyableText'; import CopyableText from 'component/copyableText';
import Card from 'component/common/card'; import Card from 'component/common/card';
import SelectChannel from 'component/selectChannel';
import analytics from 'analytics'; import analytics from 'analytics';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import LbcSymbol from 'component/common/lbc-symbol'; import LbcSymbol from 'component/common/lbc-symbol';
@ -21,7 +20,6 @@ type Props = {
function InviteNew(props: Props) { function InviteNew(props: Props) {
const { inviteNew, errorMessage, isPending, referralCode = '', channels } = props; const { inviteNew, errorMessage, isPending, referralCode = '', channels } = props;
const noChannels = !channels || !(channels.length > 0);
// Email // Email
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@ -87,14 +85,20 @@ function InviteNew(props: Props) {
actions={ actions={
<React.Fragment> <React.Fragment>
<CopyableText label={__('Your invite link')} copyable={referral} /> <CopyableText label={__('Your invite link')} copyable={referral} />
{!noChannels && ( {channels && channels.length > 0 && (
<SelectChannel <FormField
channel={referralSource} type="select"
onChannelChange={channel => handleReferralChange(channel)}
label={__('Customize link')} label={__('Customize link')}
hideAnon value={referralSource}
injected={[referralCode]} onChange={e => handleReferralChange(e.target.value)}
/> >
{channels.map(channel => (
<option key={channel.claim_id} value={channel.name}>
{channel.name}
</option>
))}
<option value={referralCode}>{referralCode}</option>
</FormField>
)} )}
</React.Fragment> </React.Fragment>
} }

View file

@ -0,0 +1,38 @@
// @flow
type Props = {
uri: ?string,
isResolvingUri: boolean,
amountNeededForTakeover: number,
};
function BidHelpText(props: Props) {
const { uri, isResolvingUri, amountNeededForTakeover } = props;
let bidHelpText;
if (uri) {
if (isResolvingUri) {
bidHelpText = __('Checking the winning claim amount...');
} else if (amountNeededForTakeover === 0) {
bidHelpText = __('You currently have the highest bid for this name.');
} else if (!amountNeededForTakeover) {
bidHelpText = __(
'Any amount will give you the highest bid, but larger amounts help your content be trusted and discovered.'
);
} else {
bidHelpText = __(
'If you bid more than %amount% LBRY Credits, when someone navigates to %uri%, it will load your published content. However, you can get a longer version of this URL for any bid.',
{
amount: amountNeededForTakeover,
uri: uri,
}
);
}
} else {
bidHelpText = __('These LBRY Credits remain yours and the deposit can be undone at any time.');
}
return bidHelpText;
}
export default BidHelpText;

View file

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import {
makeSelectPublishFormValue,
selectMyClaimForUri,
selectIsResolvingPublishUris,
doUpdatePublishForm,
doPrepareEdit,
selectBalance,
} from 'lbry-redux';
import PublishPage from './view';
const select = state => ({
name: makeSelectPublishFormValue('name')(state),
bid: makeSelectPublishFormValue('bid')(state),
uri: makeSelectPublishFormValue('uri')(state),
isResolvingUri: selectIsResolvingPublishUris(state),
balance: selectBalance(state),
myClaimForUri: selectMyClaimForUri(state),
});
const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
});
export default connect(select, perform)(PublishPage);

View file

@ -0,0 +1,77 @@
// @flow
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
import React, { useState, useEffect } from 'react';
import { FormField } from 'component/common/form';
import BidHelpText from './bid-help-text';
import Card from 'component/common/card';
import LbcSymbol from 'component/common/lbc-symbol';
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
type Props = {
name: string,
bid: number,
balance: number,
myClaimForUri: ?StreamClaim,
isResolvingUri: boolean,
amountNeededForTakeover: number,
updatePublishForm: ({}) => void,
};
function PublishName(props: Props) {
const { name, myClaimForUri, bid, isResolvingUri, amountNeededForTakeover, updatePublishForm, balance } = props;
const [bidError, setBidError] = useState(undefined);
const previousBidAmount = myClaimForUri && Number(myClaimForUri.amount);
useEffect(() => {
const totalAvailableBidAmount = previousBidAmount ? previousBidAmount + balance : balance;
let bidError;
if (bid === 0) {
bidError = __('Deposit cannot be 0');
} else if (bid < MINIMUM_PUBLISH_BID) {
bidError = __('Your deposit must be higher');
} else if (totalAvailableBidAmount < bid) {
bidError = __('Deposit cannot be higher than your available balance: %balance%', {
balance: totalAvailableBidAmount,
});
} else if (totalAvailableBidAmount <= bid + 0.05) {
bidError = __('Please decrease your deposit to account for transaction fees or acquire more LBRY Credits.');
}
setBidError(bidError);
updatePublishForm({ bidError: bidError });
}, [bid, previousBidAmount, balance, updatePublishForm]);
return (
<Card
actions={
<FormField
type="number"
name="content_bid"
min="0"
step="any"
placeholder="0.123"
className="form-field--price-amount"
label={<LbcSymbol postfix={__('Deposit')} size={12} />}
value={bid}
error={bidError}
disabled={!name}
onChange={event => updatePublishForm({ bid: parseFloat(event.target.value) })}
onWheel={e => e.stopPropagation()}
helper={
<>
<BidHelpText
uri={'lbry://' + name}
amountNeededForTakeover={amountNeededForTakeover}
isResolvingUri={isResolvingUri}
/>
<WalletSpendableBalanceHelp inline />
</>
}
/>
}
/>
);
}
export default PublishName;

View file

@ -13,6 +13,7 @@ import Spinner from 'component/spinner';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import * as PUBLISH_MODES from 'constants/publish_types'; import * as PUBLISH_MODES from 'constants/publish_types';
import PublishName from 'component/publishName';
type Props = { type Props = {
uri: ?string, uri: ?string,
@ -34,10 +35,8 @@ type Props = {
size: number, size: number,
duration: number, duration: number,
isVid: boolean, isVid: boolean,
autoPopulateName: boolean,
setPublishMode: string => void, setPublishMode: string => void,
setPrevFileText: string => void, setPrevFileText: string => void,
setAutoPopulateName: boolean => void,
header: Node, header: Node,
}; };
@ -63,8 +62,6 @@ function PublishFile(props: Props) {
isVid, isVid,
setPublishMode, setPublishMode,
setPrevFileText, setPrevFileText,
autoPopulateName,
setAutoPopulateName,
header, header,
} = props; } = props;
@ -231,10 +228,6 @@ function PublishFile(props: Props) {
const title = event.target.value; const title = event.target.value;
// Update title // Update title
updatePublishForm({ title }); updatePublishForm({ title });
// Auto populate name from title
if (autoPopulateName) {
updatePublishForm({ name: parseName(title) });
}
} }
function handleFileReaderLoaded(event: ProgressEvent) { function handleFileReaderLoaded(event: ProgressEvent) {
@ -327,11 +320,6 @@ function PublishFile(props: Props) {
publishFormParams.name = parseName(fileName); publishFormParams.name = parseName(fileName);
} }
// Prevent name autopopulation from title
if (autoPopulateName) {
setAutoPopulateName(false);
}
// File path is not supported on web for security reasons so we use the name instead. // File path is not supported on web for security reasons so we use the name instead.
setCurrentFile(file.path || file.name); setCurrentFile(file.path || file.name);
updatePublishForm(publishFormParams); updatePublishForm(publishFormParams);
@ -357,6 +345,7 @@ function PublishFile(props: Props) {
subtitle={isStillEditing && __('You are currently editing your upload.')} subtitle={isStillEditing && __('You are currently editing your upload.')}
actions={ actions={
<React.Fragment> <React.Fragment>
<PublishName />
<FormField <FormField
type="text" type="text"
name="content_title" name="content_title"

View file

@ -15,7 +15,7 @@ import {
} from 'lbry-redux'; } from 'lbry-redux';
import { doPublishDesktop } from 'redux/actions/publish'; import { doPublishDesktop } from 'redux/actions/publish';
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards'; import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
import { selectModal } from 'redux/selectors/app'; import { selectModal, selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import PublishPage from './view'; import PublishPage from './view';
@ -32,6 +32,8 @@ const select = state => ({
totalRewardValue: selectUnclaimedRewardValue(state), totalRewardValue: selectUnclaimedRewardValue(state),
modal: selectModal(state), modal: selectModal(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state), enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -9,17 +9,16 @@
*/ */
import { SITE_NAME } from 'config'; import { SITE_NAME } from 'config';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim'; import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux'; import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
import Button from 'component/button'; import Button from 'component/button';
import SelectChannel from 'component/selectChannel'; import ChannelSelect from 'component/channelSelector';
import classnames from 'classnames'; import classnames from 'classnames';
import TagsSelect from 'component/tagsSelect'; import TagsSelect from 'component/tagsSelect';
import PublishDescription from 'component/publishDescription'; import PublishDescription from 'component/publishDescription';
import PublishPrice from 'component/publishPrice'; import PublishPrice from 'component/publishPrice';
import PublishFile from 'component/publishFile'; import PublishFile from 'component/publishFile';
import PublishName from 'component/publishName'; import PublishBid from 'component/publishBid';
import PublishAdditionalOptions from 'component/publishAdditionalOptions'; import PublishAdditionalOptions from 'component/publishAdditionalOptions';
import PublishFormErrors from 'component/publishFormErrors'; import PublishFormErrors from 'component/publishFormErrors';
import SelectThumbnail from 'component/selectThumbnail'; import SelectThumbnail from 'component/selectThumbnail';
@ -60,7 +59,6 @@ type Props = {
amount: string, amount: string,
currency: string, currency: string,
}, },
channel: string,
name: ?string, name: ?string,
nameError: ?string, nameError: ?string,
isResolvingUri: boolean, isResolvingUri: boolean,
@ -85,6 +83,8 @@ type Props = {
ytSignupPending: boolean, ytSignupPending: boolean,
modal: { id: string, modalProps: {} }, modal: { id: string, modalProps: {} },
enablePublishPreview: boolean, enablePublishPreview: boolean,
activeChannelClaim: ?ChannelClaim,
incognito: boolean,
}; };
function PublishForm(props: Props) { function PublishForm(props: Props) {
@ -101,7 +101,6 @@ function PublishForm(props: Props) {
const { const {
thumbnail, thumbnail,
name, name,
channel,
editingURI, editingURI,
myClaimForUri, myClaimForUri,
resolveUri, resolveUri,
@ -123,17 +122,16 @@ function PublishForm(props: Props) {
ytSignupPending, ytSignupPending,
modal, modal,
enablePublishPreview, enablePublishPreview,
activeChannelClaim,
incognito,
} = props; } = props;
// Used to check if name should be auto-populated from title
const [autoPopulateNameFromTitle, setAutoPopulateNameFromTitle] = useState(!isStillEditing);
const TAGS_LIMIT = 5; const TAGS_LIMIT = 5;
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath; const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath;
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === ''); const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing; const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
const isInProgress = filePath || editingURI || name || title; const isInProgress = filePath || editingURI || name || title;
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
// Editing content info // Editing content info
const uri = myClaimForUri ? myClaimForUri.permanent_url : undefined; const uri = myClaimForUri ? myClaimForUri.permanent_url : undefined;
const fileMimeType = const fileMimeType =
@ -204,16 +202,13 @@ function PublishForm(props: Props) {
// Every time the channel or name changes, resolve the uris to find winning bid amounts // Every time the channel or name changes, resolve the uris to find winning bid amounts
useEffect(() => { useEffect(() => {
// If they are midway through a channel creation, treat it as anonymous until it completes
const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel;
// We are only going to store the full uri, but we need to resolve the uri with and without the channel name // We are only going to store the full uri, but we need to resolve the uri with and without the channel name
let uri; let uri;
try { try {
uri = name && buildURI({ streamName: name, channelName }); uri = name && buildURI({ streamName: name, activeChannelName });
} catch (e) {} } catch (e) {}
if (channelName && name) { if (activeChannelName && name) {
// resolve without the channel name so we know the winning bid for it // resolve without the channel name so we know the winning bid for it
try { try {
const uriLessChannel = buildURI({ streamName: name }); const uriLessChannel = buildURI({ streamName: name });
@ -227,15 +222,17 @@ function PublishForm(props: Props) {
checkAvailability(name); checkAvailability(name);
updatePublishForm({ uri }); updatePublishForm({ uri });
} }
}, [name, channel, resolveUri, updatePublishForm, checkAvailability]); }, [name, activeChannelName, resolveUri, updatePublishForm, checkAvailability]);
useEffect(() => { useEffect(() => {
updatePublishForm({ isMarkdownPost: mode === PUBLISH_MODES.POST }); updatePublishForm({ isMarkdownPost: mode === PUBLISH_MODES.POST });
}, [mode, updatePublishForm]); }, [mode, updatePublishForm]);
function handleChannelNameChange(channel) { useEffect(() => {
updatePublishForm({ channel }); if (activeChannelName) {
updatePublishForm({ channel: undefined });
} }
}, [activeChannelName, incognito, updatePublishForm]);
// @if TARGET='web' // @if TARGET='web'
function createWebFile() { function createWebFile() {
@ -331,18 +328,7 @@ function PublishForm(props: Props) {
// Editing claim uri // Editing claim uri
return ( return (
<div className="card-stack"> <div className="card-stack">
<Card <ChannelSelect disabled={disabled} />
className={disabled ? 'card--disabled' : undefined}
actions={
<React.Fragment>
<SelectChannel channel={channel} onChannelChange={handleChannelNameChange} />
<p className="help">
{__('This is a username or handle that your content can be found under.')}{' '}
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}
</p>
</React.Fragment>
}
/>
<PublishFile <PublishFile
uri={uri} uri={uri}
@ -352,8 +338,6 @@ function PublishForm(props: Props) {
inProgress={isInProgress} inProgress={isInProgress}
setPublishMode={setMode} setPublishMode={setMode}
setPrevFileText={setPrevFileText} setPrevFileText={setPrevFileText}
autoPopulateName={autoPopulateNameFromTitle}
setAutoPopulateName={setAutoPopulateNameFromTitle}
header={ header={
<> <>
{MODES.map((modeName, index) => ( {MODES.map((modeName, index) => (
@ -371,6 +355,7 @@ function PublishForm(props: Props) {
</> </>
} }
/> />
{!publishing && ( {!publishing && (
<div className={classnames({ 'card--disabled': formDisabled })}> <div className={classnames({ 'card--disabled': formDisabled })}>
{mode === PUBLISH_MODES.FILE && <PublishDescription disabled={formDisabled} />} {mode === PUBLISH_MODES.FILE && <PublishDescription disabled={formDisabled} />}
@ -402,11 +387,7 @@ function PublishForm(props: Props) {
tagsChosen={tags} tagsChosen={tags}
/> />
<PublishName <PublishBid disabled={isStillEditing || formDisabled} />
disabled={isStillEditing || formDisabled}
autoPopulateName={autoPopulateNameFromTitle}
setAutoPopulateName={setAutoPopulateNameFromTitle}
/>
<PublishPrice disabled={formDisabled} /> <PublishPrice disabled={formDisabled} />
<PublishAdditionalOptions disabled={formDisabled} /> <PublishAdditionalOptions disabled={formDisabled} />
</div> </div>

View file

@ -3,33 +3,25 @@ import {
makeSelectPublishFormValue, makeSelectPublishFormValue,
selectIsStillEditing, selectIsStillEditing,
selectMyClaimForUri, selectMyClaimForUri,
selectIsResolvingPublishUris,
selectTakeOverAmount,
doUpdatePublishForm, doUpdatePublishForm,
doPrepareEdit, doPrepareEdit,
selectBalance,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { doSetActiveChannel } from 'redux/actions/app';
import PublishPage from './view'; import PublishPage from './view';
const select = state => ({ const select = state => ({
name: makeSelectPublishFormValue('name')(state), name: makeSelectPublishFormValue('name')(state),
channel: makeSelectPublishFormValue('channel')(state),
bid: makeSelectPublishFormValue('bid')(state),
uri: makeSelectPublishFormValue('uri')(state),
isStillEditing: selectIsStillEditing(state), isStillEditing: selectIsStillEditing(state),
isResolvingUri: selectIsResolvingPublishUris(state),
amountNeededForTakeover: selectTakeOverAmount(state),
balance: selectBalance(state),
myClaimForUri: selectMyClaimForUri(state), myClaimForUri: selectMyClaimForUri(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)), updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)), prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
setActiveChannel: claimId => dispatch(doSetActiveChannel(claimId)),
}); });
export default connect( export default connect(select, perform)(PublishPage);
select,
perform
)(PublishPage);

View file

@ -1,51 +1,41 @@
// @flow // @flow
import { CHANNEL_NEW, CHANNEL_ANONYMOUS, MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim'; import { DOMAIN } from 'config';
import { INVALID_NAME_ERROR } from 'constants/claim';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { isNameValid } from 'lbry-redux'; import { isNameValid } from 'lbry-redux';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import NameHelpText from './name-help-text'; import NameHelpText from './name-help-text';
import BidHelpText from './bid-help-text';
import Card from 'component/common/card';
import LbcSymbol from 'component/common/lbc-symbol';
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
type Props = { type Props = {
name: string, name: string,
channel: string,
uri: string, uri: string,
bid: number,
balance: number,
disabled: boolean,
isStillEditing: boolean, isStillEditing: boolean,
myClaimForUri: ?StreamClaim, myClaimForUri: ?StreamClaim,
isResolvingUri: boolean,
amountNeededForTakeover: number, amountNeededForTakeover: number,
prepareEdit: ({}, string) => void, prepareEdit: ({}, string) => void,
updatePublishForm: ({}) => void, updatePublishForm: ({}) => void,
autoPopulateName: boolean, activeChannelClaim: ?ChannelClaim,
setAutoPopulateName: boolean => void, incognito: boolean,
}; };
function PublishName(props: Props) { function PublishName(props: Props) {
const { const {
name, name,
channel,
uri, uri,
disabled,
isStillEditing, isStillEditing,
myClaimForUri, myClaimForUri,
bid,
isResolvingUri,
amountNeededForTakeover,
prepareEdit, prepareEdit,
updatePublishForm, updatePublishForm,
balance, activeChannelClaim,
autoPopulateName, incognito,
setAutoPopulateName,
} = props; } = props;
const [nameError, setNameError] = useState(undefined); const [nameError, setNameError] = useState(undefined);
const [bidError, setBidError] = useState(undefined); const [blurred, setBlurred] = React.useState(false);
const previousBidAmount = myClaimForUri && Number(myClaimForUri.amount); const activeChannelName = activeChannelClaim && activeChannelClaim.name;
let prefix = IS_WEB ? `${DOMAIN}/` : 'lbry://';
if (activeChannelName && !incognito) {
prefix += `${activeChannelName}/`;
}
function editExistingClaim() { function editExistingClaim() {
if (myClaimForUri) { if (myClaimForUri) {
@ -55,21 +45,13 @@ function PublishName(props: Props) {
function handleNameChange(event) { function handleNameChange(event) {
updatePublishForm({ name: event.target.value }); updatePublishForm({ name: event.target.value });
if (autoPopulateName) {
setAutoPopulateName(false);
}
} }
useEffect(() => { useEffect(() => {
const hasName = name && name.trim() !== ''; if (!blurred && !name) {
// Enable name autopopulation from title return;
if (!hasName && !autoPopulateName) {
setAutoPopulateName(true);
} }
}, [name, autoPopulateName, setAutoPopulateName]);
useEffect(() => {
let nameError; let nameError;
if (!name) { if (!name) {
nameError = __('A name is required'); nameError = __('A name is required');
@ -78,46 +60,22 @@ function PublishName(props: Props) {
} }
setNameError(nameError); setNameError(nameError);
}, [name]); }, [name, blurred]);
useEffect(() => {
const totalAvailableBidAmount = previousBidAmount ? previousBidAmount + balance : balance;
let bidError;
if (bid === 0) {
bidError = __('Deposit cannot be 0');
} else if (bid < MINIMUM_PUBLISH_BID) {
bidError = __('Your deposit must be higher');
} else if (totalAvailableBidAmount < bid) {
bidError = __('Deposit cannot be higher than your available balance: %balance%', {
balance: totalAvailableBidAmount,
});
} else if (totalAvailableBidAmount <= bid + 0.05) {
bidError = __('Please decrease your deposit to account for transaction fees or acquire more LBRY Credits.');
}
setBidError(bidError);
updatePublishForm({ bidError: bidError });
}, [bid, previousBidAmount, balance, updatePublishForm]);
return ( return (
<Card <>
actions={
<React.Fragment>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix"> <fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section> <fieldset-section>
<label>{__('Name')}</label> <label>{__('Name')}</label>
<div className="form-field__prefix">{`lbry://${ <div className="form-field__prefix">{prefix}</div>
!channel || channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : `${channel}/`
}`}</div>
</fieldset-section> </fieldset-section>
<FormField <FormField
type="text" type="text"
name="content_name" name="content_name"
value={name} value={name}
disabled={disabled}
error={nameError} error={nameError}
onChange={handleNameChange} onChange={handleNameChange}
onBlur={() => setBlurred(true)}
/> />
</fieldset-group> </fieldset-group>
<div className="form-field__help"> <div className="form-field__help">
@ -128,33 +86,7 @@ function PublishName(props: Props) {
onEditMyClaim={editExistingClaim} onEditMyClaim={editExistingClaim}
/> />
</div> </div>
<FormField
type="number"
name="content_bid"
min="0"
step="any"
placeholder="0.123"
className="form-field--price-amount"
label={<LbcSymbol postfix={__('Deposit')} size={12} />}
value={bid}
error={bidError}
disabled={!name}
onChange={event => updatePublishForm({ bid: parseFloat(event.target.value) })}
onWheel={e => e.stopPropagation()}
helper={
<>
<BidHelpText
uri={'lbry://' + name}
amountNeededForTakeover={amountNeededForTakeover}
isResolvingUri={isResolvingUri}
/>
<WalletSpendableBalanceHelp inline />
</> </>
}
/>
</React.Fragment>
}
/>
); );
} }

View file

@ -14,8 +14,10 @@ import {
doCheckPendingClaims, doCheckPendingClaims,
makeSelectEffectiveAmountForUri, makeSelectEffectiveAmountForUri,
makeSelectIsUriResolving, makeSelectIsUriResolving,
selectFetchingMyChannels,
} from 'lbry-redux'; } from 'lbry-redux';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import RepostCreate from './view'; import RepostCreate from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -33,6 +35,8 @@ const select = (state, props) => ({
myClaims: selectMyClaimsWithoutChannels(state), myClaims: selectMyClaimsWithoutChannels(state),
isResolvingPassedRepost: props.name && makeSelectIsUriResolving(`lbry://${props.name}`)(state), isResolvingPassedRepost: props.name && makeSelectIsUriResolving(`lbry://${props.name}`)(state),
isResolvingEnteredRepost: props.repostUri && makeSelectIsUriResolving(`lbry://${props.repostUri}`)(state), isResolvingEnteredRepost: props.repostUri && makeSelectIsUriResolving(`lbry://${props.repostUri}`)(state),
activeChannelClaim: selectActiveChannelClaim(state),
fetchingMyChannels: selectFetchingMyChannels(state),
}); });
export default connect(select, { export default connect(select, {

View file

@ -1,20 +1,21 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import { CHANNEL_NEW, MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim'; import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim';
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import Card from 'component/common/card'; import Card from 'component/common/card';
import Button from 'component/button'; import Button from 'component/button';
import SelectChannel from 'component/selectChannel'; import ChannelSelector from 'component/channelSelector';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import { parseURI, isNameValid, creditsToString, isURIValid, normalizeURI } from 'lbry-redux'; import { parseURI, isNameValid, creditsToString, isURIValid, normalizeURI } from 'lbry-redux';
import usePersistedState from 'effects/use-persisted-state';
import analytics from 'analytics'; import analytics from 'analytics';
import LbcSymbol from 'component/common/lbc-symbol'; import LbcSymbol from 'component/common/lbc-symbol';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import { URL as SITE_URL, URL_LOCAL, URL_DEV } from 'config'; import { URL as SITE_URL, URL_LOCAL, URL_DEV } from 'config';
import HelpLink from 'component/common/help-link'; import HelpLink from 'component/common/help-link';
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
import Spinner from 'component/spinner';
type Props = { type Props = {
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
@ -39,6 +40,8 @@ type Props = {
enteredRepostAmount: number, enteredRepostAmount: number,
isResolvingPassedRepost: boolean, isResolvingPassedRepost: boolean,
isResolvingEnteredRepost: boolean, isResolvingEnteredRepost: boolean,
activeChannelClaim: ?ChannelClaim,
fetchingMyChannels: boolean,
}; };
function RepostCreate(props: Props) { function RepostCreate(props: Props) {
@ -50,7 +53,6 @@ function RepostCreate(props: Props) {
claim, claim,
enteredContentClaim, enteredContentClaim,
balance, balance,
channels,
reposting, reposting,
doCheckPublishNameAvailability, doCheckPublishNameAvailability,
uri, // ?from uri, // ?from
@ -63,12 +65,13 @@ function RepostCreate(props: Props) {
passedRepostAmount, passedRepostAmount,
isResolvingPassedRepost, isResolvingPassedRepost,
isResolvingEnteredRepost, isResolvingEnteredRepost,
activeChannelClaim,
fetchingMyChannels,
} = props; } = props;
const defaultName = name || (claim && claim.name) || ''; const defaultName = name || (claim && claim.name) || '';
const contentClaimId = claim && claim.claim_id; const contentClaimId = claim && claim.claim_id;
const enteredClaimId = enteredContentClaim && enteredContentClaim.claim_id; const enteredClaimId = enteredContentClaim && enteredContentClaim.claim_id;
const [repostChannel, setRepostChannel] = usePersistedState('repost-channel', 'anonymous');
const [repostBid, setRepostBid] = React.useState(0.01); const [repostBid, setRepostBid] = React.useState(0.01);
const [repostBidError, setRepostBidError] = React.useState(undefined); const [repostBidError, setRepostBidError] = React.useState(undefined);
@ -80,9 +83,7 @@ function RepostCreate(props: Props) {
const { replace, goBack } = useHistory(); const { replace, goBack } = useHistory();
const resolvingRepost = isResolvingEnteredRepost || isResolvingPassedRepost; const resolvingRepost = isResolvingEnteredRepost || isResolvingPassedRepost;
const repostUrlName = `lbry://${ const repostUrlName = `lbry://${!activeChannelClaim ? '' : `${activeChannelClaim.name}/`}`;
!repostChannel || repostChannel === CHANNEL_NEW || repostChannel === 'anonymous' ? '' : `${repostChannel}/`
}`;
const contentFirstRender = React.useRef(true); const contentFirstRender = React.useRef(true);
const setAutoRepostBid = amount => { const setAutoRepostBid = amount => {
@ -173,17 +174,6 @@ function RepostCreate(props: Props) {
contentNameError = __('A name is required'); contentNameError = __('A name is required');
} }
// repostChannel
const channelStrings = channels && channels.map(channel => channel.permanent_url).join(',');
React.useEffect(() => {
if (!repostChannel && channelStrings) {
const channels = channelStrings.split(',');
const newChannelUrl = channels[0];
const { claimName } = parseURI(newChannelUrl);
setRepostChannel(claimName);
}
}, [channelStrings]);
React.useEffect(() => { React.useEffect(() => {
if (enteredRepostName && isNameValid(enteredRepostName, false)) { if (enteredRepostName && isNameValid(enteredRepostName, false)) {
doCheckPublishNameAvailability(enteredRepostName).then(r => setAvailable(r)); doCheckPublishNameAvailability(enteredRepostName).then(r => setAvailable(r));
@ -287,12 +277,11 @@ function RepostCreate(props: Props) {
}; };
function handleSubmit() { function handleSubmit() {
const channelToRepostTo = channels && channels.find(channel => channel.name === repostChannel);
if (enteredRepostName && repostBid && repostClaimId) { if (enteredRepostName && repostBid && repostClaimId) {
doRepost({ doRepost({
name: enteredRepostName, name: enteredRepostName,
bid: creditsToString(repostBid), bid: creditsToString(repostBid),
channel_id: channelToRepostTo ? channelToRepostTo.claim_id : undefined, channel_id: activeChannelClaim ? activeChannelClaim.claim_id : undefined,
claim_id: repostClaimId, claim_id: repostClaimId,
}).then((repostClaim: StreamClaim) => { }).then((repostClaim: StreamClaim) => {
doCheckPendingClaims(); doCheckPendingClaims();
@ -308,8 +297,18 @@ function RepostCreate(props: Props) {
goBack(); goBack();
} }
if (fetchingMyChannels) {
return (
<div className="main--empty">
<Spinner />
</div>
);
}
return ( return (
<> <>
<ChannelSelector />
<Card <Card
actions={ actions={
<div> <div>
@ -331,6 +330,7 @@ function RepostCreate(props: Props) {
/> />
</> </>
)} )}
{!uri && ( {!uri && (
<fieldset-section> <fieldset-section>
<ClaimPreview key={contentUri} uri={contentUri} actions={''} type={'large'} showNullPlaceholder /> <ClaimPreview key={contentUri} uri={contentUri} actions={''} type={'large'} showNullPlaceholder />
@ -362,12 +362,6 @@ function RepostCreate(props: Props) {
/> />
</fieldset-group> </fieldset-group>
</fieldset-section> </fieldset-section>
<SelectChannel
label={__('Channel to repost on')}
hideNew
channel={repostChannel}
onChannelChange={newChannel => setRepostChannel(newChannel)}
/>
<FormField <FormField
type="number" type="number"
@ -379,9 +373,14 @@ function RepostCreate(props: Props) {
label={<LbcSymbol postfix={__('Support --[button to support a claim]--')} size={14} />} label={<LbcSymbol postfix={__('Support --[button to support a claim]--')} size={14} />}
value={repostBid} value={repostBid}
error={repostBidError} error={repostBidError}
helper={__('Winning amount: %amount%', { helper={
<>
{__('Winning amount: %amount%', {
amount: Number(takeoverAmount).toFixed(2), amount: Number(takeoverAmount).toFixed(2),
})} })}
<WalletSpendableBalanceHelp inline />
</>
}
disabled={!enteredRepostName || resolvingRepost} disabled={!enteredRepostName || resolvingRepost}
onChange={event => setRepostBid(event.target.value)} onChange={event => setRepostBid(event.target.value)}
onWheel={e => e.stopPropagation()} onWheel={e => e.stopPropagation()}

View file

@ -8,17 +8,21 @@ import {
doCreateChannel, doCreateChannel,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { doSetActiveChannel } from 'redux/actions/app';
const select = state => ({ const select = state => ({
channels: selectMyChannelClaims(state), myChannelClaims: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
balance: selectBalance(state), balance: selectBalance(state),
emailVerified: selectUserVerifiedEmail(state), emailVerified: selectUserVerifiedEmail(state),
activeChannelClaim: selectActiveChannelClaim(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()), fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
setActiveChannel: claimId => dispatch(doSetActiveChannel(claimId)),
}); });
export default connect(select, perform)(SelectChannel); export default connect(select, perform)(SelectChannel);

View file

@ -1,115 +1,51 @@
// @flow // @flow
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim'; import React from 'react';
import React, { Fragment } from 'react';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import ChannelCreate from 'component/channelCreate';
type Props = { type Props = {
channel: string, // currently selected channel tiny?: boolean,
channels: ?Array<ChannelClaim>, label: string,
balance: number, myChannelClaims: ?Array<ChannelClaim>,
onChannelChange: string => void, injected: ?Array<string>,
createChannel: (string, number) => Promise<any>, activeChannelClaim: ?ChannelClaim,
fetchChannelListMine: () => void, setActiveChannel: string => void,
fetchingChannels: boolean, fetchingChannels: boolean,
hideAnon: boolean,
hideNew: boolean,
label?: string,
injected?: Array<string>,
emailVerified: boolean,
tiny: boolean,
}; };
type State = { function SelectChannel(props: Props) {
addingChannel: boolean, const {
}; fetchingChannels,
myChannelClaims = [],
label,
injected = [],
tiny,
activeChannelClaim,
setActiveChannel,
} = props;
const ID_FF_SELECT_CHANNEL = 'ID_FF_SELECT_CHANNEL'; function handleChannelChange(event: SyntheticInputEvent<*>) {
const channelClaimId = event.target.value;
class ChannelSelection extends React.PureComponent<Props, State> { setActiveChannel(channelClaimId);
constructor(props: Props) {
super(props);
this.state = {
addingChannel: props.channel === CHANNEL_NEW,
};
(this: any).handleChannelChange = this.handleChannelChange.bind(this);
(this: any).handleChangeToNewChannel = this.handleChangeToNewChannel.bind(this);
} }
componentDidMount() {
const { channel, channels, fetchChannelListMine, fetchingChannels, emailVerified, onChannelChange } = this.props;
if (IS_WEB && !emailVerified) {
return;
}
if ((!channels || !channels.length) && !fetchingChannels) {
fetchChannelListMine();
}
if (channels && channels.length && !channels.find(chan => chan.name === channel)) {
const elem = document.getElementById(ID_FF_SELECT_CHANNEL);
// $FlowFixMe
if (elem && elem.value && elem.value !== channel) {
setTimeout(() => {
// $FlowFixMe
onChannelChange(elem.value);
}, 250);
}
}
}
componentDidUpdate() {
const { channels, fetchingChannels, hideAnon } = this.props;
if (!fetchingChannels && !channels && hideAnon) {
this.setState({ addingChannel: true });
}
}
handleChannelChange(event: SyntheticInputEvent<*>) {
const { onChannelChange } = this.props;
const channel = event.target.value;
if (channel === CHANNEL_NEW) {
this.setState({ addingChannel: true });
onChannelChange(channel);
} else {
this.setState({ addingChannel: false });
onChannelChange(channel);
}
}
handleChangeToNewChannel(props: Object) {
const { onChannelChange } = this.props;
const { newChannelName } = props;
this.setState({ addingChannel: false });
const channelName = `@${newChannelName.trim()}`;
onChannelChange(channelName);
}
render() {
const channel = this.state.addingChannel ? CHANNEL_NEW : this.props.channel;
const { fetchingChannels, channels = [], hideAnon, hideNew, label, injected = [], tiny } = this.props;
const { addingChannel } = this.state;
return ( return (
<Fragment> <>
<FormField <FormField
id={ID_FF_SELECT_CHANNEL}
name="channel" name="channel"
label={!tiny && (label || __('Channel'))} label={!tiny && (label || __('Channel'))}
labelOnLeft={tiny} labelOnLeft={tiny}
type={tiny ? 'select-tiny' : 'select'} type={tiny ? 'select-tiny' : 'select'}
onChange={this.handleChannelChange} onChange={handleChannelChange}
value={channel} value={activeChannelClaim && activeChannelClaim.claim_id}
disabled={fetchingChannels}
> >
{!hideAnon && <option value={CHANNEL_ANONYMOUS}>{__('Anonymous')}</option>} {fetchingChannels ? (
{channels && <option>{__('Loading your channels...')}</option>
channels.map(({ name, claim_id: claimId }) => ( ) : (
<option key={claimId} value={name}> <>
{myChannelClaims &&
myChannelClaims.map(({ name, claim_id: claimId }) => (
<option key={claimId} value={claimId}>
{name} {name}
</option> </option>
))} ))}
@ -119,13 +55,11 @@ class ChannelSelection extends React.PureComponent<Props, State> {
{item} {item}
</option> </option>
))} ))}
{!fetchingChannels && !hideNew && <option value={CHANNEL_NEW}>{__('New channel...')}</option>} </>
)}
</FormField> </FormField>
</>
{addingChannel && <ChannelCreate onSuccess={this.handleChangeToNewChannel} />}
</Fragment>
); );
}
} }
export default ChannelSelection; export default SelectChannel;

View file

@ -6,7 +6,6 @@ import {
selectIsSendingSupport, selectIsSendingSupport,
selectBalance, selectBalance,
SETTINGS, SETTINGS,
selectMyChannelClaims,
makeSelectClaimIsMine, makeSelectClaimIsMine,
selectFetchingMyChannels, selectFetchingMyChannels,
} from 'lbry-redux'; } from 'lbry-redux';
@ -14,6 +13,7 @@ import WalletSendTip from './view';
import { doOpenModal, doHideModal } from 'redux/actions/app'; import { doOpenModal, doHideModal } from 'redux/actions/app';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
const select = (state, props) => ({ const select = (state, props) => ({
isPending: selectIsSendingSupport(state), isPending: selectIsSendingSupport(state),
@ -22,9 +22,10 @@ const select = (state, props) => ({
balance: selectBalance(state), balance: selectBalance(state),
instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state), instantTipEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state), instantTipMax: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state),
channels: selectMyChannelClaims(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -5,13 +5,13 @@ import * as PAGES from 'constants/pages';
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import { MINIMUM_PUBLISH_BID, CHANNEL_ANONYMOUS, CHANNEL_NEW } from 'constants/claim'; import { MINIMUM_PUBLISH_BID } from 'constants/claim';
import CreditAmount from 'component/common/credit-amount'; import CreditAmount from 'component/common/credit-amount';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import Card from 'component/common/card'; import Card from 'component/common/card';
import classnames from 'classnames'; import classnames from 'classnames';
import SelectChannel from 'component/selectChannel'; import ChannelSelector from 'component/channelSelector';
import LbcSymbol from 'component/common/lbc-symbol'; import LbcSymbol from 'component/common/lbc-symbol';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
@ -34,7 +34,8 @@ type Props = {
fetchingChannels: boolean, fetchingChannels: boolean,
instantTipEnabled: boolean, instantTipEnabled: boolean,
instantTipMax: { amount: number, currency: string }, instantTipMax: { amount: number, currency: string },
channels: ?Array<ChannelClaim>, activeChannelClaim: ?ChannelClaim,
incognito: boolean,
}; };
function WalletSendTip(props: Props) { function WalletSendTip(props: Props) {
@ -49,8 +50,9 @@ function WalletSendTip(props: Props) {
instantTipMax, instantTipMax,
sendSupport, sendSupport,
closeModal, closeModal,
channels,
fetchingChannels, fetchingChannels,
incognito,
activeChannelClaim,
} = props; } = props;
const [presetTipAmount, setPresetTipAmount] = usePersistedState('comment-support:presetTip', DEFAULT_TIP_AMOUNTS[0]); const [presetTipAmount, setPresetTipAmount] = usePersistedState('comment-support:presetTip', DEFAULT_TIP_AMOUNTS[0]);
const [customTipAmount, setCustomTipAmount] = usePersistedState('comment-support:customTip', 1.0); const [customTipAmount, setCustomTipAmount] = usePersistedState('comment-support:customTip', 1.0);
@ -58,21 +60,9 @@ function WalletSendTip(props: Props) {
const [tipError, setTipError] = React.useState(); const [tipError, setTipError] = React.useState();
const [sendAsTip, setSendAsTip] = usePersistedState('comment-support:sendAsTip', true); const [sendAsTip, setSendAsTip] = usePersistedState('comment-support:sendAsTip', true);
const [isConfirming, setIsConfirming] = React.useState(false); const [isConfirming, setIsConfirming] = React.useState(false);
const [selectedChannel, setSelectedChannel] = usePersistedState('comment-support:channel');
const { claim_id: claimId } = claim; const { claim_id: claimId } = claim;
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
const noBalance = balance === 0; const noBalance = balance === 0;
const channelStrings = channels && channels.map(channel => channel.permanent_url).join(',');
React.useEffect(() => {
if (!selectedChannel && channelStrings) {
const channels = channelStrings.split(',');
const newChannelUrl = channels[0];
const { claimName } = parseURI(newChannelUrl);
setSelectedChannel(claimName);
}
}, [channelStrings, selectedChannel, setSelectedChannel]);
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount; const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
const isSupport = claimIsMine ? true : SIMPLE_SITE ? false : !sendAsTip; const isSupport = claimIsMine ? true : SIMPLE_SITE ? false : !sendAsTip;
@ -99,12 +89,8 @@ function WalletSendTip(props: Props) {
function sendSupportOrConfirm(instantTipMaxAmount = null) { function sendSupportOrConfirm(instantTipMaxAmount = null) {
let selectedChannelId; let selectedChannelId;
if (selectedChannel !== CHANNEL_ANONYMOUS) { if (!incognito && activeChannelClaim) {
const selectedChannelClaim = channels && channels.find(channelClaim => channelClaim.name === selectedChannel); selectedChannelId = activeChannelClaim.claim_id;
if (selectedChannelClaim) {
selectedChannelId = selectedChannelClaim.claim_id;
}
} }
if ( if (
@ -124,12 +110,6 @@ function WalletSendTip(props: Props) {
} }
function handleSubmit() { function handleSubmit() {
if (selectedChannel === CHANNEL_NEW) {
// This is the submission to create a new channel, and would
// be handled by <ChannelSelection>, so do nothing here.
return;
}
if (tipAmount && claimId) { if (tipAmount && claimId) {
if (instantTipEnabled) { if (instantTipEnabled) {
if (instantTipMax.currency === 'LBC') { if (instantTipMax.currency === 'LBC') {
@ -196,7 +176,9 @@ function WalletSendTip(props: Props) {
<div className="confirm__label">{__('To --[the tip recipient]--')}</div> <div className="confirm__label">{__('To --[the tip recipient]--')}</div>
<div className="confirm__value">{channelName || title}</div> <div className="confirm__value">{channelName || title}</div>
<div className="confirm__label">{__('From --[the tip sender]--')}</div> <div className="confirm__label">{__('From --[the tip sender]--')}</div>
<div className="confirm__value">{selectedChannel}</div> <div className="confirm__value">
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
</div>
<div className="confirm__label">{__(isSupport ? 'Supporting' : 'Tipping')}</div> <div className="confirm__label">{__(isSupport ? 'Supporting' : 'Tipping')}</div>
<div className="confirm__value"> <div className="confirm__value">
<LbcSymbol postfix={tipAmount} size={22} /> <LbcSymbol postfix={tipAmount} size={22} />
@ -217,11 +199,7 @@ function WalletSendTip(props: Props) {
) : ( ) : (
<> <>
<div className="section"> <div className="section">
<SelectChannel <ChannelSelector />
label={__('Channel to show support as')}
channel={selectedChannel}
onChannelChange={newChannel => setSelectedChannel(newChannel)}
/>
</div> </div>
<div className="section"> <div className="section">

View file

@ -30,6 +30,8 @@ export const SET_HAS_NAVIGATED = 'SET_HAS_NAVIGATED';
export const SET_SYNC_LOCK = 'SET_SYNC_LOCK'; export const SET_SYNC_LOCK = 'SET_SYNC_LOCK';
export const TOGGLE_YOUTUBE_SYNC_INTEREST = 'TOGGLE_YOUTUBE_SYNC_INTEREST'; export const TOGGLE_YOUTUBE_SYNC_INTEREST = 'TOGGLE_YOUTUBE_SYNC_INTEREST';
export const TOGGLE_SPLASH_ANIMATION = 'TOGGLE_SPLASH_ANIMATION'; export const TOGGLE_SPLASH_ANIMATION = 'TOGGLE_SPLASH_ANIMATION';
export const SET_ACTIVE_CHANNEL = 'SET_ACTIVE_CHANNEL';
export const SET_INCOGNITO = 'SET_INCOGNITO';
// Navigation // Navigation
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH'; export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';
@ -266,7 +268,6 @@ export const COMMENT_REACT_FAILED = 'COMMENT_REACT_FAILED';
export const COMMENT_PIN_STARTED = 'COMMENT_PIN_STARTED'; export const COMMENT_PIN_STARTED = 'COMMENT_PIN_STARTED';
export const COMMENT_PIN_COMPLETED = 'COMMENT_PIN_COMPLETED'; export const COMMENT_PIN_COMPLETED = 'COMMENT_PIN_COMPLETED';
export const COMMENT_PIN_FAILED = 'COMMENT_PIN_FAILED'; export const COMMENT_PIN_FAILED = 'COMMENT_PIN_FAILED';
export const COMMENT_SET_CHANNEL = 'COMMENT_SET_CHANNEL';
// Blocked channels // Blocked channels
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL'; export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';

View file

@ -130,3 +130,4 @@ export const PIN = 'Pin';
export const BEST = 'Best'; export const BEST = 'Best';
export const CREATOR_LIKE = 'CreatorLike'; export const CREATOR_LIKE = 'CreatorLike';
export const CHEF = 'Chef'; export const CHEF = 'Chef';
export const ANONYMOUS = 'Anonymous';

View file

@ -36,7 +36,6 @@ export const WALLET_RECEIVE = 'wallet_receive';
export const CREATE_CHANNEL = 'create_channel'; export const CREATE_CHANNEL = 'create_channel';
export const YOUTUBE_WELCOME = 'youtube_welcome'; export const YOUTUBE_WELCOME = 'youtube_welcome';
export const SET_REFERRER = 'set_referrer'; export const SET_REFERRER = 'set_referrer';
export const REPOST = 'repost';
export const SIGN_OUT = 'sign_out'; export const SIGN_OUT = 'sign_out';
export const LIQUIDATE_SUPPORTS = 'liquidate_supports'; export const LIQUIDATE_SUPPORTS = 'liquidate_supports';
export const MASS_TIP_UNLOCK = 'mass_tip_unlock'; export const MASS_TIP_UNLOCK = 'mass_tip_unlock';

View file

@ -1,34 +0,0 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import {
makeSelectClaimForUri,
makeSelectTitleForUri,
selectBalance,
selectMyChannelClaims,
doRepost,
selectRepostError,
selectRepostLoading,
doClearRepostError,
selectMyClaimsWithoutChannels,
doCheckPublishNameAvailability,
} from 'lbry-redux';
import { doToast } from 'redux/actions/notifications';
import ModalRepost from './view';
const select = (state, props) => ({
channels: selectMyChannelClaims(state),
claim: makeSelectClaimForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
balance: selectBalance(state),
error: selectRepostError(state),
reposting: selectRepostLoading(state),
myClaims: selectMyClaimsWithoutChannels(state),
});
export default connect(select, {
doHideModal,
doRepost,
doClearRepostError,
doToast,
doCheckPublishNameAvailability,
})(ModalRepost);

View file

@ -1,212 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import { CHANNEL_NEW, MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim';
import React from 'react';
import { Modal } from 'modal/modal';
import Card from 'component/common/card';
import Button from 'component/button';
import SelectChannel from 'component/selectChannel';
import ErrorText from 'component/common/error-text';
import { FormField } from 'component/common/form';
import { parseURI, isNameValid, creditsToString } from 'lbry-redux';
import usePersistedState from 'effects/use-persisted-state';
import I18nMessage from 'component/i18nMessage';
import analytics from 'analytics';
import LbcSymbol from 'component/common/lbc-symbol';
type Props = {
doHideModal: () => void,
doToast: ({ message: string }) => void,
doClearRepostError: () => void,
doRepost: StreamRepostOptions => Promise<*>,
title: string,
claim: ?StreamClaim,
balance: number,
channels: ?Array<ChannelClaim>,
doCheckPublishNameAvailability: string => Promise<*>,
error: ?string,
reposting: boolean,
};
function ModalRepost(props: Props) {
const {
doHideModal,
doToast,
doClearRepostError,
doRepost,
title,
claim,
balance,
channels,
error,
reposting,
doCheckPublishNameAvailability,
} = props;
const defaultName = claim && claim.name;
const contentClaimId = claim && claim.claim_id;
const [repostChannel, setRepostChannel] = usePersistedState('repost-channel');
const [repostBid, setRepostBid] = React.useState(0.01);
const [showAdvanced, setShowAdvanced] = React.useState();
const [repostName, setRepostName] = React.useState(defaultName);
const [available, setAvailable] = React.useState(true);
let repostBidError;
if (repostBid === 0) {
repostBidError = __('Deposit cannot be 0');
} else if (balance === repostBid) {
repostBidError = __('Please decrease your deposit to account for transaction fees');
} else if (balance < repostBid) {
repostBidError = __('Deposit cannot be higher than your available balance');
} else if (repostBid < MINIMUM_PUBLISH_BID) {
repostBidError = __('Your deposit must be higher');
}
let repostNameError;
if (!repostName) {
repostNameError = __('A name is required');
} else if (!isNameValid(repostName, false)) {
repostNameError = INVALID_NAME_ERROR;
} else if (!available) {
repostNameError = __('You already have a claim with this name.');
}
React.useEffect(() => {
if ((repostNameError || repostNameError) && !showAdvanced) {
setShowAdvanced(true);
}
}, [repostBidError, repostNameError, showAdvanced, setShowAdvanced]);
const channelStrings = channels && channels.map(channel => channel.permanent_url).join(',');
React.useEffect(() => {
if (!repostChannel && channelStrings) {
const channels = channelStrings.split(',');
const newChannelUrl = channels[0];
const { claimName } = parseURI(newChannelUrl);
setRepostChannel(claimName);
}
}, [channelStrings]);
React.useEffect(() => {
if (repostName && isNameValid(repostName, false)) {
doCheckPublishNameAvailability(repostName).then(r => setAvailable(r));
}
}, [repostName, doCheckPublishNameAvailability]);
function handleSubmit() {
const channelToRepostTo = channels && channels.find(channel => channel.name === repostChannel);
if (channelToRepostTo && repostName && repostBid && repostChannel && contentClaimId) {
doRepost({
name: repostName,
bid: creditsToString(repostBid),
channel_id: channelToRepostTo.claim_id,
claim_id: contentClaimId,
}).then((repostClaim: StreamClaim) => {
analytics.apiLogPublish(repostClaim);
doHideModal();
doToast({ message: __('Woohoo! Successfully reposted this claim.') });
});
}
}
function handleCloseModal() {
doClearRepostError();
doHideModal();
}
return (
<Modal isOpen type="card" onAborted={handleCloseModal} onConfirmed={handleCloseModal}>
<Card
title={
<span>
<I18nMessage tokens={{ title: <em>{title}</em> }}>Repost %title%</I18nMessage>
</span>
}
subtitle={
error ? (
<ErrorText>{__('There was an error reposting this claim. Please try again later.')}</ErrorText>
) : (
<span>{__('Repost your favorite claims to help more people discover them!')}</span>
)
}
actions={
<div>
<SelectChannel
label={__('Channel to repost on')}
hideAnon
hideNew
channel={repostChannel}
onChannelChange={newChannel => setRepostChannel(newChannel)}
/>
{!showAdvanced && (
<div className="section__actions">
<Button button="link" label={__('Advanced')} onClick={() => setShowAdvanced(true)} />
</div>
)}
{showAdvanced && (
<React.Fragment>
<fieldset-section>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label>{__('Name')}</label>
<div className="form-field__prefix">{`lbry://${
!repostChannel || repostChannel === CHANNEL_NEW ? '' : `${repostChannel}/`
}`}</div>
</fieldset-section>
<FormField
type="text"
name="repost_name"
value={repostName}
error={repostNameError}
onChange={event => setRepostName(event.target.value)}
/>
</fieldset-group>
</fieldset-section>
<div className="form-field__help">
<I18nMessage
tokens={{
lbry_naming_link: (
<Button button="link" label={__('community name')} href="https://lbry.com/faq/naming" />
),
}}
>
Change this to repost to a different %lbry_naming_link%.
</I18nMessage>
</div>
<FormField
type="number"
name="repost_bid"
min="0"
step="any"
placeholder="0.123"
className="form-field--price-amount"
label={<LbcSymbol postfix={__('Deposit')} size={14} />}
value={repostBid}
error={repostBidError}
disabled={!repostName}
onChange={event => setRepostBid(parseFloat(event.target.value))}
onWheel={e => e.stopPropagation()}
/>
</React.Fragment>
)}
<div className="section__actions">
<Button
icon={ICONS.REPOST}
disabled={reposting || repostBidError || repostNameError}
button="primary"
label={reposting ? __('Reposting') : __('Repost')}
onClick={handleSubmit}
/>
<Button button="link" label={__('Cancel')} onClick={handleCloseModal} />
</div>
</div>
}
/>
</Modal>
);
}
export default ModalRepost;

View file

@ -34,7 +34,6 @@ import ModalWalletReceive from 'modal/modalWalletReceive';
import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome'; import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
import ModalCreateChannel from 'modal/modalChannelCreate'; import ModalCreateChannel from 'modal/modalChannelCreate';
import ModalSetReferrer from 'modal/modalSetReferrer'; import ModalSetReferrer from 'modal/modalSetReferrer';
import ModalRepost from 'modal/modalRepost';
import ModalSignOut from 'modal/modalSignOut'; import ModalSignOut from 'modal/modalSignOut';
import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate'; import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate';
import ModalConfirmAge from 'modal/modalConfirmAge'; import ModalConfirmAge from 'modal/modalConfirmAge';
@ -135,8 +134,6 @@ function ModalRouter(props: Props) {
return <ModalCreateChannel {...modalProps} />; return <ModalCreateChannel {...modalProps} />;
case MODALS.SET_REFERRER: case MODALS.SET_REFERRER:
return <ModalSetReferrer {...modalProps} />; return <ModalSetReferrer {...modalProps} />;
case MODALS.REPOST:
return <ModalRepost {...modalProps} />;
case MODALS.SIGN_OUT: case MODALS.SIGN_OUT:
return <ModalSignOut {...modalProps} />; return <ModalSignOut {...modalProps} />;
case MODALS.CONFIRM_AGE: case MODALS.CONFIRM_AGE:

View file

@ -1,10 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; import { selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { doSetActiveChannel } from 'redux/actions/app';
import CreatorDashboardPage from './view'; import CreatorDashboardPage from './view';
const select = state => ({ const select = state => ({
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
activeChannelClaim: selectActiveChannelClaim(state),
}); });
export default connect(select)(CreatorDashboardPage); export default connect(select, { doSetActiveChannel })(CreatorDashboardPage);

View file

@ -6,54 +6,17 @@ import Spinner from 'component/spinner';
import Button from 'component/button'; import Button from 'component/button';
import CreatorAnalytics from 'component/creatorAnalytics'; import CreatorAnalytics from 'component/creatorAnalytics';
import ChannelSelector from 'component/channelSelector'; import ChannelSelector from 'component/channelSelector';
import usePersistedState from 'effects/use-persisted-state';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
import { useHistory } from 'react-router';
type Props = { type Props = {
channels: Array<ChannelClaim>, channels: Array<ChannelClaim>,
fetchingChannels: boolean, fetchingChannels: boolean,
activeChannelClaim: ?ChannelClaim,
}; };
const SELECTED_CHANNEL_QUERY_PARAM = 'channel';
export default function CreatorDashboardPage(props: Props) { export default function CreatorDashboardPage(props: Props) {
const { channels, fetchingChannels } = props; const { channels, fetchingChannels, activeChannelClaim } = props;
const {
push,
location: { search, pathname },
} = useHistory();
const urlParams = new URLSearchParams(search);
const channelFromUrl = urlParams.get(SELECTED_CHANNEL_QUERY_PARAM);
const [selectedChannelUrl, setSelectedChannelUrl] = usePersistedState('analytics-selected-channel');
const hasChannels = channels && channels.length > 0; const hasChannels = channels && channels.length > 0;
const firstChannel = hasChannels && channels[0];
const firstChannelUrl = firstChannel && (firstChannel.canonical_url || firstChannel.permanent_url); // permanent_url is needed for pending publishes
const channelFoundForSelectedChannelUrl =
channels &&
channels.find(channel => {
return selectedChannelUrl === channel.canonical_url || selectedChannelUrl === channel.permanent_url;
});
React.useEffect(() => {
// set default channel
if ((!selectedChannelUrl || !channelFoundForSelectedChannelUrl) && firstChannelUrl) {
setSelectedChannelUrl(firstChannelUrl);
}
}, [setSelectedChannelUrl, selectedChannelUrl, firstChannelUrl, channelFoundForSelectedChannelUrl]);
React.useEffect(() => {
if (channelFromUrl) {
const decodedChannel = decodeURIComponent(channelFromUrl);
setSelectedChannelUrl(decodedChannel);
}
}, [channelFromUrl, setSelectedChannelUrl]);
function updateUrl(channelUrl) {
const newUrlParams = new URLSearchParams();
newUrlParams.append(SELECTED_CHANNEL_QUERY_PARAM, encodeURIComponent(channelUrl));
push(`${pathname}?${newUrlParams.toString()}`);
}
return ( return (
<Page> <Page>
@ -63,7 +26,7 @@ export default function CreatorDashboardPage(props: Props) {
</div> </div>
)} )}
{!fetchingChannels && (!channels || !channels.length) && ( {!fetchingChannels && !hasChannels && (
<Yrbl <Yrbl
type="happy" type="happy"
title={__("You haven't created a channel yet, let's fix that!")} title={__("You haven't created a channel yet, let's fix that!")}
@ -75,18 +38,10 @@ export default function CreatorDashboardPage(props: Props) {
/> />
)} )}
{!fetchingChannels && channels && channels.length && ( {!fetchingChannels && activeChannelClaim && (
<React.Fragment> <React.Fragment>
<div className="section"> <ChannelSelector hideAnon />
<ChannelSelector <CreatorAnalytics uri={activeChannelClaim.canonical_url} />
selectedChannelUrl={selectedChannelUrl}
onChannelSelect={newChannelUrl => {
updateUrl(newChannelUrl);
setSelectedChannelUrl(newChannelUrl);
}}
/>
</div>
<CreatorAnalytics uri={selectedChannelUrl} />
</React.Fragment> </React.Fragment>
)} )}
</Page> </Page>

View file

@ -21,6 +21,7 @@ import {
SHARED_PREFERENCES, SHARED_PREFERENCES,
DAEMON_SETTINGS, DAEMON_SETTINGS,
SETTINGS, SETTINGS,
selectMyChannelClaims,
} from 'lbry-redux'; } from 'lbry-redux';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import { selectFollowedTagsList } from 'redux/selectors/tags'; import { selectFollowedTagsList } from 'redux/selectors/tags';
@ -693,3 +694,54 @@ export function doToggleSplashAnimation() {
type: ACTIONS.TOGGLE_SPLASH_ANIMATION, type: ACTIONS.TOGGLE_SPLASH_ANIMATION,
}; };
} }
export function doSetActiveChannel(claimId) {
return (dispatch, getState) => {
if (claimId) {
return dispatch({
type: ACTIONS.SET_ACTIVE_CHANNEL,
data: {
claimId,
},
});
}
// If no claimId is passed, set the active channel to the one with the highest effective_amount
const state = getState();
const myChannelClaims = selectMyChannelClaims(state);
if (!myChannelClaims || !myChannelClaims.length) {
return;
}
const myChannelClaimsByEffectiveAmount = myChannelClaims.slice().sort((a, b) => {
const effectiveAmountA = (a.meta && Number(a.meta.effective_amount)) || 0;
const effectiveAmountB = (b.meta && Number(b.meta.effective_amount)) || 0;
if (effectiveAmountA === effectiveAmountB) {
return 0;
} else if (effectiveAmountA > effectiveAmountB) {
return -1;
} else {
return 1;
}
});
const newActiveChannelClaim = myChannelClaimsByEffectiveAmount[0];
dispatch({
type: ACTIONS.SET_ACTIVE_CHANNEL,
data: {
claimId: newActiveChannelClaim.claim_id,
},
});
};
}
export function doSetIncognito(incognitoEnabled) {
return {
type: ACTIONS.SET_INCOGNITO,
data: {
enabled: incognitoEnabled,
},
};
}

View file

@ -1,16 +1,16 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as REACTION_TYPES from 'constants/reactions'; import * as REACTION_TYPES from 'constants/reactions';
import { Lbry, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux'; import { Lbry, selectClaimsByUri } from 'lbry-redux';
import { doToast, doSeeNotifications } from 'redux/actions/notifications'; import { doToast, doSeeNotifications } from 'redux/actions/notifications';
import { import {
makeSelectCommentIdsForUri, makeSelectCommentIdsForUri,
makeSelectMyReactionsForComment, makeSelectMyReactionsForComment,
makeSelectOthersReactionsForComment, makeSelectOthersReactionsForComment,
selectPendingCommentReacts, selectPendingCommentReacts,
selectCommentChannel,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications'; import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications';
import { selectActiveChannelClaim } from 'redux/selectors/app';
export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) { export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
@ -49,34 +49,23 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
}; };
} }
export function doSetCommentChannel(channelName: string) {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.COMMENT_SET_CHANNEL,
data: channelName,
});
};
}
export function doCommentReactList(uri: string | null, commentId?: string) { export function doCommentReactList(uri: string | null, commentId?: string) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const channel = selectCommentChannel(state); const activeChannelClaim = selectActiveChannelClaim(state);
const commentIds = uri ? makeSelectCommentIdsForUri(uri)(state) : [commentId]; const commentIds = uri ? makeSelectCommentIdsForUri(uri)(state) : [commentId];
const myChannels = selectMyChannelClaims(state);
dispatch({ dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_STARTED, type: ACTIONS.COMMENT_REACTION_LIST_STARTED,
}); });
const params: { comment_ids: string, channel_name?: string, channel_id?: string } = { const params: { comment_ids: string, channel_name?: string, channel_id?: string } = {
comment_ids: commentIds.join(','), comment_ids: commentIds.join(','),
}; };
if (channel && myChannels) { if (activeChannelClaim) {
const claimForChannelName = myChannels && myChannels.find(chan => chan.name === channel); params['channel_name'] = activeChannelClaim.name;
const channelId = claimForChannelName && claimForChannelName.claim_id; params['channel_id'] = activeChannelClaim.claim_id;
params['channel_name'] = channel;
params['channel_id'] = channelId;
} }
return Lbry.comment_react_list(params) return Lbry.comment_react_list(params)
@ -102,39 +91,38 @@ export function doCommentReactList(uri: string | null, commentId?: string) {
export function doCommentReact(commentId: string, type: string) { export function doCommentReact(commentId: string, type: string) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const channel = selectCommentChannel(state); const activeChannelClaim = selectActiveChannelClaim(state);
const pendingReacts = selectPendingCommentReacts(state); const pendingReacts = selectPendingCommentReacts(state);
const myChannels = selectMyChannelClaims(state);
const notification = makeSelectNotificationForCommentId(commentId)(state); const notification = makeSelectNotificationForCommentId(commentId)(state);
if (!activeChannelClaim) {
console.error('Unable to react to comment. No activeChannel is set.'); // eslint-disable-line
return;
}
if (notification && !notification.is_seen) { if (notification && !notification.is_seen) {
dispatch(doSeeNotifications([notification.id])); dispatch(doSeeNotifications([notification.id]));
} }
const exclusiveTypes = { const exclusiveTypes = {
[REACTION_TYPES.LIKE]: REACTION_TYPES.DISLIKE, [REACTION_TYPES.LIKE]: REACTION_TYPES.DISLIKE,
[REACTION_TYPES.DISLIKE]: REACTION_TYPES.LIKE, [REACTION_TYPES.DISLIKE]: REACTION_TYPES.LIKE,
}; };
if (!channel || !myChannels) {
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
data: 'No active channel found',
});
return;
}
if (pendingReacts.includes(commentId + exclusiveTypes[type]) || pendingReacts.includes(commentId + type)) { if (pendingReacts.includes(commentId + exclusiveTypes[type]) || pendingReacts.includes(commentId + type)) {
// ignore dislikes during likes, for example // ignore dislikes during likes, for example
return; return;
} }
let myReacts = makeSelectMyReactionsForComment(commentId)(state); let myReacts = makeSelectMyReactionsForComment(commentId)(state);
const othersReacts = makeSelectOthersReactionsForComment(commentId)(state); const othersReacts = makeSelectOthersReactionsForComment(commentId)(state);
const claimForChannelName = myChannels.find(chan => chan.name === channel);
const channelId = claimForChannelName && claimForChannelName.claim_id;
const params: CommentReactParams = { const params: CommentReactParams = {
comment_ids: commentId, comment_ids: commentId,
channel_name: channel, channel_name: activeChannelClaim.name,
channel_id: channelId, channel_id: activeChannelClaim.claim_id,
react_type: type, react_type: type,
}; };
if (myReacts.includes(type)) { if (myReacts.includes(type)) {
params['remove'] = true; params['remove'] = true;
myReacts.splice(myReacts.indexOf(type), 1); myReacts.splice(myReacts.indexOf(type), 1);
@ -197,15 +185,16 @@ export function doCommentReact(commentId: string, type: string) {
}; };
} }
export function doCommentCreate( export function doCommentCreate(comment: string = '', claim_id: string = '', parent_id?: string, uri: string) {
comment: string = '',
claim_id: string = '',
channel: string,
parent_id?: string,
uri: string
) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const activeChannelClaim = selectActiveChannelClaim(state);
if (!activeChannelClaim) {
console.error('Unable to create comment. No activeChannel is set.'); // eslint-disable-line
return;
}
dispatch({ dispatch({
type: ACTIONS.COMMENT_CREATE_STARTED, type: ACTIONS.COMMENT_CREATE_STARTED,
}); });
@ -217,28 +206,10 @@ export function doCommentCreate(
} }
} }
const myChannels = selectMyChannelClaims(state);
const namedChannelClaim = myChannels && myChannels.find(myChannel => myChannel.name === channel);
const channel_id = namedChannelClaim.claim_id;
if (channel_id == null) {
dispatch({
type: ACTIONS.COMMENT_CREATE_FAILED,
data: {},
});
dispatch(
doToast({
message: 'Channel cannot be anonymous, please select a channel and try again.',
isError: true,
})
);
return;
}
return Lbry.comment_create({ return Lbry.comment_create({
comment: comment, comment: comment,
claim_id: claim_id, claim_id: claim_id,
channel_id: channel_id, channel_id: activeChannelClaim.claim_id,
parent_id: parent_id, parent_id: parent_id,
}) })
.then((result: CommentCreateResponse) => { .then((result: CommentCreateResponse) => {
@ -299,32 +270,23 @@ export function doCommentHide(comment_id: string) {
export function doCommentPin(commentId: string, remove: boolean) { export function doCommentPin(commentId: string, remove: boolean) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
// const channel = localStorage.getItem('comment-channel'); const activeChannel = selectActiveChannelClaim(state);
const channel = selectCommentChannel(state);
const myChannels = selectMyChannelClaims(state); if (!activeChannel) {
const claimForChannelName = myChannels && myChannels.find(chan => chan.name === channel); console.error('Unable to pin comment. No activeChannel is set.'); // eslint-disable-line
const channelId = claimForChannelName && claimForChannelName.claim_id; return;
}
dispatch({ dispatch({
type: ACTIONS.COMMENT_PIN_STARTED, type: ACTIONS.COMMENT_PIN_STARTED,
}); });
if (!channelId || !channel || !commentId) {
return dispatch({ return Lbry.comment_pin({
type: ACTIONS.COMMENT_PIN_FAILED,
data: { message: 'missing params - unable to pin' },
});
}
const params: { comment_id: string, channel_name: string, channel_id: string, remove?: boolean } = {
comment_id: commentId, comment_id: commentId,
channel_name: channel, channel_name: activeChannel.name,
channel_id: channelId, channel_id: activeChannel.claim_id,
}; ...(remove ? { remove: true } : {}),
})
if (remove) {
params['remove'] = true;
}
return Lbry.comment_pin(params)
.then((result: CommentPinResponse) => { .then((result: CommentPinResponse) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_PIN_COMPLETED, type: ACTIONS.COMMENT_PIN_COMPLETED,

View file

@ -44,6 +44,8 @@ export type AppState = {
allowAnalytics: boolean, allowAnalytics: boolean,
hasNavigated: boolean, hasNavigated: boolean,
interestedInYoutubeSync: boolean, interestedInYoutubeSync: boolean,
activeChannel: ?string,
incognito: boolean,
}; };
const defaultState: AppState = { const defaultState: AppState = {
@ -80,6 +82,8 @@ const defaultState: AppState = {
allowAnalytics: false, allowAnalytics: false,
hasNavigated: false, hasNavigated: false,
interestedInYoutubeSync: false, interestedInYoutubeSync: false,
activeChannel: undefined,
incognito: false,
}; };
// @@router comes from react-router // @@router comes from react-router
@ -300,6 +304,19 @@ reducers[ACTIONS.TOGGLE_SPLASH_ANIMATION] = (state, action) => {
}; };
}; };
reducers[ACTIONS.SET_ACTIVE_CHANNEL] = (state, action) => {
return {
...state,
activeChannel: action.data.claimId,
};
};
reducers[ACTIONS.SET_INCOGNITO] = (state, action) => {
return {
...state,
incognito: action.data.enabled,
};
};
reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => { reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => {
const { welcomeVersion, allowAnalytics } = action.data; const { welcomeVersion, allowAnalytics } = action.data;
return { return {

View file

@ -16,7 +16,6 @@ const defaultState: CommentsState = {
typesReacting: [], typesReacting: [],
myReactsByCommentId: undefined, myReactsByCommentId: undefined,
othersReactsByCommentId: undefined, othersReactsByCommentId: undefined,
commentChannel: '',
}; };
export default handleActions( export default handleActions(
@ -31,11 +30,6 @@ export default handleActions(
isCommenting: false, isCommenting: false,
}), }),
[ACTIONS.COMMENT_SET_CHANNEL]: (state: CommentsState, action: any) => ({
...state,
commentChannel: action.data,
}),
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => { [ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
const { comment, claimId, uri }: { comment: Comment, claimId: string, uri: string } = action.data; const { comment, claimId, uri }: { comment: Comment, claimId: string, uri: string } = action.data;
const commentById = Object.assign({}, state.commentById); const commentById = Object.assign({}, state.commentById);

View file

@ -1,4 +1,5 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { selectClaimsById, selectMyChannelClaims } from 'lbry-redux';
export const selectState = state => state.app || {}; export const selectState = state => state.app || {};
@ -84,3 +85,37 @@ export const selectIsPasswordSaved = createSelector(selectState, state => state.
export const selectInterestedInYoutubeSync = createSelector(selectState, state => state.interestedInYoutubeSync); export const selectInterestedInYoutubeSync = createSelector(selectState, state => state.interestedInYoutubeSync);
export const selectSplashAnimationEnabled = createSelector(selectState, state => state.splashAnimationEnabled); export const selectSplashAnimationEnabled = createSelector(selectState, state => state.splashAnimationEnabled);
export const selectActiveChannelId = createSelector(selectState, state => state.activeChannel);
export const selectActiveChannelClaim = createSelector(
selectActiveChannelId,
selectClaimsById,
selectMyChannelClaims,
(activeChannelClaimId, claimsById, myChannelClaims) => {
if (!activeChannelClaimId || !claimsById || !myChannelClaims || !myChannelClaims.length) {
return undefined;
}
const activeChannelClaim = claimsById[activeChannelClaimId];
if (activeChannelClaim) {
return activeChannelClaim;
}
const myChannelClaimsByEffectiveAmount = myChannelClaims.slice().sort((a, b) => {
const effectiveAmountA = (a.meta && Number(a.meta.effective_amount)) || 0;
const effectiveAmountB = (b.meta && Number(b.meta.effective_amount)) || 0;
if (effectiveAmountA === effectiveAmountB) {
return 0;
} else if (effectiveAmountA > effectiveAmountB) {
return -1;
} else {
return 1;
}
});
return myChannelClaimsByEffectiveAmount[0];
}
);
export const selectIncognito = createSelector(selectState, state => state.incognito);

View file

@ -16,8 +16,6 @@ export const selectIsPostingComment = createSelector(selectState, state => state
export const selectIsFetchingReacts = createSelector(selectState, state => state.isFetchingReacts); export const selectIsFetchingReacts = createSelector(selectState, state => state.isFetchingReacts);
export const selectCommentChannel = createSelector(selectState, state => state.commentChannel);
export const selectOthersReactsById = createSelector(selectState, state => state.othersReactsByCommentId); export const selectOthersReactsById = createSelector(selectState, state => state.othersReactsByCommentId);
export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => { export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {

View file

@ -264,10 +264,12 @@ $metadata-z-index: 1;
} }
.menu__list.channel__list { .menu__list.channel__list {
margin-top: var(--spacing-s); margin-top: var(--spacing-xs);
margin-left: 0; margin-left: 0;
border-radius: var(--border-radius); border-radius: var(--border-radius);
background: transparent; background: transparent;
max-height: 15rem;
overflow-y: scroll;
[role='menuitem'] { [role='menuitem'] {
&[data-selected] { &[data-selected] {
@ -314,8 +316,14 @@ $metadata-z-index: 1;
.icon { .icon {
color: var(--color-menu-icon); color: var(--color-menu-icon);
margin-left: var(--spacing-l); }
margin-right: var(--spacing-s);
.icon__wrapper {
padding: 0;
height: 2rem;
width: 2rem;
margin-right: var(--spacing-m);
border-radius: var(--border-radius);
} }
&:hover { &:hover {
@ -326,5 +334,20 @@ $metadata-z-index: 1;
.channel__list-item--selected { .channel__list-item--selected {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: var(--color-card-background-highlighted);
.icon--ChevronDown {
margin-left: var(--spacing-l);
}
}
.channel__list-text {
font-weight: var(--font-weight-bold);
}
.channel__selector {
margin-bottom: var(--spacing-m);
@media (min-width: $breakpoint-small) {
margin-bottom: var(--spacing-l);
}
} }

View file

@ -105,7 +105,7 @@ label {
font-size: var(--font-small); font-size: var(--font-small);
color: var(--color-input-label); color: var(--color-input-label);
display: inline-block; display: inline-block;
margin-bottom: var(--spacing-xxs); margin-bottom: 0.1rem;
.icon__lbc { .icon__lbc {
margin-bottom: 4px; margin-bottom: 4px;
@ -361,11 +361,9 @@ fieldset-group {
border: 1px solid; border: 1px solid;
border-top-left-radius: var(--border-radius); border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius); border-bottom-left-radius: var(--border-radius);
border-right: 0;
border-color: var(--color-input-border); border-color: var(--color-input-border);
color: var(--color-text-help); color: var(--color-text-help);
background-color: var(--color-input-bg); background-color: var(--color-input-bg);
border-right: 1px solid var(--border-color);
} }
} }

View file

@ -33,6 +33,14 @@
} }
} }
.icon__wrapper--Anonymous {
background-color: var(--color-gray-1);
.icon {
stroke: var(--color-black);
}
}
.icon--help { .icon--help {
color: var(--color-subtitle); color: var(--color-subtitle);
margin-left: var(--spacing-xs); margin-left: var(--spacing-xs);

View file

@ -5,6 +5,10 @@
overflow-y: hidden; overflow-y: hidden;
} }
} }
[data-reach-menu] {
z-index: 10000;
}
} }
.modal-overlay { .modal-overlay {

View file

@ -5,7 +5,6 @@
} }
[data-reach-menu] { [data-reach-menu] {
font-family: sans-serif;
display: block; display: block;
position: absolute; position: absolute;
z-index: 2; z-index: 2;

View file

@ -58,10 +58,10 @@ const appFilter = createFilter('app', [
'welcomeVersion', 'welcomeVersion',
'interestedInYoutubeSync', 'interestedInYoutubeSync',
'splashAnimationEnabled', 'splashAnimationEnabled',
'activeChannel',
]); ]);
// We only need to persist the receiveAddress for the wallet // We only need to persist the receiveAddress for the wallet
const walletFilter = createFilter('wallet', ['receiveAddress']); const walletFilter = createFilter('wallet', ['receiveAddress']);
const commentsFilter = createFilter('comments', ['commentChannel']);
const searchFilter = createFilter('search', ['options']); const searchFilter = createFilter('search', ['options']);
const tagsFilter = createFilter('tags', ['followedTags']); const tagsFilter = createFilter('tags', ['followedTags']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']); const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
@ -82,7 +82,6 @@ const whiteListedReducers = [
]; ];
const transforms = [ const transforms = [
commentsFilter,
fileInfoFilter, fileInfoFilter,
walletFilter, walletFilter,
blockedFilter, blockedFilter,