New moderation tools: block & mute (#5572)
* initial support for block/mute * hide blocked + muted content everywhere * add info message for blocked/muted characteristics * sort blocked list by most recent block first * add 'blocked' message on channel page for channels that you have blocked * cleanup * delete unused files * always pass mute/block list to claim_search on homepage * PR cleanup
This commit is contained in:
parent
277a1d5d1f
commit
ea74a66dbd
75 changed files with 1115 additions and 877 deletions
flow-typed
package.jsonui
comments.js
component
abandonedChannelPreview
app
blockButton
channelBlockButton
channelContent
channelCreate
channelEdit
channelForm
channelMuteButton
channelStakedIndicator
claimList
claimListDiscover
claimMenuList
claimPreview
claimPreviewTile
claimTilesDiscover
comment
commentMenuList
common
fileAuthor
header
notification
router
constants
modal
page
channel
channelNew
channels
channelsFollowingDiscover
listBlocked
settings
redux
scss
store.jsutil
4
flow-typed/Comment.js
vendored
4
flow-typed/Comment.js
vendored
|
@ -27,6 +27,10 @@ declare type CommentsState = {
|
||||||
myReactsByCommentId: any,
|
myReactsByCommentId: any,
|
||||||
othersReactsByCommentId: any,
|
othersReactsByCommentId: any,
|
||||||
pendingCommentReactions: Array<string>,
|
pendingCommentReactions: Array<string>,
|
||||||
|
moderationBlockList: ?Array<string>,
|
||||||
|
fetchingModerationBlockList: boolean,
|
||||||
|
blockingByUri: {},
|
||||||
|
unBlockingByUri: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type CommentReactParams = {
|
declare type CommentReactParams = {
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
"electron-updater": "^4.2.4",
|
"electron-updater": "^4.2.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"if-env": "^1.0.4",
|
"if-env": "^1.0.4",
|
||||||
"remove-markdown": "^0.3.0",
|
"remove-markdown": "^0.3.0",
|
||||||
"tempy": "^0.6.0",
|
"tempy": "^0.6.0",
|
||||||
"videojs-logo": "^2.1.4"
|
"videojs-logo": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
@ -184,7 +184,7 @@
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
"remark": "^9.0.0",
|
"remark": "^9.0.0",
|
||||||
"remark-attr": "^0.8.3",
|
"remark-attr": "^0.8.3",
|
||||||
"remark-breaks": "^1.0.5",
|
"remark-breaks": "^1.0.5",
|
||||||
"remark-emoji": "^2.0.1",
|
"remark-emoji": "^2.0.1",
|
||||||
"remark-frontmatter": "^2.0.0",
|
"remark-frontmatter": "^2.0.0",
|
||||||
"remark-react": "^8.0.0",
|
"remark-react": "^8.0.0",
|
||||||
|
|
|
@ -6,6 +6,8 @@ const Comments = {
|
||||||
enabled: Boolean(COMMENT_SERVER_API),
|
enabled: Boolean(COMMENT_SERVER_API),
|
||||||
|
|
||||||
moderation_block: (params: ModerationBlockParams) => fetchCommentsApi('moderation.Block', params),
|
moderation_block: (params: ModerationBlockParams) => fetchCommentsApi('moderation.Block', params),
|
||||||
|
moderation_unblock: (params: ModerationBlockParams) => fetchCommentsApi('moderation.UnBlock', params),
|
||||||
|
moderation_block_list: (params: ModerationBlockParams) => fetchCommentsApi('moderation.BlockedList', params),
|
||||||
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
|
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
|
||||||
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
||||||
};
|
};
|
||||||
|
@ -30,8 +32,8 @@ function fetchCommentsApi(method: string, params: {}) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch(url, options)
|
return fetch(url, options)
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(res => res.result);
|
.then((res) => res.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Comments;
|
export default Comments;
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
|
||||||
import { doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
|
||||||
import AbandonedChannelPreview from './view';
|
import AbandonedChannelPreview from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({});
|
||||||
blockedChannelUris: selectBlockedChannels(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select)(AbandonedChannelPreview);
|
||||||
doChannelUnsubscribe,
|
|
||||||
doOpenModal,
|
|
||||||
})(AbandonedChannelPreview);
|
|
||||||
|
|
|
@ -2,28 +2,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import Button from 'component/button';
|
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
import * as ICONS from '../../constants/icons';
|
import ChannelBlockButton from 'component/channelBlockButton';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import ChannelMuteButton from 'component/channelMuteButton';
|
||||||
|
|
||||||
type SubscriptionArgs = {
|
|
||||||
channelName: string,
|
|
||||||
uri: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
doChannelUnsubscribe: SubscriptionArgs => void,
|
|
||||||
type: string,
|
type: string,
|
||||||
blockedChannelUris: Array<string>,
|
|
||||||
doOpenModal: (string, {}) => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function AbandonedChannelPreview(props: Props) {
|
function AbandonedChannelPreview(props: Props) {
|
||||||
const { uri, doChannelUnsubscribe, type, blockedChannelUris, doOpenModal } = props;
|
const { uri, type } = props;
|
||||||
const { channelName } = parseURI(uri);
|
const { channelName } = parseURI(uri);
|
||||||
const isBlockedChannel = blockedChannelUris.includes(uri);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={classnames('claim-preview__wrapper', 'claim-preview__wrapper--notice')}>
|
<li className={classnames('claim-preview__wrapper', 'claim-preview__wrapper--notice')}>
|
||||||
|
@ -37,31 +27,10 @@ function AbandonedChannelPreview(props: Props) {
|
||||||
<div className="media__subtitle">{__(`This channel may have been unpublished.`)}</div>
|
<div className="media__subtitle">{__(`This channel may have been unpublished.`)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="claim-preview__actions">
|
<div className="claim-preview__actions">
|
||||||
{isBlockedChannel && (
|
<div className="section__actions">
|
||||||
<Button
|
<ChannelBlockButton uri={uri} />
|
||||||
iconColor="red"
|
<ChannelMuteButton uri={uri} />
|
||||||
icon={ICONS.UNBLOCK}
|
</div>
|
||||||
button={'alt'}
|
|
||||||
label={__('Unblock')}
|
|
||||||
onClick={() => doOpenModal(MODALS.REMOVE_BLOCKED, { blockedUri: uri })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* SubscribeButton uses resolved permanentUri; modifying it didn't seem worth it. */}
|
|
||||||
{!isBlockedChannel && (
|
|
||||||
<Button
|
|
||||||
iconColor="red"
|
|
||||||
icon={ICONS.UNSUBSCRIBE}
|
|
||||||
button={'alt'}
|
|
||||||
label={__('Unfollow')}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
doChannelUnsubscribe({
|
|
||||||
channelName: `@${channelName}`,
|
|
||||||
uri,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -27,9 +27,10 @@ import {
|
||||||
doSetActiveChannel,
|
doSetActiveChannel,
|
||||||
doSetIncognito,
|
doSetIncognito,
|
||||||
} from 'redux/actions/app';
|
} from 'redux/actions/app';
|
||||||
|
import { doFetchModBlockedList } from 'redux/actions/comments';
|
||||||
import App from './view';
|
import App from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
accessToken: selectAccessToken(state),
|
accessToken: selectAccessToken(state),
|
||||||
theme: selectThemePath(state),
|
theme: selectThemePath(state),
|
||||||
|
@ -48,18 +49,19 @@ const select = state => ({
|
||||||
myChannelUrls: selectMyChannelUrls(state),
|
myChannelUrls: selectMyChannelUrls(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
||||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||||
setLanguage: language => dispatch(doSetLanguage(language)),
|
setLanguage: (language) => dispatch(doSetLanguage(language)),
|
||||||
signIn: () => dispatch(doSignIn()),
|
signIn: () => dispatch(doSignIn()),
|
||||||
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
|
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
|
||||||
updatePreferences: () => dispatch(doGetAndPopulatePreferences()),
|
updatePreferences: () => dispatch(doGetAndPopulatePreferences()),
|
||||||
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()),
|
setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()),
|
||||||
setIncognito: () => dispatch(doSetIncognito()),
|
setIncognito: () => dispatch(doSetIncognito()),
|
||||||
|
fetchModBlockedList: () => dispatch(doFetchModBlockedList()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default hot(connect(select, perform)(App));
|
export default hot(connect(select, perform)(App));
|
||||||
|
|
|
@ -56,14 +56,14 @@ type Props = {
|
||||||
goForward: () => void,
|
goForward: () => void,
|
||||||
index: number,
|
index: number,
|
||||||
length: number,
|
length: number,
|
||||||
push: string => void,
|
push: (string) => void,
|
||||||
},
|
},
|
||||||
fetchAccessToken: () => void,
|
fetchAccessToken: () => void,
|
||||||
fetchChannelListMine: () => void,
|
fetchChannelListMine: () => void,
|
||||||
signIn: () => void,
|
signIn: () => void,
|
||||||
requestDownloadUpgrade: () => void,
|
requestDownloadUpgrade: () => void,
|
||||||
onSignedIn: () => void,
|
onSignedIn: () => void,
|
||||||
setLanguage: string => void,
|
setLanguage: (string) => void,
|
||||||
isUpgradeAvailable: boolean,
|
isUpgradeAvailable: boolean,
|
||||||
autoUpdateDownloaded: boolean,
|
autoUpdateDownloaded: boolean,
|
||||||
updatePreferences: () => Promise<any>,
|
updatePreferences: () => Promise<any>,
|
||||||
|
@ -83,7 +83,8 @@ type Props = {
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
myChannelUrls: ?Array<string>,
|
myChannelUrls: ?Array<string>,
|
||||||
setActiveChannelIfNotSet: () => void,
|
setActiveChannelIfNotSet: () => void,
|
||||||
setIncognito: boolean => void,
|
setIncognito: (boolean) => void,
|
||||||
|
fetchModBlockedList: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function App(props: Props) {
|
function App(props: Props) {
|
||||||
|
@ -114,6 +115,7 @@ function App(props: Props) {
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
setActiveChannelIfNotSet,
|
setActiveChannelIfNotSet,
|
||||||
setIncognito,
|
setIncognito,
|
||||||
|
fetchModBlockedList,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const appRef = useRef();
|
const appRef = useRef();
|
||||||
|
@ -134,7 +136,7 @@ function App(props: Props) {
|
||||||
const showUpgradeButton =
|
const showUpgradeButton =
|
||||||
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
|
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
|
||||||
// referral claiming
|
// referral claiming
|
||||||
const referredRewardAvailable = rewards && rewards.some(reward => reward.reward_type === REWARDS.TYPE_REFEREE);
|
const referredRewardAvailable = rewards && rewards.some((reward) => reward.reward_type === REWARDS.TYPE_REFEREE);
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const rawReferrerParam = urlParams.get('r');
|
const rawReferrerParam = urlParams.get('r');
|
||||||
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
|
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
|
||||||
|
@ -166,7 +168,7 @@ function App(props: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!uploadCount) return;
|
if (!uploadCount) return;
|
||||||
const handleBeforeUnload = event => {
|
const handleBeforeUnload = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.returnValue = 'magic'; // without setting this to something it doesn't work
|
event.returnValue = 'magic'; // without setting this to something it doesn't work
|
||||||
};
|
};
|
||||||
|
@ -176,7 +178,7 @@ function App(props: Props) {
|
||||||
|
|
||||||
// allows user to navigate history using the forward and backward buttons on a mouse
|
// allows user to navigate history using the forward and backward buttons on a mouse
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleForwardAndBackButtons = e => {
|
const handleForwardAndBackButtons = (e) => {
|
||||||
switch (e.button) {
|
switch (e.button) {
|
||||||
case MOUSE_BACK_BTN:
|
case MOUSE_BACK_BTN:
|
||||||
history.index > 0 && history.goBack();
|
history.index > 0 && history.goBack();
|
||||||
|
@ -192,7 +194,7 @@ function App(props: Props) {
|
||||||
|
|
||||||
// allows user to pause miniplayer using the spacebar without the page scrolling down
|
// allows user to pause miniplayer using the spacebar without the page scrolling down
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyPress = e => {
|
const handleKeyPress = (e) => {
|
||||||
if (e.key === ' ' && e.target === document.body) {
|
if (e.key === ' ' && e.target === document.body) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -244,6 +246,10 @@ function App(props: Props) {
|
||||||
} else if (hasNoChannels) {
|
} else if (hasNoChannels) {
|
||||||
setIncognito(true);
|
setIncognito(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasMyChannels) {
|
||||||
|
fetchModBlockedList();
|
||||||
|
}
|
||||||
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
|
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -358,7 +364,7 @@ function App(props: Props) {
|
||||||
[`${MAIN_WRAPPER_CLASS}--scrollbar`]: useCustomScrollbar,
|
[`${MAIN_WRAPPER_CLASS}--scrollbar`]: useCustomScrollbar,
|
||||||
})}
|
})}
|
||||||
ref={appRef}
|
ref={appRef}
|
||||||
onContextMenu={IS_WEB ? undefined : e => openContextMenu(e)}
|
onContextMenu={IS_WEB ? undefined : (e) => openContextMenu(e)}
|
||||||
>
|
>
|
||||||
{IS_WEB && lbryTvApiStatus === STATUS_DOWN ? (
|
{IS_WEB && lbryTvApiStatus === STATUS_DOWN ? (
|
||||||
<Yrbl
|
<Yrbl
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { makeSelectClaimIsMine, makeSelectShortUrlForUri, makeSelectPermanentUrlForUri } from 'lbry-redux';
|
|
||||||
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
import { doToggleBlockChannel } from 'redux/actions/blocked';
|
|
||||||
import BlockButton from './view';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
|
||||||
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
|
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
|
||||||
shortUrl: makeSelectShortUrlForUri(props.uri)(state),
|
|
||||||
permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, {
|
|
||||||
toggleBlockChannel: doToggleBlockChannel,
|
|
||||||
doToast,
|
|
||||||
})(BlockButton);
|
|
|
@ -1,49 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import useHover from 'effects/use-hover';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
permanentUrl: ?string,
|
|
||||||
shortUrl: string,
|
|
||||||
isSubscribed: boolean,
|
|
||||||
toggleBlockChannel: (uri: string) => void,
|
|
||||||
channelIsBlocked: boolean,
|
|
||||||
claimIsMine: boolean,
|
|
||||||
doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function BlockButton(props: Props) {
|
|
||||||
const { permanentUrl, shortUrl, toggleBlockChannel, channelIsBlocked, claimIsMine, doToast } = props;
|
|
||||||
|
|
||||||
const blockRef = useRef();
|
|
||||||
const isHovering = useHover(blockRef);
|
|
||||||
const blockLabel = channelIsBlocked ? __('Blocked') : __('Block');
|
|
||||||
const blockTitlePrefix = channelIsBlocked ? __('Unblock this channel') : __('Block this channel');
|
|
||||||
const blockedOverride = channelIsBlocked && isHovering && __('Unblock');
|
|
||||||
|
|
||||||
return permanentUrl && (!claimIsMine || channelIsBlocked) ? (
|
|
||||||
<Button
|
|
||||||
ref={blockRef}
|
|
||||||
icon={ICONS.BLOCK}
|
|
||||||
button={'alt'}
|
|
||||||
label={blockedOverride || blockLabel}
|
|
||||||
title={blockTitlePrefix}
|
|
||||||
requiresAuth={IS_WEB}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!channelIsBlocked) {
|
|
||||||
doToast({
|
|
||||||
message: __('Blocked %channelUrl%', { channelUrl: shortUrl }),
|
|
||||||
linkText: __('Manage'),
|
|
||||||
linkTarget: `/${PAGES.BLOCKED}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleBlockChannel(permanentUrl);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
}
|
|
14
ui/component/channelBlockButton/index.js
Normal file
14
ui/component/channelBlockButton/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doCommentModUnBlock, doCommentModBlock } from 'redux/actions/comments';
|
||||||
|
import { makeSelectChannelIsBlocked, makeSelectUriIsBlockingOrUnBlocking } from 'redux/selectors/comments';
|
||||||
|
import ChannelBlockButton from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
isBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
||||||
|
isBlockingOrUnBlocking: makeSelectUriIsBlockingOrUnBlocking(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doCommentModUnBlock,
|
||||||
|
doCommentModBlock,
|
||||||
|
})(ChannelBlockButton);
|
41
ui/component/channelBlockButton/view.jsx
Normal file
41
ui/component/channelBlockButton/view.jsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
isBlocked: boolean,
|
||||||
|
isBlockingOrUnBlocking: boolean,
|
||||||
|
doCommentModUnBlock: (string) => void,
|
||||||
|
doCommentModBlock: (string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ChannelBlockButton(props: Props) {
|
||||||
|
const { uri, doCommentModUnBlock, doCommentModBlock, isBlocked, isBlockingOrUnBlocking } = props;
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (isBlocked) {
|
||||||
|
doCommentModUnBlock(uri);
|
||||||
|
} else {
|
||||||
|
doCommentModBlock(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
button={isBlocked ? 'alt' : 'secondary'}
|
||||||
|
label={
|
||||||
|
isBlocked
|
||||||
|
? isBlockingOrUnBlocking
|
||||||
|
? __('Unblocking...')
|
||||||
|
: __('Unblock')
|
||||||
|
: isBlockingOrUnBlocking
|
||||||
|
? __('Blocking...')
|
||||||
|
: __('Block')
|
||||||
|
}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelBlockButton;
|
|
@ -8,7 +8,7 @@ import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
@ -24,7 +24,7 @@ const select = (state, props) => {
|
||||||
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
|
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
|
||||||
totalPages: makeSelectTotalPagesInChannelSearch(props.uri, PAGE_SIZE)(state),
|
totalPages: makeSelectTotalPagesInChannelSearch(props.uri, PAGE_SIZE)(state),
|
||||||
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
|
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
|
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
|
||||||
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
||||||
|
|
|
@ -29,6 +29,7 @@ type Props = {
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
showMature: boolean,
|
showMature: boolean,
|
||||||
tileLayout: boolean,
|
tileLayout: boolean,
|
||||||
|
viewBlockedChannel: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelContent(props: Props) {
|
function ChannelContent(props: Props) {
|
||||||
|
@ -44,6 +45,7 @@ function ChannelContent(props: Props) {
|
||||||
defaultInfiniteScroll = true,
|
defaultInfiniteScroll = true,
|
||||||
showMature,
|
showMature,
|
||||||
tileLayout,
|
tileLayout,
|
||||||
|
viewBlockedChannel,
|
||||||
} = props;
|
} = props;
|
||||||
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
const [searchQuery, setSearchQuery] = React.useState('');
|
||||||
|
@ -120,6 +122,7 @@ function ChannelContent(props: Props) {
|
||||||
|
|
||||||
{claim && claimsInChannel > 0 ? (
|
{claim && claimsInChannel > 0 ? (
|
||||||
<ClaimListDiscover
|
<ClaimListDiscover
|
||||||
|
showHiddenByUser={viewBlockedChannel}
|
||||||
forceShowReposts
|
forceShowReposts
|
||||||
tileLayout={tileLayout}
|
tileLayout={tileLayout}
|
||||||
uris={searchResults}
|
uris={searchResults}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import ChannelCreate from './view';
|
|
||||||
import { selectBalance, doCreateChannel, selectCreatingChannel, selectCreateChannelError } from 'lbry-redux';
|
|
||||||
|
|
||||||
const select = state => ({
|
|
||||||
balance: selectBalance(state),
|
|
||||||
creatingChannel: selectCreatingChannel(state),
|
|
||||||
createChannelError: selectCreateChannelError(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = dispatch => ({
|
|
||||||
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(ChannelCreate);
|
|
|
@ -1,149 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import { isNameValid } from 'lbry-redux';
|
|
||||||
import { Form, FormField } from 'component/common/form';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import analytics from 'analytics';
|
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
|
||||||
import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim';
|
|
||||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
balance: number,
|
|
||||||
createChannel: (string, number) => Promise<any>,
|
|
||||||
onSuccess?: ({}) => void,
|
|
||||||
creatingChannel: boolean,
|
|
||||||
createChannelError: ?string,
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
newChannelName: string,
|
|
||||||
newChannelBid: number,
|
|
||||||
newChannelNameError: string,
|
|
||||||
newChannelBidError: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChannelCreate extends React.PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
newChannelName: '',
|
|
||||||
newChannelBid: 0.001,
|
|
||||||
newChannelNameError: '',
|
|
||||||
newChannelBidError: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
(this: any).handleNewChannelNameChange = this.handleNewChannelNameChange.bind(this);
|
|
||||||
(this: any).handleNewChannelBidChange = this.handleNewChannelBidChange.bind(this);
|
|
||||||
(this: any).handleCreateChannel = this.handleCreateChannel.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewChannelNameChange(event: SyntheticInputEvent<*>) {
|
|
||||||
let newChannelName = event.target.value;
|
|
||||||
|
|
||||||
if (newChannelName.startsWith('@')) {
|
|
||||||
newChannelName = newChannelName.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let newChannelNameError;
|
|
||||||
if (newChannelName.length > 0 && !isNameValid(newChannelName, false)) {
|
|
||||||
newChannelNameError = INVALID_NAME_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
newChannelNameError,
|
|
||||||
newChannelName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewChannelBidChange(newChannelBid: number) {
|
|
||||||
const { balance } = this.props;
|
|
||||||
let newChannelBidError;
|
|
||||||
if (newChannelBid === 0) {
|
|
||||||
newChannelBidError = __('Your deposit cannot be 0');
|
|
||||||
} else if (newChannelBid === balance) {
|
|
||||||
newChannelBidError = __('Please decrease your deposit to account for transaction fees');
|
|
||||||
} else if (newChannelBid > balance) {
|
|
||||||
newChannelBidError = __('Deposit cannot be higher than your available balance');
|
|
||||||
} else if (newChannelBid < MINIMUM_PUBLISH_BID) {
|
|
||||||
newChannelBidError = __('Your deposit must be higher');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
newChannelBid,
|
|
||||||
newChannelBidError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCreateChannel() {
|
|
||||||
const { createChannel, onSuccess } = this.props;
|
|
||||||
const { newChannelBid, newChannelName } = this.state;
|
|
||||||
|
|
||||||
const channelName = `@${newChannelName.trim()}`;
|
|
||||||
|
|
||||||
const success = channelClaim => {
|
|
||||||
analytics.apiLogPublish(channelClaim);
|
|
||||||
|
|
||||||
if (onSuccess !== undefined) {
|
|
||||||
onSuccess({ ...this.props, ...this.state });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
createChannel(channelName, newChannelBid).then(success);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { newChannelName, newChannelNameError, newChannelBid, newChannelBidError } = this.state;
|
|
||||||
const { creatingChannel, createChannelError } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form onSubmit={this.handleCreateChannel}>
|
|
||||||
{createChannelError && <div className="error__text">{createChannelError}</div>}
|
|
||||||
<div>
|
|
||||||
<FormField
|
|
||||||
label={__('Name')}
|
|
||||||
name="channel-input"
|
|
||||||
type="text"
|
|
||||||
placeholder={__('ChannelName')}
|
|
||||||
error={newChannelNameError}
|
|
||||||
value={newChannelName}
|
|
||||||
onChange={this.handleNewChannelNameChange}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
className="form-field--price-amount"
|
|
||||||
name="channel-deposit"
|
|
||||||
label={<LbcSymbol postfix={__('Deposit')} size={14} />}
|
|
||||||
step="any"
|
|
||||||
min="0"
|
|
||||||
type="number"
|
|
||||||
helper={
|
|
||||||
<>
|
|
||||||
{__(
|
|
||||||
'These LBRY Credits remain yours. It is a deposit to reserve the name and can be undone at any time.'
|
|
||||||
)}
|
|
||||||
<WalletSpendableBalanceHelp inline />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
error={newChannelBidError}
|
|
||||||
value={newChannelBid}
|
|
||||||
onChange={event => this.handleNewChannelBidChange(parseFloat(event.target.value))}
|
|
||||||
onWheel={e => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
<div className="section__actions">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
button="primary"
|
|
||||||
label={!creatingChannel ? __('Create channel') : __('Creating channel...')}
|
|
||||||
disabled={
|
|
||||||
!newChannelName || !newChannelBid || creatingChannel || newChannelNameError || newChannelBidError
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChannelCreate;
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
doClearChannelErrors,
|
doClearChannelErrors,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
|
||||||
import ChannelPage from './view';
|
import ChannelPage from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
@ -38,12 +38,16 @@ const select = (state, props) => ({
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
updateChannel: params => dispatch(doUpdateChannel(params)),
|
updateChannel: (params) => dispatch(doUpdateChannel(params)),
|
||||||
createChannel: params => {
|
createChannel: (params) => {
|
||||||
const { name, amount, ...optionalParams } = params;
|
const { name, amount, ...optionalParams } = params;
|
||||||
return dispatch(doCreateChannel('@' + name, amount, optionalParams));
|
return dispatch(
|
||||||
|
doCreateChannel('@' + name, amount, optionalParams, (channelClaim) => {
|
||||||
|
dispatch(doUpdateBlockListForPublishedChannel(channelClaim));
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
clearChannelErrors: () => dispatch(doClearChannelErrors()),
|
clearChannelErrors: () => dispatch(doClearChannelErrors()),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import {
|
|
||||||
doResolveUri,
|
|
||||||
selectPublishFormValues,
|
|
||||||
selectIsStillEditing,
|
|
||||||
selectMyClaimForUri,
|
|
||||||
selectIsResolvingPublishUris,
|
|
||||||
selectTakeOverAmount,
|
|
||||||
doResetThumbnailStatus,
|
|
||||||
doClearPublish,
|
|
||||||
doUpdatePublishForm,
|
|
||||||
doPrepareEdit,
|
|
||||||
} from 'lbry-redux';
|
|
||||||
import { doPublishDesktop } from 'redux/actions/publish';
|
|
||||||
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
|
|
||||||
import ChannelForm from './view';
|
|
||||||
|
|
||||||
const select = state => ({
|
|
||||||
...selectPublishFormValues(state),
|
|
||||||
// The winning claim for a short lbry uri
|
|
||||||
amountNeededForTakeover: selectTakeOverAmount(state),
|
|
||||||
// My previously published claims under this short lbry uri
|
|
||||||
myClaimForUri: selectMyClaimForUri(state),
|
|
||||||
// If I clicked the "edit" button, have I changed the uri?
|
|
||||||
// Need this to make it easier to find the source on previously published content
|
|
||||||
isStillEditing: selectIsStillEditing(state),
|
|
||||||
isResolvingUri: selectIsResolvingPublishUris(state),
|
|
||||||
totalRewardValue: selectUnclaimedRewardValue(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = dispatch => ({
|
|
||||||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
|
||||||
clearPublish: () => dispatch(doClearPublish()),
|
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
|
||||||
publish: filePath => dispatch(doPublishDesktop(filePath)),
|
|
||||||
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
|
||||||
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(ChannelForm);
|
|
|
@ -1,63 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React, { useEffect, Fragment } from 'react';
|
|
||||||
import { CHANNEL_NEW } from 'constants/claim';
|
|
||||||
import { buildURI, isURIValid } from 'lbry-redux';
|
|
||||||
import ChannelCreate from 'component/channelCreate';
|
|
||||||
import Card from 'component/common/card';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
name: ?string,
|
|
||||||
channel: string,
|
|
||||||
resolveUri: string => void,
|
|
||||||
updatePublishForm: any => void,
|
|
||||||
onSuccess: () => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ChannelForm(props: Props) {
|
|
||||||
const { name, channel, resolveUri, updatePublishForm, onSuccess } = props;
|
|
||||||
|
|
||||||
// Every time the channel or name changes, resolve the uris to find winning bid amounts
|
|
||||||
useEffect(() => {
|
|
||||||
// If they are midway through a channel creation, treat it as anonymous until it completes
|
|
||||||
const channelName = 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
|
|
||||||
let uri;
|
|
||||||
try {
|
|
||||||
uri = name && buildURI({ streamName: name, channelName });
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
if (channelName && name) {
|
|
||||||
// resolve without the channel name so we know the winning bid for it
|
|
||||||
try {
|
|
||||||
const uriLessChannel = buildURI({ streamName: name });
|
|
||||||
resolveUri(uriLessChannel);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = isURIValid(uri);
|
|
||||||
if (uri && isValid) {
|
|
||||||
resolveUri(uri);
|
|
||||||
updatePublishForm({ uri });
|
|
||||||
}
|
|
||||||
}, [name, channel, resolveUri, updatePublishForm]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Card
|
|
||||||
icon={ICONS.CHANNEL}
|
|
||||||
title="Create a New Channel"
|
|
||||||
subtitle="This is a username or handle that your content can be found under."
|
|
||||||
actions={
|
|
||||||
<React.Fragment>
|
|
||||||
<ChannelCreate onSuccess={onSuccess} onChannelChange={channel => updatePublishForm({ channel })} />
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChannelForm;
|
|
12
ui/component/channelMuteButton/index.js
Normal file
12
ui/component/channelMuteButton/index.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToggleMuteChannel } from 'redux/actions/blocked';
|
||||||
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
|
import ChannelMuteButton from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
isMuted: makeSelectChannelIsMuted(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doToggleMuteChannel,
|
||||||
|
})(ChannelMuteButton);
|
24
ui/component/channelMuteButton/view.jsx
Normal file
24
ui/component/channelMuteButton/view.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
isMuted: boolean,
|
||||||
|
channelClaim: ?ChannelClaim,
|
||||||
|
doToggleMuteChannel: (string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ChannelBlockButton(props: Props) {
|
||||||
|
const { uri, doToggleMuteChannel, isMuted } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
button={isMuted ? 'alt' : 'secondary'}
|
||||||
|
label={isMuted ? __('Unmute') : __('Mute')}
|
||||||
|
onClick={() => doToggleMuteChannel(uri)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelBlockButton;
|
|
@ -11,6 +11,7 @@ import CreditAmount from 'component/common/credit-amount';
|
||||||
type Props = {
|
type Props = {
|
||||||
channelClaim: ChannelClaim,
|
channelClaim: ChannelClaim,
|
||||||
large?: boolean,
|
large?: boolean,
|
||||||
|
inline?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getChannelLevel(amount: number): number {
|
function getChannelLevel(amount: number): number {
|
||||||
|
@ -47,7 +48,7 @@ function getChannelIcon(level: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChannelStakedIndicator(props: Props) {
|
function ChannelStakedIndicator(props: Props) {
|
||||||
const { channelClaim, large = false } = props;
|
const { channelClaim, large = false, inline = false } = props;
|
||||||
|
|
||||||
if (!channelClaim || !channelClaim.meta) {
|
if (!channelClaim || !channelClaim.meta) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -79,6 +80,7 @@ function ChannelStakedIndicator(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classnames('channel-staked__wrapper', {
|
className={classnames('channel-staked__wrapper', {
|
||||||
'channel-staked__wrapper--large': large,
|
'channel-staked__wrapper--large': large,
|
||||||
|
'channel-staked__wrapper--inline': inline,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<LevelIcon icon={icon} large={large} isControlling={isControlling} />
|
<LevelIcon icon={icon} large={large} isControlling={isControlling} />
|
||||||
|
|
|
@ -32,12 +32,12 @@ type Props = {
|
||||||
showUnresolvedClaims?: boolean,
|
showUnresolvedClaims?: boolean,
|
||||||
renderProperties: ?(Claim) => Node,
|
renderProperties: ?(Claim) => Node,
|
||||||
includeSupportAction?: boolean,
|
includeSupportAction?: boolean,
|
||||||
hideBlock: boolean,
|
|
||||||
injectedItem: ?Node,
|
injectedItem: ?Node,
|
||||||
timedOutMessage?: Node,
|
timedOutMessage?: Node,
|
||||||
tileLayout?: boolean,
|
tileLayout?: boolean,
|
||||||
renderActions?: (Claim) => ?Node,
|
renderActions?: (Claim) => ?Node,
|
||||||
searchInLanguage: boolean,
|
searchInLanguage: boolean,
|
||||||
|
hideMenu?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ClaimList(props: Props) {
|
export default function ClaimList(props: Props) {
|
||||||
|
@ -57,12 +57,12 @@ export default function ClaimList(props: Props) {
|
||||||
showUnresolvedClaims,
|
showUnresolvedClaims,
|
||||||
renderProperties,
|
renderProperties,
|
||||||
includeSupportAction,
|
includeSupportAction,
|
||||||
hideBlock,
|
|
||||||
injectedItem,
|
injectedItem,
|
||||||
timedOutMessage,
|
timedOutMessage,
|
||||||
tileLayout = false,
|
tileLayout = false,
|
||||||
renderActions,
|
renderActions,
|
||||||
searchInLanguage,
|
searchInLanguage,
|
||||||
|
hideMenu,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||||
|
@ -100,7 +100,8 @@ export default function ClaimList(props: Props) {
|
||||||
|
|
||||||
return tileLayout && !header ? (
|
return tileLayout && !header ? (
|
||||||
<section className="claim-grid">
|
<section className="claim-grid">
|
||||||
{urisLength > 0 && uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} />)}
|
{urisLength > 0 &&
|
||||||
|
uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} showHiddenByUser={showHiddenByUser} />)}
|
||||||
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
||||||
{timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
|
{timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
|
||||||
</section>
|
</section>
|
||||||
|
@ -149,12 +150,13 @@ export default function ClaimList(props: Props) {
|
||||||
<ClaimPreview
|
<ClaimPreview
|
||||||
uri={uri}
|
uri={uri}
|
||||||
type={type}
|
type={type}
|
||||||
|
hideMenu={hideMenu}
|
||||||
includeSupportAction={includeSupportAction}
|
includeSupportAction={includeSupportAction}
|
||||||
showUnresolvedClaim={showUnresolvedClaims}
|
showUnresolvedClaim={showUnresolvedClaims}
|
||||||
properties={renderProperties || (type !== 'small' ? undefined : false)}
|
properties={renderProperties || (type !== 'small' ? undefined : false)}
|
||||||
renderActions={renderActions}
|
renderActions={renderActions}
|
||||||
showUserBlocked={showHiddenByUser}
|
showUserBlocked={showHiddenByUser}
|
||||||
hideBlock={hideBlock}
|
showHiddenByUser={showHiddenByUser}
|
||||||
customShouldHide={(claim: StreamClaim) => {
|
customShouldHide={(claim: StreamClaim) => {
|
||||||
// Hack to hide spee.ch thumbnail publishes
|
// Hack to hide spee.ch thumbnail publishes
|
||||||
// If it meets these requirements, it was probably uploaded here:
|
// If it meets these requirements, it was probably uploaded here:
|
||||||
|
|
|
@ -7,12 +7,13 @@ import {
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||||
import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings';
|
||||||
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import ClaimListDiscover from './view';
|
import ClaimListDiscover from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
followedTags: selectFollowedTags(state),
|
followedTags: selectFollowedTags(state),
|
||||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
claimSearchByQuery: selectClaimSearchByQuery(state),
|
||||||
claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state),
|
claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state),
|
||||||
|
@ -20,7 +21,8 @@ const select = state => ({
|
||||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
||||||
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||||
languageSetting: selectLanguage(state),
|
languageSetting: selectLanguage(state),
|
||||||
hiddenUris: selectBlockedChannels(state),
|
mutedUris: selectMutedChannels(state),
|
||||||
|
blockedUris: selectModerationBlockList(state),
|
||||||
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
|
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,17 +20,18 @@ type Props = {
|
||||||
doClaimSearch: ({}) => void,
|
doClaimSearch: ({}) => void,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
personalView: boolean,
|
personalView: boolean,
|
||||||
doToggleTagFollowDesktop: string => void,
|
doToggleTagFollowDesktop: (string) => void,
|
||||||
meta?: Node,
|
meta?: Node,
|
||||||
showNsfw: boolean,
|
showNsfw: boolean,
|
||||||
hideReposts: boolean,
|
hideReposts: boolean,
|
||||||
history: { action: string, push: string => void, replace: string => void },
|
history: { action: string, push: (string) => void, replace: (string) => void },
|
||||||
location: { search: string, pathname: string },
|
location: { search: string, pathname: string },
|
||||||
claimSearchByQuery: {
|
claimSearchByQuery: {
|
||||||
[string]: Array<string>,
|
[string]: Array<string>,
|
||||||
},
|
},
|
||||||
claimSearchByQueryLastPageReached: { [string]: boolean },
|
claimSearchByQueryLastPageReached: { [string]: boolean },
|
||||||
hiddenUris: Array<string>,
|
mutedUris: Array<string>,
|
||||||
|
blockedUris: Array<string>,
|
||||||
hiddenNsfwMessage?: Node,
|
hiddenNsfwMessage?: Node,
|
||||||
channelIds?: Array<string>,
|
channelIds?: Array<string>,
|
||||||
claimIds?: Array<string>,
|
claimIds?: Array<string>,
|
||||||
|
@ -43,13 +44,12 @@ type Props = {
|
||||||
header?: Node,
|
header?: Node,
|
||||||
headerLabel?: string | Node,
|
headerLabel?: string | Node,
|
||||||
name?: string,
|
name?: string,
|
||||||
hideBlock?: boolean,
|
|
||||||
hideAdvancedFilter?: boolean,
|
hideAdvancedFilter?: boolean,
|
||||||
claimType?: Array<string>,
|
claimType?: Array<string>,
|
||||||
defaultClaimType?: Array<string>,
|
defaultClaimType?: Array<string>,
|
||||||
streamType?: string | Array<string>,
|
streamType?: string | Array<string>,
|
||||||
defaultStreamType?: string | Array<string>,
|
defaultStreamType?: string | Array<string>,
|
||||||
renderProperties?: Claim => Node,
|
renderProperties?: (Claim) => Node,
|
||||||
includeSupportAction?: boolean,
|
includeSupportAction?: boolean,
|
||||||
repostedClaimId?: string,
|
repostedClaimId?: string,
|
||||||
pageSize?: number,
|
pageSize?: number,
|
||||||
|
@ -64,6 +64,7 @@ type Props = {
|
||||||
languageSetting: string,
|
languageSetting: string,
|
||||||
searchInLanguage: boolean,
|
searchInLanguage: boolean,
|
||||||
scrollAnchor?: string,
|
scrollAnchor?: string,
|
||||||
|
showHiddenByUser?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimListDiscover(props: Props) {
|
function ClaimListDiscover(props: Props) {
|
||||||
|
@ -80,7 +81,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
hideReposts,
|
hideReposts,
|
||||||
history,
|
history,
|
||||||
location,
|
location,
|
||||||
hiddenUris,
|
mutedUris,
|
||||||
|
blockedUris,
|
||||||
hiddenNsfwMessage,
|
hiddenNsfwMessage,
|
||||||
defaultOrderBy,
|
defaultOrderBy,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
@ -89,7 +91,6 @@ function ClaimListDiscover(props: Props) {
|
||||||
name,
|
name,
|
||||||
claimType,
|
claimType,
|
||||||
pageSize,
|
pageSize,
|
||||||
hideBlock,
|
|
||||||
defaultClaimType,
|
defaultClaimType,
|
||||||
streamType,
|
streamType,
|
||||||
defaultStreamType,
|
defaultStreamType,
|
||||||
|
@ -112,6 +113,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
languageSetting,
|
languageSetting,
|
||||||
searchInLanguage,
|
searchInLanguage,
|
||||||
scrollAnchor,
|
scrollAnchor,
|
||||||
|
showHiddenByUser = false,
|
||||||
} = props;
|
} = props;
|
||||||
const didNavigateForward = history.action === 'PUSH';
|
const didNavigateForward = history.action === 'PUSH';
|
||||||
const { search } = location;
|
const { search } = location;
|
||||||
|
@ -120,13 +122,14 @@ function ClaimListDiscover(props: Props) {
|
||||||
const isLargeScreen = useIsLargeScreen();
|
const isLargeScreen = useIsLargeScreen();
|
||||||
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
|
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
|
||||||
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
|
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
|
||||||
const followed = (followedTags && followedTags.map(t => t.name)) || [];
|
const followed = (followedTags && followedTags.map((t) => t.name)) || [];
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
|
const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
|
||||||
(tags && getParamFromTags(tags)) ||
|
(tags && getParamFromTags(tags)) ||
|
||||||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
|
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
|
||||||
(defaultTags && getParamFromTags(defaultTags));
|
(defaultTags && getParamFromTags(defaultTags));
|
||||||
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
|
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
|
||||||
|
const mutedAndBlockedChannelIds = Array.from(new Set(mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1])));
|
||||||
|
|
||||||
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
|
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
|
||||||
const languageParams = searchInLanguage
|
const languageParams = searchInLanguage
|
||||||
|
@ -206,7 +209,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
no_totals: true,
|
no_totals: true,
|
||||||
not_channel_ids:
|
not_channel_ids:
|
||||||
// If channelIdsParam were passed in, we don't need not_channel_ids
|
// If channelIdsParam were passed in, we don't need not_channel_ids
|
||||||
!channelIdsParam && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
|
!channelIdsParam ? mutedAndBlockedChannelIds : [],
|
||||||
not_tags: !showNsfw ? MATURE_TAGS : [],
|
not_tags: !showNsfw ? MATURE_TAGS : [],
|
||||||
order_by:
|
order_by:
|
||||||
orderParam === CS.ORDER_BY_TRENDING
|
orderParam === CS.ORDER_BY_TRENDING
|
||||||
|
@ -247,12 +250,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
|
|
||||||
if (claimType !== CS.CLAIM_CHANNEL) {
|
if (claimType !== CS.CLAIM_CHANNEL) {
|
||||||
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
|
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
|
||||||
options.release_time = `>${Math.floor(
|
options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`;
|
||||||
moment()
|
|
||||||
.subtract(1, freshnessParam)
|
|
||||||
.startOf('hour')
|
|
||||||
.unix()
|
|
||||||
)}`;
|
|
||||||
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
|
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
|
||||||
// Warning - hack below
|
// Warning - hack below
|
||||||
// If users are following more than 10 channels or tags, limit results to stuff less than a year old
|
// If users are following more than 10 channels or tags, limit results to stuff less than a year old
|
||||||
|
@ -263,29 +261,15 @@ function ClaimListDiscover(props: Props) {
|
||||||
(options.channel_ids && options.channel_ids.length > 20) ||
|
(options.channel_ids && options.channel_ids.length > 20) ||
|
||||||
(options.any_tags && options.any_tags.length > 20)
|
(options.any_tags && options.any_tags.length > 20)
|
||||||
) {
|
) {
|
||||||
options.release_time = `>${Math.floor(
|
options.release_time = `>${Math.floor(moment().subtract(3, CS.FRESH_MONTH).startOf('week').unix())}`;
|
||||||
moment()
|
|
||||||
.subtract(3, CS.FRESH_MONTH)
|
|
||||||
.startOf('week')
|
|
||||||
.unix()
|
|
||||||
)}`;
|
|
||||||
} else if (
|
} else if (
|
||||||
(options.channel_ids && options.channel_ids.length > 10) ||
|
(options.channel_ids && options.channel_ids.length > 10) ||
|
||||||
(options.any_tags && options.any_tags.length > 10)
|
(options.any_tags && options.any_tags.length > 10)
|
||||||
) {
|
) {
|
||||||
options.release_time = `>${Math.floor(
|
options.release_time = `>${Math.floor(moment().subtract(1, CS.FRESH_YEAR).startOf('week').unix())}`;
|
||||||
moment()
|
|
||||||
.subtract(1, CS.FRESH_YEAR)
|
|
||||||
.startOf('week')
|
|
||||||
.unix()
|
|
||||||
)}`;
|
|
||||||
} else {
|
} else {
|
||||||
// Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
|
// Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
|
||||||
options.release_time = `<${Math.floor(
|
options.release_time = `<${Math.floor(moment().startOf('minute').unix())}`;
|
||||||
moment()
|
|
||||||
.startOf('minute')
|
|
||||||
.unix()
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,14 +317,14 @@ function ClaimListDiscover(props: Props) {
|
||||||
if (hideReposts && !options.reposted_claim_id && !forceShowReposts) {
|
if (hideReposts && !options.reposted_claim_id && !forceShowReposts) {
|
||||||
if (Array.isArray(options.claim_type)) {
|
if (Array.isArray(options.claim_type)) {
|
||||||
if (options.claim_type.length > 1) {
|
if (options.claim_type.length > 1) {
|
||||||
options.claim_type = options.claim_type.filter(claimType => claimType !== 'repost');
|
options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
options.claim_type = ['stream', 'channel'];
|
options.claim_type = ['stream', 'channel'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasMatureTags = tagsParam && tagsParam.split(',').some(t => MATURE_TAGS.includes(t));
|
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
|
||||||
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
|
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
|
||||||
const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
|
const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
|
||||||
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
|
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
|
||||||
|
@ -499,8 +483,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
timedOutMessage={timedOutMessage}
|
timedOutMessage={timedOutMessage}
|
||||||
renderProperties={renderProperties}
|
renderProperties={renderProperties}
|
||||||
includeSupportAction={includeSupportAction}
|
includeSupportAction={includeSupportAction}
|
||||||
hideBlock={hideBlock}
|
|
||||||
injectedItem={injectedItem}
|
injectedItem={injectedItem}
|
||||||
|
showHiddenByUser={showHiddenByUser}
|
||||||
/>
|
/>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="claim-grid">
|
<div className="claim-grid">
|
||||||
|
@ -527,8 +511,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
timedOutMessage={timedOutMessage}
|
timedOutMessage={timedOutMessage}
|
||||||
renderProperties={renderProperties}
|
renderProperties={renderProperties}
|
||||||
includeSupportAction={includeSupportAction}
|
includeSupportAction={includeSupportAction}
|
||||||
hideBlock={hideBlock}
|
|
||||||
injectedItem={injectedItem}
|
injectedItem={injectedItem}
|
||||||
|
showHiddenByUser={showHiddenByUser}
|
||||||
/>
|
/>
|
||||||
{loading && new Array(dynamicPageSize).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
|
{loading && new Array(dynamicPageSize).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
|
||||||
</div>
|
</div>
|
||||||
|
|
20
ui/component/claimMenuList/index.js
Normal file
20
ui/component/claimMenuList/index.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectClaimForUri, makeSelectClaimIsMine } from 'lbry-redux';
|
||||||
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
|
import { doToggleMuteChannel } from 'redux/actions/blocked';
|
||||||
|
import { doCommentModBlock, doCommentModUnBlock } from 'redux/actions/comments';
|
||||||
|
import { makeSelectChannelIsBlocked } from 'redux/selectors/comments';
|
||||||
|
import ClaimPreview from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
channelIsMuted: makeSelectChannelIsMuted(props.uri)(state),
|
||||||
|
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doToggleMuteChannel,
|
||||||
|
doCommentModBlock,
|
||||||
|
doCommentModUnBlock,
|
||||||
|
})(ClaimPreview);
|
86
ui/component/claimMenuList/view.jsx
Normal file
86
ui/component/claimMenuList/view.jsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
claim: ?Claim,
|
||||||
|
inline?: boolean,
|
||||||
|
claimIsMine: boolean,
|
||||||
|
channelIsMuted: boolean,
|
||||||
|
channelIsBlocked: boolean,
|
||||||
|
doToggleMuteChannel: (string) => void,
|
||||||
|
doCommentModBlock: (string) => void,
|
||||||
|
doCommentModUnBlock: (string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ClaimMenuList(props: Props) {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
inline = false,
|
||||||
|
claimIsMine,
|
||||||
|
doToggleMuteChannel,
|
||||||
|
channelIsMuted,
|
||||||
|
channelIsBlocked,
|
||||||
|
doCommentModBlock,
|
||||||
|
doCommentModUnBlock,
|
||||||
|
} = props;
|
||||||
|
const channelUri =
|
||||||
|
claim &&
|
||||||
|
(claim.value_type === 'channel'
|
||||||
|
? claim.permanent_url
|
||||||
|
: claim.signing_channel && claim.signing_channel.permanent_url);
|
||||||
|
|
||||||
|
if (!channelUri || claimIsMine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleMute() {
|
||||||
|
doToggleMuteChannel(channelUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleBlock() {
|
||||||
|
if (channelIsBlocked) {
|
||||||
|
doCommentModUnBlock(channelUri);
|
||||||
|
} else {
|
||||||
|
doCommentModBlock(channelUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
className={classnames('menu__button', { 'claim__menu-button': !inline, 'claim__menu-button--inline': inline })}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={20} icon={ICONS.MORE_VERTICAL} />
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList className="menu__list">
|
||||||
|
{!claimIsMine && (
|
||||||
|
<>
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleToggleBlock}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.BLOCK} />
|
||||||
|
{channelIsBlocked ? __('Unblock Channel') : __('Block Channel')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleToggleMute}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.MUTE} />
|
||||||
|
{channelIsMuted ? __('Unmute Channel') : __('Mute Channel')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClaimMenuList;
|
|
@ -11,11 +11,12 @@ import {
|
||||||
makeSelectClaimWasPurchased,
|
makeSelectClaimWasPurchased,
|
||||||
makeSelectStreamingUrlForUri,
|
makeSelectStreamingUrlForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectBlockedChannels, selectChannelIsBlocked } from 'redux/selectors/blocked';
|
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import ClaimPreview from './view';
|
import ClaimPreview from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
@ -30,17 +31,18 @@ const select = (state, props) => ({
|
||||||
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
|
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
filteredOutpoints: selectFilteredOutpoints(state),
|
filteredOutpoints: selectFilteredOutpoints(state),
|
||||||
blockedChannelUris: selectBlockedChannels(state),
|
mutedUris: selectMutedChannels(state),
|
||||||
|
blockedUris: selectModerationBlockList(state),
|
||||||
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
|
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
|
||||||
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
|
channelIsBlocked: props.uri && makeSelectChannelIsMuted(props.uri)(state),
|
||||||
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
|
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
|
||||||
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
|
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
|
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
getFile: uri => dispatch(doFileGet(uri, false)),
|
getFile: (uri) => dispatch(doFileGet(uri, false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(ClaimPreview);
|
export default connect(select, perform)(ClaimPreview);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type { Node } from 'react';
|
||||||
import React, { useEffect, forwardRef } from 'react';
|
import React, { useEffect, forwardRef } from 'react';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { SIMPLE_SITE } from 'config';
|
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
import { openClaimPreviewMenu } from 'util/context-menu';
|
import { openClaimPreviewMenu } from 'util/context-menu';
|
||||||
|
@ -16,7 +15,6 @@ import FileProperties from 'component/fileProperties';
|
||||||
import ClaimTags from 'component/claimTags';
|
import ClaimTags from 'component/claimTags';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import BlockButton from 'component/blockButton';
|
|
||||||
import ClaimSupportButton from 'component/claimSupportButton';
|
import ClaimSupportButton from 'component/claimSupportButton';
|
||||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||||
import ClaimPreviewTitle from 'component/claimPreviewTitle';
|
import ClaimPreviewTitle from 'component/claimPreviewTitle';
|
||||||
|
@ -25,6 +23,7 @@ import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
import AbandonedChannelPreview from 'component/abandonedChannelPreview';
|
import AbandonedChannelPreview from 'component/abandonedChannelPreview';
|
||||||
import PublishPending from 'component/publishPending';
|
import PublishPending from 'component/publishPending';
|
||||||
|
import ClaimMenuList from 'component/claimMenuList';
|
||||||
import ClaimPreviewLoading from './claim-preview-loading';
|
import ClaimPreviewLoading from './claim-preview-loading';
|
||||||
import ClaimPreviewHidden from './claim-preview-no-mature';
|
import ClaimPreviewHidden from './claim-preview-no-mature';
|
||||||
import ClaimPreviewNoContent from './claim-preview-no-content';
|
import ClaimPreviewNoContent from './claim-preview-no-content';
|
||||||
|
@ -53,14 +52,13 @@ type Props = {
|
||||||
txid: string,
|
txid: string,
|
||||||
nout: number,
|
nout: number,
|
||||||
}>,
|
}>,
|
||||||
blockedChannelUris: Array<string>,
|
mutedUris: Array<string>,
|
||||||
|
blockedUris: Array<string>,
|
||||||
channelIsBlocked: boolean,
|
channelIsBlocked: boolean,
|
||||||
isSubscribed: boolean,
|
|
||||||
actions: boolean | Node | string | number,
|
actions: boolean | Node | string | number,
|
||||||
properties: boolean | Node | string | number | ((Claim) => Node),
|
properties: boolean | Node | string | number | ((Claim) => Node),
|
||||||
empty?: Node,
|
empty?: Node,
|
||||||
onClick?: (any) => any,
|
onClick?: (any) => any,
|
||||||
hideBlock?: boolean,
|
|
||||||
streamingUrl: ?string,
|
streamingUrl: ?string,
|
||||||
getFile: (string) => void,
|
getFile: (string) => void,
|
||||||
customShouldHide?: (Claim) => boolean,
|
customShouldHide?: (Claim) => boolean,
|
||||||
|
@ -72,6 +70,7 @@ type Props = {
|
||||||
wrapperElement?: string,
|
wrapperElement?: string,
|
||||||
hideRepostLabel?: boolean,
|
hideRepostLabel?: boolean,
|
||||||
repostUrl?: string,
|
repostUrl?: string,
|
||||||
|
hideMenu?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
@ -87,7 +86,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
// is the claim consider nsfw?
|
// is the claim consider nsfw?
|
||||||
nsfw,
|
nsfw,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
isSubscribed,
|
|
||||||
streamingUrl,
|
streamingUrl,
|
||||||
// user properties
|
// user properties
|
||||||
channelIsBlocked,
|
channelIsBlocked,
|
||||||
|
@ -113,13 +111,14 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
hideActions = false,
|
hideActions = false,
|
||||||
properties,
|
properties,
|
||||||
onClick,
|
onClick,
|
||||||
hideBlock,
|
|
||||||
actions,
|
actions,
|
||||||
blockedChannelUris,
|
mutedUris,
|
||||||
|
blockedUris,
|
||||||
blackListedOutpoints,
|
blackListedOutpoints,
|
||||||
filteredOutpoints,
|
filteredOutpoints,
|
||||||
includeSupportAction,
|
includeSupportAction,
|
||||||
renderActions,
|
renderActions,
|
||||||
|
hideMenu = false,
|
||||||
// repostUrl,
|
// repostUrl,
|
||||||
} = props;
|
} = props;
|
||||||
const WrapperElement = wrapperElement || 'li';
|
const WrapperElement = wrapperElement || 'li';
|
||||||
|
@ -172,13 +171,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// block stream claims
|
// block stream claims
|
||||||
if (claim && !shouldHide && !showUserBlocked && blockedChannelUris.length && signingChannel) {
|
if (claim && !shouldHide && !showUserBlocked && mutedUris.length && signingChannel) {
|
||||||
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
shouldHide = mutedUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
||||||
}
|
}
|
||||||
// block channel claims if we can't control for them in claim search
|
if (claim && !shouldHide && !showUserBlocked && blockedUris.length && signingChannel) {
|
||||||
// e.g. fetchRecommendedSubscriptions
|
shouldHide = blockedUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
||||||
if (claim && isChannelUri && !shouldHide && !showUserBlocked && blockedChannelUris.length) {
|
|
||||||
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldHide && customShouldHide && claim) {
|
if (!shouldHide && customShouldHide && claim) {
|
||||||
|
@ -275,7 +272,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
>
|
>
|
||||||
{isChannelUri && claim ? (
|
{isChannelUri && claim ? (
|
||||||
<UriIndicator uri={contentUri} link>
|
<UriIndicator uri={contentUri} link>
|
||||||
<ChannelThumbnail uri={contentUri} obscure={channelIsBlocked} />
|
<ChannelThumbnail uri={contentUri} />
|
||||||
</UriIndicator>
|
</UriIndicator>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -313,9 +310,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type !== 'small' && !isChannelUri && signingChannel && SIMPLE_SITE && (
|
{/* {type !== 'small' && !isChannelUri && signingChannel && SIMPLE_SITE && (
|
||||||
<ChannelThumbnail uri={signingChannel.permanent_url} />
|
<ChannelThumbnail uri={signingChannel.permanent_url} />
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
<ClaimPreviewSubtitle uri={uri} type={type} />
|
<ClaimPreviewSubtitle uri={uri} type={type} />
|
||||||
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
||||||
|
@ -329,12 +326,16 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
actions
|
actions
|
||||||
) : (
|
) : (
|
||||||
<div className="claim-preview__primary-actions">
|
<div className="claim-preview__primary-actions">
|
||||||
|
{!isChannelUri && signingChannel && (
|
||||||
|
<div className="claim-preview__channel-staked">
|
||||||
|
<ChannelThumbnail uri={signingChannel.permanent_url} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isChannelUri && !channelIsBlocked && !claimIsMine && (
|
{isChannelUri && !channelIsBlocked && !claimIsMine && (
|
||||||
<SubscribeButton uri={contentUri.startsWith('lbry://') ? contentUri : `lbry://${contentUri}`} />
|
<SubscribeButton uri={contentUri.startsWith('lbry://') ? contentUri : `lbry://${contentUri}`} />
|
||||||
)}
|
)}
|
||||||
{!hideBlock && isChannelUri && !isSubscribed && (!claimIsMine || channelIsBlocked) && (
|
|
||||||
<BlockButton uri={contentUri.startsWith('lbry://') ? contentUri : `lbry://${contentUri}`} />
|
|
||||||
)}
|
|
||||||
{includeSupportAction && <ClaimSupportButton uri={uri} />}
|
{includeSupportAction && <ClaimSupportButton uri={uri} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -355,6 +356,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!hideMenu && <ClaimMenuList uri={uri} />}
|
||||||
</WrapperElement>
|
</WrapperElement>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
makeSelectChannelForClaimUri,
|
makeSelectChannelForClaimUri,
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import ClaimPreviewTile from './view';
|
import ClaimPreviewTile from './view';
|
||||||
|
@ -22,14 +22,14 @@ const select = (state, props) => ({
|
||||||
title: props.uri && makeSelectTitleForUri(props.uri)(state),
|
title: props.uri && makeSelectTitleForUri(props.uri)(state),
|
||||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
filteredOutpoints: selectFilteredOutpoints(state),
|
filteredOutpoints: selectFilteredOutpoints(state),
|
||||||
blockedChannelUris: selectBlockedChannels(state),
|
blockedChannelUris: selectMutedChannels(state),
|
||||||
showMature: selectShowMatureContent(state),
|
showMature: selectShowMatureContent(state),
|
||||||
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
getFile: uri => dispatch(doFileGet(uri, false)),
|
getFile: (uri) => dispatch(doFileGet(uri, false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(ClaimPreviewTile);
|
export default connect(select, perform)(ClaimPreviewTile);
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { parseURI } from 'lbry-redux';
|
||||||
import FileProperties from 'component/fileProperties';
|
import FileProperties from 'component/fileProperties';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
||||||
|
import ClaimMenuList from 'component/claimMenuList';
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
import { openClaimPreviewMenu } from 'util/context-menu';
|
import { openClaimPreviewMenu } from 'util/context-menu';
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -41,6 +43,7 @@ type Props = {
|
||||||
streamingUrl: string,
|
streamingUrl: string,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
showMature: boolean,
|
showMature: boolean,
|
||||||
|
showHiddenByUser?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimPreviewTile(props: Props) {
|
function ClaimPreviewTile(props: Props) {
|
||||||
|
@ -60,6 +63,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
blockedChannelUris,
|
blockedChannelUris,
|
||||||
isMature,
|
isMature,
|
||||||
showMature,
|
showMature,
|
||||||
|
showHiddenByUser,
|
||||||
} = props;
|
} = props;
|
||||||
const isRepost = claim && claim.repost_channel_url;
|
const isRepost = claim && claim.repost_channel_url;
|
||||||
const shouldFetch = claim === undefined;
|
const shouldFetch = claim === undefined;
|
||||||
|
@ -128,12 +132,12 @@ function ClaimPreviewTile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// block stream claims
|
// block stream claims
|
||||||
if (claim && !shouldHide && blockedChannelUris.length && signingChannel) {
|
if (claim && !shouldHide && !showHiddenByUser && blockedChannelUris.length && signingChannel) {
|
||||||
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
||||||
}
|
}
|
||||||
// block channel claims if we can't control for them in claim search
|
// block channel claims if we can't control for them in claim search
|
||||||
// e.g. fetchRecommendedSubscriptions
|
// e.g. fetchRecommendedSubscriptions
|
||||||
if (claim && isChannel && !shouldHide && blockedChannelUris.length) {
|
if (claim && isChannel && !shouldHide && !showHiddenByUser && blockedChannelUris.length) {
|
||||||
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url);
|
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +198,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
<UriIndicator uri={uri} link />
|
<UriIndicator uri={uri} link />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<ClaimMenuList uri={uri} />
|
||||||
</h2>
|
</h2>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doClaimSearch, selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, SETTINGS } from 'lbry-redux';
|
import { doClaimSearch, selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, SETTINGS } from 'lbry-redux';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import ClaimListDiscover from './view';
|
import ClaimListDiscover from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
claimSearchByQuery: selectClaimSearchByQuery(state),
|
||||||
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
|
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
|
||||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
|
||||||
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||||
hiddenUris: selectBlockedChannels(state),
|
mutedUris: selectMutedChannels(state),
|
||||||
|
blockedUris: selectModerationBlockList(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = {
|
const perform = {
|
||||||
|
|
|
@ -10,7 +10,7 @@ type Props = {
|
||||||
doClaimSearch: ({}) => void,
|
doClaimSearch: ({}) => void,
|
||||||
showNsfw: boolean,
|
showNsfw: boolean,
|
||||||
hideReposts: boolean,
|
hideReposts: boolean,
|
||||||
history: { action: string, push: string => void, replace: string => void },
|
history: { action: string, push: (string) => void, replace: (string) => void },
|
||||||
claimSearchByQuery: {
|
claimSearchByQuery: {
|
||||||
[string]: Array<string>,
|
[string]: Array<string>,
|
||||||
},
|
},
|
||||||
|
@ -19,10 +19,10 @@ type Props = {
|
||||||
},
|
},
|
||||||
// claim search options are below
|
// claim search options are below
|
||||||
tags: Array<string>,
|
tags: Array<string>,
|
||||||
hiddenUris: Array<string>,
|
blockedUris: Array<string>,
|
||||||
|
mutedUris: Array<string>,
|
||||||
claimIds?: Array<string>,
|
claimIds?: Array<string>,
|
||||||
channelIds?: Array<string>,
|
channelIds?: Array<string>,
|
||||||
notChannelIds?: Array<string>,
|
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
orderBy?: Array<string>,
|
orderBy?: Array<string>,
|
||||||
releaseTime?: string,
|
releaseTime?: string,
|
||||||
|
@ -39,12 +39,12 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
claimSearchByQuery,
|
claimSearchByQuery,
|
||||||
showNsfw,
|
showNsfw,
|
||||||
hideReposts,
|
hideReposts,
|
||||||
hiddenUris,
|
blockedUris,
|
||||||
|
mutedUris,
|
||||||
// Below are options to pass that are forwarded to claim_search
|
// Below are options to pass that are forwarded to claim_search
|
||||||
tags,
|
tags,
|
||||||
channelIds,
|
channelIds,
|
||||||
claimIds,
|
claimIds,
|
||||||
notChannelIds,
|
|
||||||
orderBy,
|
orderBy,
|
||||||
pageSize = 8,
|
pageSize = 8,
|
||||||
releaseTime,
|
releaseTime,
|
||||||
|
@ -60,6 +60,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
const urlParams = new URLSearchParams(location.search);
|
const urlParams = new URLSearchParams(location.search);
|
||||||
const feeAmountInUrl = urlParams.get('fee_amount');
|
const feeAmountInUrl = urlParams.get('fee_amount');
|
||||||
const feeAmountParam = feeAmountInUrl || feeAmount;
|
const feeAmountParam = feeAmountInUrl || feeAmount;
|
||||||
|
const mutedAndBlockedChannelIds = Array.from(new Set(mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1])));
|
||||||
const options: {
|
const options: {
|
||||||
page_size: number,
|
page_size: number,
|
||||||
no_totals: boolean,
|
no_totals: boolean,
|
||||||
|
@ -86,10 +87,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
not_tags: !showNsfw ? MATURE_TAGS : [],
|
not_tags: !showNsfw ? MATURE_TAGS : [],
|
||||||
any_languages: languages,
|
any_languages: languages,
|
||||||
channel_ids: channelIds || [],
|
channel_ids: channelIds || [],
|
||||||
not_channel_ids:
|
not_channel_ids: mutedAndBlockedChannelIds || [],
|
||||||
notChannelIds ||
|
|
||||||
// If channelIds were passed in, we don't need not_channel_ids
|
|
||||||
(!channelIds && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : []),
|
|
||||||
order_by: orderBy || ['trending_group', 'trending_mixed'],
|
order_by: orderBy || ['trending_group', 'trending_mixed'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,7 +106,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
// https://github.com/lbryio/lbry-desktop/issues/3774
|
// https://github.com/lbryio/lbry-desktop/issues/3774
|
||||||
if (hideReposts) {
|
if (hideReposts) {
|
||||||
if (Array.isArray(options.claim_type)) {
|
if (Array.isArray(options.claim_type)) {
|
||||||
options.claim_type = options.claim_type.filter(claimType => claimType !== 'repost');
|
options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost');
|
||||||
} else {
|
} else {
|
||||||
options.claim_type = ['stream', 'channel'];
|
options.claim_type = ['stream', 'channel'];
|
||||||
}
|
}
|
||||||
|
@ -143,7 +141,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
return (
|
return (
|
||||||
<ul className="claim-grid">
|
<ul className="claim-grid">
|
||||||
{uris && uris.length
|
{uris && uris.length
|
||||||
? uris.map(uri => <ClaimPreviewTile key={uri} uri={uri} />)
|
? uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} />)
|
||||||
: new Array(pageSize).fill(1).map((x, i) => <ClaimPreviewTile key={i} placeholder />)}
|
: new Array(pageSize).fill(1).map((x, i) => <ClaimPreviewTile key={i} placeholder />)}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux';
|
import { makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux';
|
||||||
import { doCommentUpdate } from 'redux/actions/comments';
|
import { doCommentUpdate } from 'redux/actions/comments';
|
||||||
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } 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';
|
||||||
|
@ -11,7 +11,7 @@ import Comment from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
||||||
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state),
|
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
|
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
|
|
@ -200,7 +200,7 @@ function Comment(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="comment__menu">
|
<div className="comment__menu">
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton>
|
<MenuButton className="menu__button">
|
||||||
<Icon
|
<Icon
|
||||||
size={18}
|
size={18}
|
||||||
className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'}
|
className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'}
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
import {
|
import { doCommentAbandon, doCommentPin, doCommentList, doCommentModBlock } from 'redux/actions/comments';
|
||||||
doCommentAbandon,
|
import { doToggleMuteChannel } from 'redux/actions/blocked';
|
||||||
doCommentPin,
|
|
||||||
doCommentList,
|
|
||||||
// doCommentModBlock,
|
|
||||||
} from 'redux/actions/comments';
|
|
||||||
import { doToggleBlockChannel } from 'redux/actions/blocked';
|
|
||||||
// import { doSetActiveChannel } from 'redux/actions/app';
|
// import { doSetActiveChannel } from 'redux/actions/app';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
|
@ -19,14 +14,14 @@ const select = (state, props) => ({
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
closeInlinePlayer: () => dispatch(doSetPlayingUri({ uri: null })),
|
closeInlinePlayer: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||||
deleteComment: (commentId, creatorChannelUrl) => dispatch(doCommentAbandon(commentId, creatorChannelUrl)),
|
deleteComment: (commentId, creatorChannelUrl) => dispatch(doCommentAbandon(commentId, creatorChannelUrl)),
|
||||||
blockChannel: channelUri => dispatch(doToggleBlockChannel(channelUri)),
|
blockChannel: (channelUri) => dispatch(doToggleMuteChannel(channelUri)),
|
||||||
pinComment: (commentId, remove) => dispatch(doCommentPin(commentId, remove)),
|
pinComment: (commentId, remove) => dispatch(doCommentPin(commentId, remove)),
|
||||||
fetchComments: uri => dispatch(doCommentList(uri)),
|
fetchComments: (uri) => dispatch(doCommentList(uri)),
|
||||||
// setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)),
|
// setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)),
|
||||||
// commentModBlock: commentAuthor => dispatch(doCommentModBlock(commentAuthor)),
|
commentModBlock: (commentAuthor) => dispatch(doCommentModBlock(commentAuthor)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentMenuList);
|
export default connect(select, perform)(CommentMenuList);
|
||||||
|
|
|
@ -23,7 +23,7 @@ type Props = {
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
isTopLevel: boolean,
|
isTopLevel: boolean,
|
||||||
// commentModBlock: string => void,
|
commentModBlock: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentMenuList(props: Props) {
|
function CommentMenuList(props: Props) {
|
||||||
|
@ -43,17 +43,10 @@ function CommentMenuList(props: Props) {
|
||||||
isPinned,
|
isPinned,
|
||||||
handleEditComment,
|
handleEditComment,
|
||||||
fetchComments,
|
fetchComments,
|
||||||
// commentModBlock,
|
commentModBlock,
|
||||||
// setActiveChannel,
|
|
||||||
} = props;
|
} = props;
|
||||||
const activeChannelIsCreator = activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl;
|
const activeChannelIsCreator = activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl;
|
||||||
|
|
||||||
// let authorChannel;
|
|
||||||
// try {
|
|
||||||
// const { claimName } = parseURI(authorUri);
|
|
||||||
// authorChannel = claimName;
|
|
||||||
// } catch (e) {}
|
|
||||||
|
|
||||||
function handlePinComment(commentId, remove) {
|
function handlePinComment(commentId, remove) {
|
||||||
pinComment(commentId, remove).then(() => fetchComments(uri));
|
pinComment(commentId, remove).then(() => fetchComments(uri));
|
||||||
}
|
}
|
||||||
|
@ -65,32 +58,16 @@ function CommentMenuList(props: Props) {
|
||||||
|
|
||||||
function handleCommentBlock() {
|
function handleCommentBlock() {
|
||||||
if (claimIsMine) {
|
if (claimIsMine) {
|
||||||
// Block them from commenting on future content
|
commentModBlock(authorUri);
|
||||||
// commentModBlock(authorUri);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommentMute() {
|
||||||
blockChannel(authorUri);
|
blockChannel(authorUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
// function handleChooseChannel() {
|
|
||||||
// const { channelClaimId } = parseURI(authorUri);
|
|
||||||
// setActiveChannel(channelClaimId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuList className="menu__list--comments">
|
<MenuList className="menu__list">
|
||||||
{/* {commentIsMine && activeChannelClaim && activeChannelClaim.permanent_url !== authorUri && (
|
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleChooseChannel}>
|
|
||||||
<div className="menu__link">
|
|
||||||
<Icon aria-hidden icon={ICONS.CHANNEL} />
|
|
||||||
{__('Use this channel')}
|
|
||||||
</div>
|
|
||||||
<span className="comment__menu-help">
|
|
||||||
{__('Switch to %channel_name% to interact with this comment', { channel_name: authorChannel })}.
|
|
||||||
</span>
|
|
||||||
</MenuItem>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{activeChannelIsCreator && <div className="comment__menu-title">{__('Creator tools')}</div>}
|
{activeChannelIsCreator && <div className="comment__menu-title">{__('Creator tools')}</div>}
|
||||||
|
|
||||||
{activeChannelIsCreator && isTopLevel && (
|
{activeChannelIsCreator && isTopLevel && (
|
||||||
|
@ -123,16 +100,27 @@ function CommentMenuList(props: Props) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Disabled until we deal with current app blocklist parity */}
|
|
||||||
{!commentIsMine && (
|
{!commentIsMine && (
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleCommentBlock}>
|
<MenuItem className="comment__menu-option" onSelect={handleCommentBlock}>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={ICONS.BLOCK} />
|
<Icon aria-hidden icon={ICONS.BLOCK} />
|
||||||
{__('Block')}
|
{__('Block')}
|
||||||
</div>
|
</div>
|
||||||
{/* {activeChannelIsCreator && (
|
{activeChannelIsCreator && (
|
||||||
<span className="comment__menu-help">Hide this channel's comments and block them from commenting.</span>
|
<span className="comment__menu-help">{__('Prevent this channel from interacting with you.')}</span>
|
||||||
)} */}
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!commentIsMine && (
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleCommentMute}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.MUTE} />
|
||||||
|
{__('Mute')}
|
||||||
|
</div>
|
||||||
|
{activeChannelIsCreator && (
|
||||||
|
<span className="comment__menu-help">{__('Hide this channel for you only.')}</span>
|
||||||
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -238,6 +238,13 @@ export const icons = {
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
|
[ICONS.MUTE]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||||
|
<line x1="23" y1="9" x2="17" y2="15" />
|
||||||
|
<line x1="17" y1="9" x2="23" y2="15" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
[ICONS.LIGHT]: buildIcon(
|
[ICONS.LIGHT]: buildIcon(
|
||||||
<g>
|
<g>
|
||||||
<circle cx="12" cy="12" r="5" />
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
|
|
@ -10,7 +10,7 @@ function FileAuthor(props: Props) {
|
||||||
const { channelUri } = props;
|
const { channelUri } = props;
|
||||||
|
|
||||||
return channelUri ? (
|
return channelUri ? (
|
||||||
<ClaimPreview uri={channelUri} type="inline" properties={false} hideBlock />
|
<ClaimPreview uri={channelUri} type="inline" properties={false} />
|
||||||
) : (
|
) : (
|
||||||
<div className="claim-preview--inline claim-preview__title">{__('Anonymous')}</div>
|
<div className="claim-preview--inline claim-preview__title">{__('Anonymous')}</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/setting
|
||||||
import { selectHasNavigated, selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectHasNavigated, selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import Header from './view';
|
import Header from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
language: selectLanguage(state),
|
language: selectLanguage(state),
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
|
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
|
||||||
|
@ -27,10 +27,9 @@ const select = state => ({
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
setClientSetting: (key, value, push) => dispatch(doSetClientSetting(key, value, push)),
|
setClientSetting: (key, value, push) => dispatch(doSetClientSetting(key, value, push)),
|
||||||
signOut: () => dispatch(doSignOut()),
|
signOut: () => dispatch(doSignOut()),
|
||||||
openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)),
|
|
||||||
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
|
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
|
||||||
clearEmailEntry: () => dispatch(doClearEmailEntry()),
|
clearEmailEntry: () => dispatch(doClearEmailEntry()),
|
||||||
clearPasswordEntry: () => dispatch(doClearPasswordEntry()),
|
clearPasswordEntry: () => dispatch(doClearPasswordEntry()),
|
||||||
|
|
|
@ -33,8 +33,8 @@ type Props = {
|
||||||
index: number,
|
index: number,
|
||||||
length: number,
|
length: number,
|
||||||
location: { pathname: string },
|
location: { pathname: string },
|
||||||
push: string => void,
|
push: (string) => void,
|
||||||
replace: string => void,
|
replace: (string) => void,
|
||||||
},
|
},
|
||||||
currentTheme: string,
|
currentTheme: string,
|
||||||
automaticDarkModeEnabled: boolean,
|
automaticDarkModeEnabled: boolean,
|
||||||
|
@ -52,13 +52,12 @@ type Props = {
|
||||||
syncError: ?string,
|
syncError: ?string,
|
||||||
emailToVerify?: string,
|
emailToVerify?: string,
|
||||||
signOut: () => void,
|
signOut: () => void,
|
||||||
openChannelCreate: () => void,
|
|
||||||
openSignOutModal: () => void,
|
openSignOutModal: () => void,
|
||||||
clearEmailEntry: () => void,
|
clearEmailEntry: () => void,
|
||||||
clearPasswordEntry: () => void,
|
clearPasswordEntry: () => void,
|
||||||
hasNavigated: boolean,
|
hasNavigated: boolean,
|
||||||
sidebarOpen: boolean,
|
sidebarOpen: boolean,
|
||||||
setSidebarOpen: boolean => void,
|
setSidebarOpen: (boolean) => void,
|
||||||
isAbsoluteSideNavHidden: boolean,
|
isAbsoluteSideNavHidden: boolean,
|
||||||
hideCancel: boolean,
|
hideCancel: boolean,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
|
@ -181,7 +180,7 @@ const Header = (props: Props) => {
|
||||||
label={hideBalance || Number(roundedBalance) === 0 ? __('Your Wallet') : roundedBalance}
|
label={hideBalance || Number(roundedBalance) === 0 ? __('Your Wallet') : roundedBalance}
|
||||||
icon={ICONS.LBC}
|
icon={ICONS.LBC}
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
onDoubleClick={e => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -197,7 +196,7 @@ const Header = (props: Props) => {
|
||||||
// @endif
|
// @endif
|
||||||
})}
|
})}
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
onDoubleClick={e => {
|
onDoubleClick={(e) => {
|
||||||
remote.getCurrentWindow().maximize();
|
remote.getCurrentWindow().maximize();
|
||||||
}}
|
}}
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -250,7 +249,7 @@ const Header = (props: Props) => {
|
||||||
if (history.location.pathname === '/') window.location.reload();
|
if (history.location.pathname === '/') window.location.reload();
|
||||||
}}
|
}}
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
onDoubleClick={e => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -308,7 +307,7 @@ const Header = (props: Props) => {
|
||||||
icon={ICONS.REMOVE}
|
icon={ICONS.REMOVE}
|
||||||
{...closeButtonNavigationProps}
|
{...closeButtonNavigationProps}
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
onDoubleClick={e => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -326,8 +325,8 @@ const Header = (props: Props) => {
|
||||||
type HeaderMenuButtonProps = {
|
type HeaderMenuButtonProps = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
notificationsEnabled: boolean,
|
notificationsEnabled: boolean,
|
||||||
history: { push: string => void },
|
history: { push: (string) => void },
|
||||||
handleThemeToggle: string => void,
|
handleThemeToggle: (string) => void,
|
||||||
currentTheme: string,
|
currentTheme: string,
|
||||||
activeChannelUrl: ?string,
|
activeChannelUrl: ?string,
|
||||||
openSignOutModal: () => void,
|
openSignOutModal: () => void,
|
||||||
|
@ -357,7 +356,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
title={__('Publish a file, or create a channel')}
|
title={__('Publish a file, or create a channel')}
|
||||||
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
|
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
onDoubleClick={e => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -386,7 +385,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
title={__('Settings')}
|
title={__('Settings')}
|
||||||
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
|
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
onDoubleClick={e => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -419,7 +418,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
'header__navigation-item--profile-pic': activeChannelUrl,
|
'header__navigation-item--profile-pic': activeChannelUrl,
|
||||||
})}
|
})}
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
onDoubleClick={e => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
// @endif
|
// @endif
|
||||||
|
|
|
@ -22,7 +22,7 @@ type Props = {
|
||||||
menuButton: boolean,
|
menuButton: boolean,
|
||||||
children: any,
|
children: any,
|
||||||
doReadNotifications: ([number]) => void,
|
doReadNotifications: ([number]) => void,
|
||||||
doDeleteNotification: number => void,
|
doDeleteNotification: (number) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Notification(props: Props) {
|
export default function Notification(props: Props) {
|
||||||
|
@ -166,10 +166,10 @@ export default function Notification(props: Props) {
|
||||||
|
|
||||||
<div className="notification__menu">
|
<div className="notification__menu">
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton onClick={e => e.stopPropagation()}>
|
<MenuButton onClick={(e) => e.stopPropagation()}>
|
||||||
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList className="menu__list--comments">
|
<MenuList className="menu__list">
|
||||||
<MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}>
|
<MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}>
|
||||||
<Icon aria-hidden icon={ICONS.DELETE} />
|
<Icon aria-hidden icon={ICONS.DELETE} />
|
||||||
{__('Delete')}
|
{__('Delete')}
|
||||||
|
|
|
@ -272,7 +272,7 @@ function AppRouter(props: Props) {
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
||||||
|
|
|
@ -268,6 +268,15 @@ 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_MODERATION_BLOCK_LIST_STARTED = 'COMMENT_MODERATION_BLOCK_LIST_STARTED';
|
||||||
|
export const COMMENT_MODERATION_BLOCK_LIST_COMPLETED = 'COMMENT_MODERATION_BLOCK_LIST_COMPLETED';
|
||||||
|
export const COMMENT_MODERATION_BLOCK_LIST_FAILED = 'COMMENT_MODERATION_BLOCK_LIST_FAILED';
|
||||||
|
export const COMMENT_MODERATION_BLOCK_STARTED = 'COMMENT_MODERATION_BLOCK_STARTED';
|
||||||
|
export const COMMENT_MODERATION_BLOCK_COMPLETE = 'COMMENT_MODERATION_BLOCK_COMPLETE';
|
||||||
|
export const COMMENT_MODERATION_BLOCK_FAILED = 'COMMENT_MODERATION_BLOCK_FAILED';
|
||||||
|
export const COMMENT_MODERATION_UN_BLOCK_STARTED = 'COMMENT_MODERATION_UN_BLOCK_STARTED';
|
||||||
|
export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
|
||||||
|
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
|
||||||
|
|
||||||
// Blocked channels
|
// Blocked channels
|
||||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
||||||
|
|
|
@ -82,6 +82,7 @@ export const LIBRARY = 'Folder';
|
||||||
export const TAG = 'Tag';
|
export const TAG = 'Tag';
|
||||||
export const SUPPORT = 'TrendingUp';
|
export const SUPPORT = 'TrendingUp';
|
||||||
export const BLOCK = 'Slash';
|
export const BLOCK = 'Slash';
|
||||||
|
export const MUTE = 'VolumeX';
|
||||||
export const UNBLOCK = 'Circle';
|
export const UNBLOCK = 'Circle';
|
||||||
export const VIEW = 'View';
|
export const VIEW = 'View';
|
||||||
export const EYE = 'Eye';
|
export const EYE = 'Eye';
|
||||||
|
|
|
@ -41,7 +41,6 @@ export const LIQUIDATE_SUPPORTS = 'liquidate_supports';
|
||||||
export const MASS_TIP_UNLOCK = 'mass_tip_unlock';
|
export const MASS_TIP_UNLOCK = 'mass_tip_unlock';
|
||||||
export const CONFIRM_AGE = 'confirm_age';
|
export const CONFIRM_AGE = 'confirm_age';
|
||||||
export const SYNC_ENABLE = 'SYNC_ENABLE';
|
export const SYNC_ENABLE = 'SYNC_ENABLE';
|
||||||
export const REMOVE_BLOCKED = 'remove_blocked';
|
|
||||||
export const IMAGE_UPLOAD = 'image_upload';
|
export const IMAGE_UPLOAD = 'image_upload';
|
||||||
export const MOBILE_SEARCH = 'mobile_search';
|
export const MOBILE_SEARCH = 'mobile_search';
|
||||||
export const VIEW_IMAGE = 'view_image';
|
export const VIEW_IMAGE = 'view_image';
|
||||||
|
|
|
@ -24,6 +24,7 @@ exports.SEND = 'send';
|
||||||
exports.SETTINGS = 'settings';
|
exports.SETTINGS = 'settings';
|
||||||
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
|
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
|
||||||
exports.SETTINGS_ADVANCED = 'settings/advanced';
|
exports.SETTINGS_ADVANCED = 'settings/advanced';
|
||||||
|
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
|
||||||
exports.SHOW = 'show';
|
exports.SHOW = 'show';
|
||||||
exports.ACCOUNT = 'account';
|
exports.ACCOUNT = 'account';
|
||||||
exports.SEARCH = 'search';
|
exports.SEARCH = 'search';
|
||||||
|
@ -36,7 +37,6 @@ exports.CHANNELS_FOLLOWING = 'following';
|
||||||
exports.DEPRECATED__CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage';
|
exports.DEPRECATED__CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage';
|
||||||
exports.CHANNELS_FOLLOWING_DISCOVER = 'following/discover';
|
exports.CHANNELS_FOLLOWING_DISCOVER = 'following/discover';
|
||||||
exports.WALLET = 'wallet';
|
exports.WALLET = 'wallet';
|
||||||
exports.BLOCKED = 'blocked';
|
|
||||||
exports.CHANNELS = 'channels';
|
exports.CHANNELS = 'channels';
|
||||||
exports.EMBED = 'embed';
|
exports.EMBED = 'embed';
|
||||||
exports.TOP = 'top';
|
exports.TOP = 'top';
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { doHideModal } from 'redux/actions/app';
|
|
||||||
import ChannelCreate from './view';
|
|
||||||
|
|
||||||
const select = state => ({});
|
|
||||||
|
|
||||||
const perform = {
|
|
||||||
doHideModal,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(ChannelCreate);
|
|
|
@ -1,18 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import ChannelForm from 'component/channelForm';
|
|
||||||
import { Modal } from 'modal/modal';
|
|
||||||
|
|
||||||
type Props = { doHideModal: () => void };
|
|
||||||
|
|
||||||
const ModalChannelCreate = (props: Props) => {
|
|
||||||
const { doHideModal } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen type="card" onAborted={doHideModal}>
|
|
||||||
<ChannelForm onSuccess={doHideModal} />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalChannelCreate;
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { doHideModal } from 'redux/actions/app';
|
|
||||||
import { doToggleBlockChannel } from 'redux/actions/blocked';
|
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
|
||||||
import ModalRemoveBlocked from './view';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
|
||||||
blockedChannels: selectBlockedChannels(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = dispatch => ({
|
|
||||||
closeModal: () => dispatch(doHideModal()),
|
|
||||||
toggleBlockChannel: uri => dispatch(doToggleBlockChannel(uri)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(ModalRemoveBlocked);
|
|
|
@ -1,46 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import { Modal } from 'modal/modal';
|
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
blockedUri: string,
|
|
||||||
closeModal: () => void,
|
|
||||||
blockedChannels: Array<string>,
|
|
||||||
toggleBlockChannel: (uri: string) => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ModalRemoveBlocked(props: Props) {
|
|
||||||
const { blockedUri, closeModal, blockedChannels, toggleBlockChannel } = props;
|
|
||||||
|
|
||||||
function handleConfirm() {
|
|
||||||
if (blockedUri && blockedChannels.includes(blockedUri)) {
|
|
||||||
// DANGER: Always ensure the uri is actually in the list since we are using a
|
|
||||||
// toggle function. If 'null' is accidentally toggled INTO the list, the app
|
|
||||||
// won't start. Ideally, we should add a "removeBlockedChannel()", but with
|
|
||||||
// the gating above, it should be safe/good enough.
|
|
||||||
toggleBlockChannel(blockedUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen
|
|
||||||
type="confirm"
|
|
||||||
title={__('Remove from blocked list')}
|
|
||||||
confirmButtonLabel={__('Remove')}
|
|
||||||
onConfirmed={handleConfirm}
|
|
||||||
onAborted={() => closeModal()}
|
|
||||||
>
|
|
||||||
<em>{blockedUri}</em>
|
|
||||||
<p />
|
|
||||||
<p>
|
|
||||||
<I18nMessage>Are you sure you want to remove this from the list?</I18nMessage>
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModalRemoveBlocked;
|
|
|
@ -16,7 +16,6 @@ import ModalRevokeClaim from 'modal/modalRevokeClaim';
|
||||||
import ModalPhoneCollection from 'modal/modalPhoneCollection';
|
import ModalPhoneCollection from 'modal/modalPhoneCollection';
|
||||||
import ModalFirstSubscription from 'modal/modalFirstSubscription';
|
import ModalFirstSubscription from 'modal/modalFirstSubscription';
|
||||||
import ModalConfirmTransaction from 'modal/modalConfirmTransaction';
|
import ModalConfirmTransaction from 'modal/modalConfirmTransaction';
|
||||||
import ModalRemoveBlocked from 'modal/modalRemoveBlocked';
|
|
||||||
import ModalSocialShare from 'modal/modalSocialShare';
|
import ModalSocialShare from 'modal/modalSocialShare';
|
||||||
import ModalSendTip from 'modal/modalSendTip';
|
import ModalSendTip from 'modal/modalSendTip';
|
||||||
import ModalPublish from 'modal/modalPublish';
|
import ModalPublish from 'modal/modalPublish';
|
||||||
|
@ -32,7 +31,6 @@ import ModalCommentAcknowledgement from 'modal/modalCommentAcknowledgement';
|
||||||
import ModalWalletSend from 'modal/modalWalletSend';
|
import ModalWalletSend from 'modal/modalWalletSend';
|
||||||
import ModalWalletReceive from 'modal/modalWalletReceive';
|
import ModalWalletReceive from 'modal/modalWalletReceive';
|
||||||
import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
|
import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
|
||||||
import ModalCreateChannel from 'modal/modalChannelCreate';
|
|
||||||
import ModalSetReferrer from 'modal/modalSetReferrer';
|
import ModalSetReferrer from 'modal/modalSetReferrer';
|
||||||
import ModalSignOut from 'modal/modalSignOut';
|
import ModalSignOut from 'modal/modalSignOut';
|
||||||
import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate';
|
import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate';
|
||||||
|
@ -130,8 +128,6 @@ function ModalRouter(props: Props) {
|
||||||
return <ModalWalletReceive {...modalProps} />;
|
return <ModalWalletReceive {...modalProps} />;
|
||||||
case MODALS.YOUTUBE_WELCOME:
|
case MODALS.YOUTUBE_WELCOME:
|
||||||
return <ModalYoutubeWelcome />;
|
return <ModalYoutubeWelcome />;
|
||||||
case MODALS.CREATE_CHANNEL:
|
|
||||||
return <ModalCreateChannel {...modalProps} />;
|
|
||||||
case MODALS.SET_REFERRER:
|
case MODALS.SET_REFERRER:
|
||||||
return <ModalSetReferrer {...modalProps} />;
|
return <ModalSetReferrer {...modalProps} />;
|
||||||
case MODALS.SIGN_OUT:
|
case MODALS.SIGN_OUT:
|
||||||
|
@ -142,8 +138,6 @@ function ModalRouter(props: Props) {
|
||||||
return <ModalFileSelection {...modalProps} />;
|
return <ModalFileSelection {...modalProps} />;
|
||||||
case MODALS.LIQUIDATE_SUPPORTS:
|
case MODALS.LIQUIDATE_SUPPORTS:
|
||||||
return <ModalSupportsLiquidate {...modalProps} />;
|
return <ModalSupportsLiquidate {...modalProps} />;
|
||||||
case MODALS.REMOVE_BLOCKED:
|
|
||||||
return <ModalRemoveBlocked {...modalProps} />;
|
|
||||||
case MODALS.IMAGE_UPLOAD:
|
case MODALS.IMAGE_UPLOAD:
|
||||||
return <ModalImageUpload {...modalProps} />;
|
return <ModalImageUpload {...modalProps} />;
|
||||||
case MODALS.SYNC_ENABLE:
|
case MODALS.SYNC_ENABLE:
|
||||||
|
|
|
@ -8,10 +8,11 @@ import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectClaimIsPending,
|
makeSelectClaimIsPending,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc';
|
import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc';
|
||||||
import { selectYoutubeChannels } from 'redux/selectors/user';
|
import { selectYoutubeChannels } from 'redux/selectors/user';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import ChannelPage from './view';
|
import ChannelPage from './view';
|
||||||
|
|
||||||
|
@ -23,16 +24,17 @@ const select = (state, props) => ({
|
||||||
page: selectCurrentChannelPage(state),
|
page: selectCurrentChannelPage(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
||||||
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
|
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
|
||||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
subCount: makeSelectSubCountForUri(props.uri)(state),
|
subCount: makeSelectSubCountForUri(props.uri)(state),
|
||||||
pending: makeSelectClaimIsPending(props.uri)(state),
|
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||||
youtubeChannels: selectYoutubeChannels(state),
|
youtubeChannels: selectYoutubeChannels(state),
|
||||||
|
blockedChannels: selectModerationBlockList(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
fetchSubCount: claimId => dispatch(doFetchSubCount(claimId)),
|
fetchSubCount: (claimId) => dispatch(doFetchSubCount(claimId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(ChannelPage);
|
export default connect(select, perform)(ChannelPage);
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { parseURI } from 'lbry-redux';
|
||||||
import { YOUTUBE_STATUSES } from 'lbryinc';
|
import { YOUTUBE_STATUSES } from 'lbryinc';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
import BlockButton from 'component/blockButton';
|
|
||||||
import ShareButton from 'component/shareButton';
|
import ShareButton from 'component/shareButton';
|
||||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
@ -21,6 +20,8 @@ import classnames from 'classnames';
|
||||||
import HelpLink from 'component/common/help-link';
|
import HelpLink from 'component/common/help-link';
|
||||||
import ClaimSupportButton from 'component/claimSupportButton';
|
import ClaimSupportButton from 'component/claimSupportButton';
|
||||||
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
||||||
|
import ClaimMenuList from 'component/claimMenuList';
|
||||||
|
import Yrbl from 'component/yrbl';
|
||||||
|
|
||||||
export const PAGE_VIEW_QUERY = `view`;
|
export const PAGE_VIEW_QUERY = `view`;
|
||||||
const ABOUT_PAGE = `about`;
|
const ABOUT_PAGE = `about`;
|
||||||
|
@ -46,6 +47,7 @@ type Props = {
|
||||||
subCount: number,
|
subCount: number,
|
||||||
pending: boolean,
|
pending: boolean,
|
||||||
youtubeChannels: ?Array<{ channel_claim_id: string, sync_status: string, transfer_state: string }>,
|
youtubeChannels: ?Array<{ channel_claim_id: string, sync_status: string, transfer_state: string }>,
|
||||||
|
blockedChannels: Array<string>,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelPage(props: Props) {
|
function ChannelPage(props: Props) {
|
||||||
|
@ -63,12 +65,14 @@ function ChannelPage(props: Props) {
|
||||||
subCount,
|
subCount,
|
||||||
pending,
|
pending,
|
||||||
youtubeChannels,
|
youtubeChannels,
|
||||||
|
blockedChannels,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
goBack,
|
goBack,
|
||||||
location: { search },
|
location: { search },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
const [viewBlockedChannel, setViewBlockedChannel] = React.useState(false);
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
|
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
|
||||||
const [discussionWasMounted, setDiscussionWasMounted] = React.useState(false);
|
const [discussionWasMounted, setDiscussionWasMounted] = React.useState(false);
|
||||||
|
@ -77,6 +81,7 @@ function ChannelPage(props: Props) {
|
||||||
const { permanent_url: permanentUrl } = claim;
|
const { permanent_url: permanentUrl } = claim;
|
||||||
const claimId = claim.claim_id;
|
const claimId = claim.claim_id;
|
||||||
const formattedSubCount = Number(subCount).toLocaleString();
|
const formattedSubCount = Number(subCount).toLocaleString();
|
||||||
|
const isBlocked = claim && blockedChannels.includes(claim.permanent_url);
|
||||||
const isMyYouTubeChannel =
|
const isMyYouTubeChannel =
|
||||||
claim &&
|
claim &&
|
||||||
youtubeChannels &&
|
youtubeChannels &&
|
||||||
|
@ -157,7 +162,7 @@ function ChannelPage(props: Props) {
|
||||||
{!channelIsBlocked && !channelIsBlackListed && <ShareButton uri={uri} />}
|
{!channelIsBlocked && !channelIsBlackListed && <ShareButton uri={uri} />}
|
||||||
{!channelIsBlocked && <ClaimSupportButton uri={uri} />}
|
{!channelIsBlocked && <ClaimSupportButton uri={uri} />}
|
||||||
{!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
|
{!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
|
||||||
{!isSubscribed && <BlockButton uri={permanentUrl} />}
|
<ClaimMenuList uri={claim.permanent_url} inline />
|
||||||
</div>
|
</div>
|
||||||
{cover && (
|
{cover && (
|
||||||
<img
|
<img
|
||||||
|
@ -203,24 +208,44 @@ function ChannelPage(props: Props) {
|
||||||
<div className="channel-cover__gradient" />
|
<div className="channel-cover__gradient" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
{isBlocked && !viewBlockedChannel ? (
|
||||||
<TabList className="tabs__list--channel-page">
|
<div className="main--empty">
|
||||||
<Tab disabled={editing}>{__('Content')}</Tab>
|
<Yrbl
|
||||||
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
|
title={__('This channel is blocked')}
|
||||||
<Tab disabled={editing}>{__('Community')}</Tab>
|
subtitle={__('Are you sure you want to view this content? Viewing will not unblock @%channel%', {
|
||||||
</TabList>
|
channel: channelName,
|
||||||
<TabPanels>
|
})}
|
||||||
<TabPanel>
|
actions={
|
||||||
<ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} />
|
<div className="section__actions">
|
||||||
</TabPanel>
|
<Button button="primary" label={__('View Content')} onClick={() => setViewBlockedChannel(true)} />
|
||||||
<TabPanel>
|
</div>
|
||||||
<ChannelAbout uri={uri} />
|
}
|
||||||
</TabPanel>
|
/>
|
||||||
<TabPanel>
|
</div>
|
||||||
{(discussionWasMounted || currentView === DISCUSSION_PAGE) && <ChannelDiscussion uri={uri} />}
|
) : (
|
||||||
</TabPanel>
|
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||||
</TabPanels>
|
<TabList className="tabs__list--channel-page">
|
||||||
</Tabs>
|
<Tab disabled={editing}>{__('Content')}</Tab>
|
||||||
|
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
|
||||||
|
<Tab disabled={editing}>{__('Community')}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel>
|
||||||
|
<ChannelContent
|
||||||
|
uri={uri}
|
||||||
|
channelIsBlackListed={channelIsBlackListed}
|
||||||
|
viewBlockedChannel={viewBlockedChannel}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<ChannelAbout uri={uri} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
{(discussionWasMounted || currentView === DISCUSSION_PAGE) && <ChannelDiscussion uri={uri} />}
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||||
import { selectBalance } from 'lbry-redux';
|
import { selectBalance } from 'lbry-redux';
|
||||||
import ChannelNew from './view';
|
import ChannelNew from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,12 @@ function ChannelNew(props: Props) {
|
||||||
<Page noSideNavigation noFooter backout={{ title: __('Create a channel'), backLabel: __('Cancel') }}>
|
<Page noSideNavigation noFooter backout={{ title: __('Create a channel'), backLabel: __('Cancel') }}>
|
||||||
{emptyBalance && <YrblWalletEmpty />}
|
{emptyBalance && <YrblWalletEmpty />}
|
||||||
|
|
||||||
<ChannelEdit disabled={emptyBalance} onDone={() => push(redirectUrl || `/$/${PAGES.CHANNELS}`)} />
|
<ChannelEdit
|
||||||
|
disabled={emptyBalance}
|
||||||
|
onDone={() => {
|
||||||
|
push(redirectUrl || `/$/${PAGES.CHANNELS}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,14 +24,14 @@ export default function ChannelsPage(props: Props) {
|
||||||
const { channels, channelUrls, fetchChannelListMine, fetchingChannels, youtubeChannels } = props;
|
const { channels, channelUrls, fetchChannelListMine, fetchingChannels, youtubeChannels } = props;
|
||||||
const [rewardData, setRewardData] = React.useState();
|
const [rewardData, setRewardData] = React.useState();
|
||||||
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
||||||
const hasPendingChannels = channels && channels.some(channel => channel.confirmations < 0);
|
const hasPendingChannels = channels && channels.some((channel) => channel.confirmations < 0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchChannelListMine();
|
fetchChannelListMine();
|
||||||
}, [fetchChannelListMine, hasPendingChannels]);
|
}, [fetchChannelListMine, hasPendingChannels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Lbryio.call('user_rewards', 'view_rate').then(data => setRewardData(data));
|
Lbryio.call('user_rewards', 'view_rate').then((data) => setRewardData(data));
|
||||||
}, [setRewardData]);
|
}, [setRewardData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -52,7 +52,7 @@ export default function ChannelsPage(props: Props) {
|
||||||
}
|
}
|
||||||
loading={fetchingChannels}
|
loading={fetchingChannels}
|
||||||
uris={channelUrls}
|
uris={channelUrls}
|
||||||
renderActions={claim => {
|
renderActions={(claim) => {
|
||||||
const claimsInChannel = claim.meta.claims_in_channel;
|
const claimsInChannel = claim.meta.claims_in_channel;
|
||||||
return claimsInChannel === 0 ? (
|
return claimsInChannel === 0 ? (
|
||||||
<span />
|
<span />
|
||||||
|
@ -67,7 +67,7 @@ export default function ChannelsPage(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
renderProperties={claim => {
|
renderProperties={(claim) => {
|
||||||
const claimsInChannel = claim.meta.claims_in_channel;
|
const claimsInChannel = claim.meta.claims_in_channel;
|
||||||
if (!claim || claimsInChannel === 0) {
|
if (!claim || claimsInChannel === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -76,7 +76,7 @@ export default function ChannelsPage(props: Props) {
|
||||||
const channelRewardData =
|
const channelRewardData =
|
||||||
rewardData &&
|
rewardData &&
|
||||||
rewardData.rates &&
|
rewardData.rates &&
|
||||||
rewardData.rates.find(data => {
|
rewardData.rates.find((data) => {
|
||||||
return data.channel_claim_id === claim.claim_id;
|
return data.channel_claim_id === claim.claim_id;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { selectHomepageData } from 'redux/selectors/settings';
|
import { selectHomepageData } from 'redux/selectors/settings';
|
||||||
import ChannelsFollowingManagePage from './view';
|
import ChannelsFollowingManagePage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
followedTags: selectFollowedTags(state),
|
followedTags: selectFollowedTags(state),
|
||||||
subscribedChannels: selectSubscriptions(state),
|
subscribedChannels: selectSubscriptions(state),
|
||||||
blockedChannels: selectBlockedChannels(state),
|
blockedChannels: selectMutedChannels(state),
|
||||||
homepageData: selectHomepageData(state),
|
homepageData: selectHomepageData(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
|
import { selectModerationBlockList, selectFetchingModerationBlockList } from 'redux/selectors/comments';
|
||||||
import ListBlocked from './view';
|
import ListBlocked from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
uris: selectBlockedChannels(state),
|
mutedUris: selectMutedChannels(state),
|
||||||
|
blockedUris: selectModerationBlockList(state),
|
||||||
|
fetchingModerationBlockList: selectFetchingModerationBlockList(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, null)(ListBlocked);
|
export default connect(select)(ListBlocked);
|
||||||
|
|
|
@ -1,34 +1,141 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Card from 'component/common/card';
|
import Spinner from 'component/spinner';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import usePrevious from 'effects/use-previous';
|
||||||
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import ChannelBlockButton from 'component/channelBlockButton';
|
||||||
|
import ChannelMuteButton from 'component/channelMuteButton';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uris: Array<string>,
|
mutedUris: ?Array<string>,
|
||||||
|
blockedUris: ?Array<string>,
|
||||||
|
fetchingModerationBlockList: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VIEW_BLOCKED = 'blocked';
|
||||||
|
const VIEW_MUTED = 'muted';
|
||||||
|
|
||||||
function ListBlocked(props: Props) {
|
function ListBlocked(props: Props) {
|
||||||
const { uris } = props;
|
const { mutedUris, blockedUris, fetchingModerationBlockList } = props;
|
||||||
|
|
||||||
|
const [viewMode, setViewMode] = usePersistedState('blocked-muted:display', VIEW_BLOCKED);
|
||||||
|
const [loading, setLoading] = React.useState(!blockedUris || !blockedUris.length);
|
||||||
|
|
||||||
|
// Keep a local list to allow for undoing actions in this component
|
||||||
|
const [localBlockedList, setLocalBlockedList] = React.useState(undefined);
|
||||||
|
const [localMutedList, setLocalMutedList] = React.useState(undefined);
|
||||||
|
|
||||||
|
const previousFetchingModBlockList = usePrevious(fetchingModerationBlockList);
|
||||||
|
const hasLocalMuteList = localMutedList && localMutedList.length > 0;
|
||||||
|
const hasLocalBlockList = localBlockedList && localBlockedList.length > 0;
|
||||||
|
const stringifiedMutedChannels = JSON.stringify(mutedUris);
|
||||||
|
const justMuted = localMutedList && mutedUris && localMutedList.length < mutedUris.length;
|
||||||
|
const justBlocked = localBlockedList && blockedUris && localBlockedList.length < blockedUris.length;
|
||||||
|
const stringifiedBlockedChannels = JSON.stringify(blockedUris);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (previousFetchingModBlockList && !fetchingModerationBlockList) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [setLoading, previousFetchingModBlockList, fetchingModerationBlockList]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const jsonMutedChannels = stringifiedMutedChannels && JSON.parse(stringifiedMutedChannels);
|
||||||
|
if (!hasLocalMuteList && jsonMutedChannels && jsonMutedChannels.length > 0) {
|
||||||
|
setLocalMutedList(jsonMutedChannels);
|
||||||
|
}
|
||||||
|
}, [stringifiedMutedChannels, hasLocalMuteList]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const jsonBlockedChannels = stringifiedBlockedChannels && JSON.parse(stringifiedBlockedChannels);
|
||||||
|
if (!hasLocalBlockList && jsonBlockedChannels && jsonBlockedChannels.length > 0) {
|
||||||
|
setLocalBlockedList(jsonBlockedChannels);
|
||||||
|
}
|
||||||
|
}, [stringifiedBlockedChannels, hasLocalBlockList]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (justMuted && stringifiedMutedChannels) {
|
||||||
|
setLocalMutedList(JSON.parse(stringifiedMutedChannels));
|
||||||
|
}
|
||||||
|
}, [stringifiedMutedChannels, justMuted, setLocalMutedList]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (justBlocked && stringifiedBlockedChannels) {
|
||||||
|
setLocalBlockedList(JSON.parse(stringifiedBlockedChannels));
|
||||||
|
}
|
||||||
|
}, [stringifiedBlockedChannels, justBlocked, setLocalBlockedList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{uris && uris.length ? (
|
{loading && (
|
||||||
<Card
|
|
||||||
isBodyList
|
|
||||||
title={__('Your blocked channels')}
|
|
||||||
body={<ClaimList uris={uris} showUnresolvedClaims showHiddenByUser />}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<section className="card card--section">
|
<Spinner />
|
||||||
<h2 className="card__title card__title--deprecated">{__('You aren’t blocking any channels')}</h2>
|
|
||||||
<p className="section__subtitle">
|
|
||||||
{__('When you block a channel, all content from that channel will be hidden.')}
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<div className="section__header--actions">
|
||||||
|
<div className="section__actions--inline">
|
||||||
|
<Button
|
||||||
|
icon={ICONS.BLOCK}
|
||||||
|
button="alt"
|
||||||
|
label={__('Blocked')}
|
||||||
|
className={classnames(`button-toggle`, {
|
||||||
|
'button-toggle--active': viewMode === VIEW_BLOCKED,
|
||||||
|
})}
|
||||||
|
onClick={() => setViewMode(VIEW_BLOCKED)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={ICONS.MUTE}
|
||||||
|
button="alt"
|
||||||
|
label={__('Muted')}
|
||||||
|
className={classnames(`button-toggle`, {
|
||||||
|
'button-toggle--active': viewMode === VIEW_MUTED,
|
||||||
|
})}
|
||||||
|
onClick={() => setViewMode(VIEW_MUTED)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="help--notice">
|
||||||
|
{viewMode === VIEW_MUTED
|
||||||
|
? __(
|
||||||
|
'Muted channels will be invisible to you in the app. They will not know they are muted and can still interact with you and your content.'
|
||||||
|
)
|
||||||
|
: __(
|
||||||
|
"Blocked channels will be invisible to you in the app. They will not be able to comment on your content, or reply to you comments left on other channels' content."
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ClaimList
|
||||||
|
uris={viewMode === VIEW_MUTED ? localMutedList : localBlockedList}
|
||||||
|
showUnresolvedClaims
|
||||||
|
showHiddenByUser
|
||||||
|
hideMenu
|
||||||
|
renderActions={(claim) => {
|
||||||
|
return (
|
||||||
|
<div className="section__actions">
|
||||||
|
{viewMode === VIEW_MUTED ? (
|
||||||
|
<>
|
||||||
|
<ChannelMuteButton uri={claim.permanent_url} />
|
||||||
|
<ChannelBlockButton uri={claim.permanent_url} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChannelBlockButton uri={claim.permanent_url} />
|
||||||
|
<ChannelMuteButton uri={claim.permanent_url} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,10 @@ import {
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { makeSelectClientSetting, selectDaemonSettings, selectLanguage } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectDaemonSettings, selectLanguage } from 'redux/selectors/settings';
|
||||||
import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
|
import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
|
||||||
import { selectBlockedChannelsCount } from 'redux/selectors/blocked';
|
|
||||||
import SettingsPage from './view';
|
import SettingsPage from './view';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
daemonSettings: selectDaemonSettings(state),
|
daemonSettings: selectDaemonSettings(state),
|
||||||
allowAnalytics: selectAllowAnalytics(state),
|
allowAnalytics: selectAllowAnalytics(state),
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
|
@ -27,7 +26,6 @@ const select = state => ({
|
||||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||||
walletEncrypted: selectWalletIsEncrypted(state),
|
walletEncrypted: selectWalletIsEncrypted(state),
|
||||||
autoDownload: makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state),
|
autoDownload: makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state),
|
||||||
userBlockedChannelsCount: selectBlockedChannelsCount(state),
|
|
||||||
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
|
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
|
||||||
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||||
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||||
|
@ -35,14 +33,14 @@ const select = state => ({
|
||||||
language: selectLanguage(state),
|
language: selectLanguage(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
|
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
|
||||||
clearDaemonSetting: key => dispatch(doClearDaemonSetting(key)),
|
clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)),
|
||||||
toggle3PAnalytics: allow => dispatch(doToggle3PAnalytics(allow)),
|
toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
|
||||||
clearCache: () => dispatch(doClearCache()),
|
clearCache: () => dispatch(doClearCache()),
|
||||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||||
updateWalletStatus: () => dispatch(doWalletStatus()),
|
updateWalletStatus: () => dispatch(doWalletStatus()),
|
||||||
confirmForgetPassword: modalProps => dispatch(doNotifyForgetPassword(modalProps)),
|
confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
|
||||||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||||
setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)),
|
setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)),
|
||||||
openModal: (id, params) => dispatch(doOpenModal(id, params)),
|
openModal: (id, params) => dispatch(doOpenModal(id, params)),
|
||||||
|
|
|
@ -44,9 +44,9 @@ type DaemonSettings = {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||||
clearDaemonSetting: string => void,
|
clearDaemonSetting: (string) => void,
|
||||||
setClientSetting: (string, SetDaemonSettingArg) => void,
|
setClientSetting: (string, SetDaemonSettingArg) => void,
|
||||||
toggle3PAnalytics: boolean => void,
|
toggle3PAnalytics: (boolean) => void,
|
||||||
clearCache: () => Promise<any>,
|
clearCache: () => Promise<any>,
|
||||||
daemonSettings: DaemonSettings,
|
daemonSettings: DaemonSettings,
|
||||||
allowAnalytics: boolean,
|
allowAnalytics: boolean,
|
||||||
|
@ -60,14 +60,13 @@ type Props = {
|
||||||
autoplay: boolean,
|
autoplay: boolean,
|
||||||
updateWalletStatus: () => void,
|
updateWalletStatus: () => void,
|
||||||
walletEncrypted: boolean,
|
walletEncrypted: boolean,
|
||||||
userBlockedChannelsCount?: number,
|
|
||||||
confirmForgetPassword: ({}) => void,
|
confirmForgetPassword: ({}) => void,
|
||||||
floatingPlayer: boolean,
|
floatingPlayer: boolean,
|
||||||
hideReposts: ?boolean,
|
hideReposts: ?boolean,
|
||||||
clearPlayingUri: () => void,
|
clearPlayingUri: () => void,
|
||||||
darkModeTimes: DarkModeTimes,
|
darkModeTimes: DarkModeTimes,
|
||||||
setDarkTime: (string, {}) => void,
|
setDarkTime: (string, {}) => void,
|
||||||
openModal: string => void,
|
openModal: (string) => void,
|
||||||
language?: string,
|
language?: string,
|
||||||
enterSettings: () => void,
|
enterSettings: () => void,
|
||||||
exitSettings: () => void,
|
exitSettings: () => void,
|
||||||
|
@ -98,7 +97,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
if (isAuthenticated || !IS_WEB) {
|
if (isAuthenticated || !IS_WEB) {
|
||||||
this.props.updateWalletStatus();
|
this.props.updateWalletStatus();
|
||||||
getPasswordFromCookie().then(p => {
|
getPasswordFromCookie().then((p) => {
|
||||||
if (typeof p === 'string') {
|
if (typeof p === 'string') {
|
||||||
this.setState({ storedPassword: true });
|
this.setState({ storedPassword: true });
|
||||||
}
|
}
|
||||||
|
@ -172,7 +171,6 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
setDaemonSetting,
|
setDaemonSetting,
|
||||||
setClientSetting,
|
setClientSetting,
|
||||||
toggle3PAnalytics,
|
toggle3PAnalytics,
|
||||||
userBlockedChannelsCount,
|
|
||||||
floatingPlayer,
|
floatingPlayer,
|
||||||
hideReposts,
|
hideReposts,
|
||||||
clearPlayingUri,
|
clearPlayingUri,
|
||||||
|
@ -262,7 +260,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
value={currentTheme}
|
value={currentTheme}
|
||||||
disabled={automaticDarkModeEnabled}
|
disabled={automaticDarkModeEnabled}
|
||||||
>
|
>
|
||||||
{themes.map(theme => (
|
{themes.map((theme) => (
|
||||||
<option key={theme} value={theme}>
|
<option key={theme} value={theme}>
|
||||||
{theme === 'light' ? __('Light') : __('Dark')}
|
{theme === 'light' ? __('Light') : __('Dark')}
|
||||||
</option>
|
</option>
|
||||||
|
@ -282,11 +280,11 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
<FormField
|
<FormField
|
||||||
type="select"
|
type="select"
|
||||||
name="automatic_dark_mode_range"
|
name="automatic_dark_mode_range"
|
||||||
onChange={value => this.onChangeTime(value, { fromTo: 'from', time: 'hour' })}
|
onChange={(value) => this.onChangeTime(value, { fromTo: 'from', time: 'hour' })}
|
||||||
value={darkModeTimes.from.hour}
|
value={darkModeTimes.from.hour}
|
||||||
label={__('From --[initial time]--')}
|
label={__('From --[initial time]--')}
|
||||||
>
|
>
|
||||||
{startHours.map(time => (
|
{startHours.map((time) => (
|
||||||
<option key={time} value={time}>
|
<option key={time} value={time}>
|
||||||
{this.to12Hour(time)}
|
{this.to12Hour(time)}
|
||||||
</option>
|
</option>
|
||||||
|
@ -296,10 +294,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
type="select"
|
type="select"
|
||||||
name="automatic_dark_mode_range"
|
name="automatic_dark_mode_range"
|
||||||
label={__('To --[final time]--')}
|
label={__('To --[final time]--')}
|
||||||
onChange={value => this.onChangeTime(value, { fromTo: 'to', time: 'hour' })}
|
onChange={(value) => this.onChangeTime(value, { fromTo: 'to', time: 'hour' })}
|
||||||
value={darkModeTimes.to.hour}
|
value={darkModeTimes.to.hour}
|
||||||
>
|
>
|
||||||
{endHours.map(time => (
|
{endHours.map((time) => (
|
||||||
<option key={time} value={time}>
|
<option key={time} value={time}>
|
||||||
{this.to12Hour(time)}
|
{this.to12Hour(time)}
|
||||||
</option>
|
</option>
|
||||||
|
@ -342,7 +340,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="hide_reposts"
|
name="hide_reposts"
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
|
let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
|
||||||
Lbryio.call('user_tag', 'edit', param);
|
Lbryio.call('user_tag', 'edit', param);
|
||||||
|
@ -411,7 +409,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="share_third_party"
|
name="share_third_party"
|
||||||
onChange={e => toggle3PAnalytics(e.target.checked)}
|
onChange={(e) => toggle3PAnalytics(e.target.checked)}
|
||||||
checked={allowAnalytics}
|
checked={allowAnalytics}
|
||||||
label={__('Allow the app to access third party analytics platforms')}
|
label={__('Allow the app to access third party analytics platforms')}
|
||||||
helper={__('We use detailed analytics to improve all aspects of the LBRY experience.')}
|
helper={__('We use detailed analytics to improve all aspects of the LBRY experience.')}
|
||||||
|
@ -438,21 +436,14 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title={__('Blocked channels')}
|
title={__('Blocked and muted channels')}
|
||||||
subtitle={
|
|
||||||
userBlockedChannelsCount === 0
|
|
||||||
? __("You don't have blocked channels.")
|
|
||||||
: userBlockedChannelsCount === 1
|
|
||||||
? __('You have one blocked channel.')
|
|
||||||
: __('You have %channels% blocked channels.', { channels: userBlockedChannelsCount })
|
|
||||||
}
|
|
||||||
actions={
|
actions={
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button
|
<Button
|
||||||
button="secondary"
|
button="secondary"
|
||||||
label={__('Manage')}
|
label={__('Manage')}
|
||||||
icon={ICONS.SETTINGS}
|
icon={ICONS.SETTINGS}
|
||||||
navigate={`/$/${PAGES.BLOCKED}`}
|
navigate={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
import { selectPrefsReady } from 'redux/selectors/sync';
|
import { selectPrefsReady } from 'redux/selectors/sync';
|
||||||
import { doAlertWaitingForSync } from 'redux/actions/app';
|
import { doAlertWaitingForSync } from 'redux/actions/app';
|
||||||
|
|
||||||
export const doToggleBlockChannel = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
|
export const doToggleMuteChannel = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const ready = selectPrefsReady(state);
|
const ready = selectPrefsReady(state);
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
// @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 } from 'lbry-redux';
|
import { Lbry, parseURI, buildURI, selectClaimsByUri, selectMyChannelClaims } 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,
|
||||||
|
selectModerationBlockList,
|
||||||
} 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';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
|
@ -50,7 +51,7 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_LIST_FAILED,
|
type: ACTIONS.COMMENT_LIST_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -89,7 +90,7 @@ export function doCommentReactList(uri: string | null, commentId?: string) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
|
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -171,14 +172,14 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
data: commentId + type,
|
data: commentId + type,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACT_FAILED,
|
type: ACTIONS.COMMENT_REACT_FAILED,
|
||||||
data: commentId + type,
|
data: commentId + type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const myRevertedReactsObj = myReacts
|
const myRevertedReactsObj = myReacts
|
||||||
.filter(el => el !== type)
|
.filter((el) => el !== type)
|
||||||
.reduce((acc, el) => {
|
.reduce((acc, el) => {
|
||||||
acc[el] = 1;
|
acc[el] = 1;
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -233,14 +234,20 @@ export function doCommentCreate(comment: string = '', claim_id: string = '', par
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_CREATE_FAILED,
|
type: ACTIONS.COMMENT_CREATE_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let toastMessage = __('Unable to create comment, please try again later.');
|
||||||
|
if (error && error.message === 'channel is blocked by publisher') {
|
||||||
|
toastMessage = __('Unable to comment. This channel has blocked you.');
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
doToast({
|
doToast({
|
||||||
message: 'Unable to create comment, please try again later.',
|
message: toastMessage,
|
||||||
isError: true,
|
isError: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -274,7 +281,7 @@ export function doCommentPin(commentId: string, remove: boolean) {
|
||||||
data: result,
|
data: result,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_PIN_FAILED,
|
type: ACTIONS.COMMENT_PIN_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -339,7 +346,7 @@ export function doCommentAbandon(commentId: string, creatorChannelUri?: string)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_ABANDON_FAILED,
|
type: ACTIONS.COMMENT_ABANDON_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -389,7 +396,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_UPDATE_FAILED,
|
type: ACTIONS.COMMENT_UPDATE_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -406,38 +413,203 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hides a users comments from all creator's claims and prevent them from commenting in the future
|
// Hides a users comments from all creator's claims and prevent them from commenting in the future
|
||||||
export function doCommentModBlock(commentAuthor: string) {
|
export function doCommentModToggleBlock(channelUri: string, unblock: boolean = false) {
|
||||||
return async (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const claim = selectClaimsByUri(state)[commentAuthor];
|
const myChannels = selectMyChannelClaims(state);
|
||||||
|
const claim = selectClaimsByUri(state)[channelUri];
|
||||||
|
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
console.error("Can't find claim to block"); // eslint-disable-line
|
console.error("Can't find claim to block"); // eslint-disable-line
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const creatorIdToBan = claim ? claim.claim_id : null;
|
dispatch({
|
||||||
const creatorNameToBan = claim ? claim.name : null;
|
type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED : ACTIONS.COMMENT_MODERATION_BLOCK_STARTED,
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
data: {
|
||||||
|
uri: channelUri,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
let channelSignature = {};
|
const creatorIdForAction = claim ? claim.claim_id : null;
|
||||||
if (activeChannelClaim) {
|
const creatorNameForAction = claim ? claim.name : null;
|
||||||
try {
|
|
||||||
channelSignature = await Lbry.channel_sign({
|
let channelSignatures = [];
|
||||||
channel_id: activeChannelClaim.claim_id,
|
if (myChannels) {
|
||||||
hexdata: toHex(activeChannelClaim.name),
|
for (const channelClaim of myChannels) {
|
||||||
});
|
try {
|
||||||
} catch (e) {}
|
const channelSignature = await Lbry.channel_sign({
|
||||||
|
channel_id: channelClaim.claim_id,
|
||||||
|
hexdata: toHex(channelClaim.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Comments.moderation_block({
|
const sharedModBlockParams = unblock
|
||||||
mod_channel_id: activeChannelClaim.claim_id,
|
? {
|
||||||
mod_channel_name: activeChannelClaim.name,
|
un_blocked_channel_id: creatorIdForAction,
|
||||||
signature: channelSignature.signature,
|
un_blocked_channel_name: creatorNameForAction,
|
||||||
signing_ts: channelSignature.signing_ts,
|
}
|
||||||
banned_channel_id: creatorIdToBan,
|
: {
|
||||||
banned_channel_name: creatorNameToBan,
|
blocked_channel_id: creatorIdForAction,
|
||||||
delete_all: true,
|
blocked_channel_name: creatorNameForAction,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const commentAction = unblock ? Comments.moderation_unblock : Comments.moderation_block;
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
channelSignatures.map((signatureData) =>
|
||||||
|
commentAction({
|
||||||
|
mod_channel_id: signatureData.claim_id,
|
||||||
|
mod_channel_name: signatureData.name,
|
||||||
|
signature: signatureData.signature,
|
||||||
|
signing_ts: signatureData.signing_ts,
|
||||||
|
...sharedModBlockParams,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
dispatch({
|
||||||
|
type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_COMPLETE : ACTIONS.COMMENT_MODERATION_BLOCK_COMPLETE,
|
||||||
|
data: { channelUri },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!unblock) {
|
||||||
|
dispatch(doToast({ message: __('Channel blocked. You will not see them again.') }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch({
|
||||||
|
type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED : ACTIONS.COMMENT_MODERATION_BLOCK_FAILED,
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doCommentModBlock(commentAuthor: string) {
|
||||||
|
return (dispatch: Dispatch) => {
|
||||||
|
return dispatch(doCommentModToggleBlock(commentAuthor));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doCommentModUnBlock(commentAuthor: string) {
|
||||||
|
return (dispatch: Dispatch) => {
|
||||||
|
return dispatch(doCommentModToggleBlock(commentAuthor, true));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doFetchModBlockedList() {
|
||||||
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const myChannels = selectMyChannelClaims(state);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
let channelSignatures = [];
|
||||||
|
if (myChannels) {
|
||||||
|
for (const channelClaim of myChannels) {
|
||||||
|
try {
|
||||||
|
const channelSignature = await Lbry.channel_sign({
|
||||||
|
channel_id: channelClaim.claim_id,
|
||||||
|
hexdata: toHex(channelClaim.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
channelSignatures.map((signatureData) =>
|
||||||
|
Comments.moderation_block_list({
|
||||||
|
mod_channel_id: signatureData.claim_id,
|
||||||
|
mod_channel_name: signatureData.name,
|
||||||
|
signature: signatureData.signature,
|
||||||
|
signing_ts: signatureData.signing_ts,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then((blockLists) => {
|
||||||
|
let globalBlockList = [];
|
||||||
|
blockLists
|
||||||
|
.sort((a, b) => {
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.forEach((channelBlockListData) => {
|
||||||
|
const blockListForChannel = channelBlockListData && channelBlockListData.blocked_channels;
|
||||||
|
if (blockListForChannel) {
|
||||||
|
blockListForChannel.forEach((blockedChannel) => {
|
||||||
|
if (blockedChannel.blocked_channel_name) {
|
||||||
|
const channelUri = buildURI({
|
||||||
|
channelName: blockedChannel.blocked_channel_name,
|
||||||
|
claimId: blockedChannel.blocked_channel_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!globalBlockList.find((blockedChannel) => blockedChannel.channelUri === channelUri)) {
|
||||||
|
globalBlockList.push({ channelUri, blockedAt: blockedChannel.blocked_at });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED,
|
||||||
|
data: {
|
||||||
|
blockList: globalBlockList
|
||||||
|
.sort((a, b) => new Date(a.blockedAt) - new Date(b.blockedAt))
|
||||||
|
.map((blockedChannel) => blockedChannel.channelUri),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_FAILED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const doUpdateBlockListForPublishedChannel = (channelClaim: ChannelClaim) => {
|
||||||
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const blockedUris = selectModerationBlockList(state);
|
||||||
|
|
||||||
|
let channelSignature: ?{
|
||||||
|
signature: string,
|
||||||
|
signing_ts: string,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
channelSignature = await Lbry.channel_sign({
|
||||||
|
channel_id: channelClaim.claim_id,
|
||||||
|
hexdata: toHex(channelClaim.name),
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (!channelSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
blockedUris.map((uri) => {
|
||||||
|
const { channelName, channelClaimId } = parseURI(uri);
|
||||||
|
|
||||||
|
return Comments.moderation_block({
|
||||||
|
mod_channel_id: channelClaim.claim_id,
|
||||||
|
mod_channel_name: channelClaim.name,
|
||||||
|
// $FlowFixMe
|
||||||
|
signature: channelSignature.signature,
|
||||||
|
// $FlowFixMe
|
||||||
|
signing_ts: channelSignature.signing_ts,
|
||||||
|
blocked_channel_id: channelClaimId,
|
||||||
|
blocked_channel_name: channelName,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -15,9 +15,9 @@ export default handleActions(
|
||||||
let newBlockedChannels = blockedChannels.slice();
|
let newBlockedChannels = blockedChannels.slice();
|
||||||
|
|
||||||
if (newBlockedChannels.includes(uri)) {
|
if (newBlockedChannels.includes(uri)) {
|
||||||
newBlockedChannels = newBlockedChannels.filter(id => id !== uri);
|
newBlockedChannels = newBlockedChannels.filter((id) => id !== uri);
|
||||||
} else {
|
} else {
|
||||||
newBlockedChannels.push(uri);
|
newBlockedChannels.unshift(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -29,7 +29,7 @@ export default handleActions(
|
||||||
action: { data: { blocked: ?Array<string> } }
|
action: { data: { blocked: ?Array<string> } }
|
||||||
) => {
|
) => {
|
||||||
const { blocked } = action.data;
|
const { blocked } = action.data;
|
||||||
const sanitizedBlocked = blocked && blocked.filter(e => typeof e === 'string');
|
const sanitizedBlocked = blocked && blocked.filter((e) => typeof e === 'string');
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
blockedChannels: sanitizedBlocked && sanitizedBlocked.length ? sanitizedBlocked : state.blockedChannels,
|
blockedChannels: sanitizedBlocked && sanitizedBlocked.length ? sanitizedBlocked : state.blockedChannels,
|
||||||
|
|
|
@ -16,6 +16,10 @@ const defaultState: CommentsState = {
|
||||||
typesReacting: [],
|
typesReacting: [],
|
||||||
myReactsByCommentId: undefined,
|
myReactsByCommentId: undefined,
|
||||||
othersReactsByCommentId: undefined,
|
othersReactsByCommentId: undefined,
|
||||||
|
moderationBlockList: undefined,
|
||||||
|
fetchingModerationBlockList: false,
|
||||||
|
blockingByUri: {},
|
||||||
|
unBlockingByUri: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
|
@ -144,7 +148,7 @@ export default handleActions(
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_LIST_STARTED]: state => ({ ...state, isLoading: true }),
|
[ACTIONS.COMMENT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }),
|
||||||
|
|
||||||
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
|
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
const { comments, claimId, uri } = action.data;
|
const { comments, claimId, uri } = action.data;
|
||||||
|
@ -227,17 +231,15 @@ export default handleActions(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// do nothing
|
|
||||||
[ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isCommenting: false,
|
isCommenting: false,
|
||||||
}),
|
}),
|
||||||
// do nothing
|
|
||||||
[ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isCommenting: true,
|
isCommenting: true,
|
||||||
}),
|
}),
|
||||||
// replace existing comment with comment returned here under its comment_id
|
|
||||||
[ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => {
|
[ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
const { comment } = action.data;
|
const { comment } = action.data;
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
|
@ -249,25 +251,100 @@ export default handleActions(
|
||||||
isCommenting: false,
|
isCommenting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// nothing can be done here
|
|
||||||
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isCmmenting: false,
|
isCmmenting: false,
|
||||||
}),
|
}),
|
||||||
// nothing can really be done here
|
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED]: (state: CommentsState, action: any) => ({
|
||||||
[ACTIONS.COMMENT_HIDE_STARTED]: (state: CommentsState, action: any) => ({
|
|
||||||
...state,
|
...state,
|
||||||
isLoading: true,
|
fetchingModerationBlockList: true,
|
||||||
}),
|
}),
|
||||||
[ACTIONS.COMMENT_HIDE_COMPLETED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
...state, // todo: add HiddenComments state & create selectors
|
const { blockList } = action.data;
|
||||||
isLoading: false,
|
|
||||||
}),
|
return {
|
||||||
// nothing can be done here
|
...state,
|
||||||
[ACTIONS.COMMENT_HIDE_FAILED]: (state: CommentsState, action: any) => ({
|
moderationBlockList: blockList,
|
||||||
|
fetchingModerationBlockList: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isLoading: false,
|
fetchingModerationBlockList: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_MODERATION_BLOCK_STARTED]: (state: CommentsState, action: any) => ({
|
||||||
|
...state,
|
||||||
|
blockingByUri: {
|
||||||
|
...state.blockingByUri,
|
||||||
|
[action.data.uri]: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED]: (state: CommentsState, action: any) => ({
|
||||||
|
...state,
|
||||||
|
unBlockingByUri: {
|
||||||
|
...state.unBlockingByUri,
|
||||||
|
[action.data.uri]: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[ACTIONS.COMMENT_MODERATION_BLOCK_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
|
...state,
|
||||||
|
blockingByUri: {
|
||||||
|
...state.blockingByUri,
|
||||||
|
[action.data.uri]: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
|
...state,
|
||||||
|
unBlockingByUri: {
|
||||||
|
...state.unBlockingByUri,
|
||||||
|
[action.data.uri]: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_MODERATION_BLOCK_COMPLETE]: (state: CommentsState, action: any) => {
|
||||||
|
const { channelUri } = action.data;
|
||||||
|
const commentById = Object.assign({}, state.commentById);
|
||||||
|
const blockingByUri = Object.assign({}, state.blockingByUri);
|
||||||
|
const moderationBlockList = state.moderationBlockList || [];
|
||||||
|
const newModerationBlockList = moderationBlockList.slice();
|
||||||
|
|
||||||
|
for (const commentId in commentById) {
|
||||||
|
const comment = commentById[commentId];
|
||||||
|
|
||||||
|
if (channelUri === comment.channel_url) {
|
||||||
|
delete commentById[comment.comment_id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete blockingByUri[channelUri];
|
||||||
|
|
||||||
|
newModerationBlockList.push(channelUri);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
commentById,
|
||||||
|
blockingByUri,
|
||||||
|
moderationBlockList: newModerationBlockList,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ACTIONS.COMMENT_MODERATION_UN_BLOCK_COMPLETE]: (state: CommentsState, action: any) => {
|
||||||
|
const { channelUri } = action.data;
|
||||||
|
const unBlockingByUri = Object.assign(state.unBlockingByUri, {});
|
||||||
|
const moderationBlockList = state.moderationBlockList || [];
|
||||||
|
const newModerationBlockList = moderationBlockList.slice().filter((uri) => uri !== channelUri);
|
||||||
|
|
||||||
|
delete unBlockingByUri[channelUri];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
unBlockingByUri,
|
||||||
|
moderationBlockList: newModerationBlockList,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultState
|
defaultState
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,23 +3,11 @@ import { createSelector } from 'reselect';
|
||||||
|
|
||||||
const selectState = (state: { blocked: BlocklistState }) => state.blocked || {};
|
const selectState = (state: { blocked: BlocklistState }) => state.blocked || {};
|
||||||
|
|
||||||
export const selectBlockedChannels = createSelector(selectState, (state: BlocklistState) => {
|
export const selectMutedChannels = createSelector(selectState, (state: BlocklistState) => {
|
||||||
return state.blockedChannels.filter(e => typeof e === 'string');
|
return state.blockedChannels.filter((e) => typeof e === 'string');
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectBlockedChannelsCount = createSelector(selectBlockedChannels, (state: Array<string>) => state.length);
|
export const makeSelectChannelIsMuted = (uri: string) =>
|
||||||
|
createSelector(selectMutedChannels, (state: Array<string>) => {
|
||||||
export const selectBlockedChannelsObj = createSelector(selectState, (state: BlocklistState) => {
|
|
||||||
return state.blockedChannels.reduce((acc: any, val: any) => {
|
|
||||||
const outpoint = `${val.txid}:${String(val.nout)}`;
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[outpoint]: 1,
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const selectChannelIsBlocked = (uri: string) =>
|
|
||||||
createSelector(selectBlockedChannels, (state: Array<string>) => {
|
|
||||||
return state.includes(uri);
|
return state.includes(uri);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,22 +1,27 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||||
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
|
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
|
||||||
|
|
||||||
const selectState = state => state.comments || {};
|
const selectState = (state) => state.comments || {};
|
||||||
|
|
||||||
export const selectCommentsById = createSelector(selectState, state => state.commentById || {});
|
export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {});
|
||||||
|
export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading);
|
||||||
export const selectIsFetchingComments = createSelector(selectState, state => state.isLoading);
|
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
|
||||||
|
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
|
||||||
export const selectIsPostingComment = createSelector(selectState, state => state.isCommenting);
|
export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
|
||||||
|
export const selectModerationBlockList = createSelector(selectState, (state) =>
|
||||||
export const selectIsFetchingReacts = createSelector(selectState, state => state.isFetchingReacts);
|
state.moderationBlockList ? state.moderationBlockList.reverse() : []
|
||||||
|
);
|
||||||
export const selectOthersReactsById = createSelector(selectState, state => state.othersReactsByCommentId);
|
export const selectBlockingByUri = createSelector(selectState, (state) => state.blockingByUri);
|
||||||
|
export const selectUnBlockingByUri = createSelector(selectState, (state) => state.unBlockingByUri);
|
||||||
|
export const selectFetchingModerationBlockList = createSelector(
|
||||||
|
selectState,
|
||||||
|
(state) => state.fetchingModerationBlockList
|
||||||
|
);
|
||||||
|
|
||||||
export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||||
const byClaimId = state.byId || {};
|
const byClaimId = state.byId || {};
|
||||||
|
@ -40,7 +45,7 @@ export const selectTopLevelCommentsByClaimId = createSelector(selectState, selec
|
||||||
const comments = {};
|
const comments = {};
|
||||||
|
|
||||||
// replace every comment_id in the list with the actual comment object
|
// replace every comment_id in the list with the actual comment object
|
||||||
Object.keys(byClaimId).forEach(claimId => {
|
Object.keys(byClaimId).forEach((claimId) => {
|
||||||
const commentIds = byClaimId[claimId];
|
const commentIds = byClaimId[claimId];
|
||||||
|
|
||||||
comments[claimId] = Array(commentIds === null ? 0 : commentIds.length);
|
comments[claimId] = Array(commentIds === null ? 0 : commentIds.length);
|
||||||
|
@ -53,14 +58,14 @@ export const selectTopLevelCommentsByClaimId = createSelector(selectState, selec
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectCommentForCommentId = (commentId: string) =>
|
export const makeSelectCommentForCommentId = (commentId: string) =>
|
||||||
createSelector(selectCommentsById, comments => comments[commentId]);
|
createSelector(selectCommentsById, (comments) => comments[commentId]);
|
||||||
|
|
||||||
export const selectRepliesByParentId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
export const selectRepliesByParentId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||||
const byParentId = state.repliesByParentId || {};
|
const byParentId = state.repliesByParentId || {};
|
||||||
const comments = {};
|
const comments = {};
|
||||||
|
|
||||||
// replace every comment_id in the list with the actual comment object
|
// replace every comment_id in the list with the actual comment object
|
||||||
Object.keys(byParentId).forEach(id => {
|
Object.keys(byParentId).forEach((id) => {
|
||||||
const commentIds = byParentId[id];
|
const commentIds = byParentId[id];
|
||||||
|
|
||||||
comments[id] = Array(commentIds === null ? 0 : commentIds.length);
|
comments[id] = Array(commentIds === null ? 0 : commentIds.length);
|
||||||
|
@ -77,10 +82,10 @@ export const selectRepliesByParentId = createSelector(selectState, selectComment
|
||||||
selectState,
|
selectState,
|
||||||
state => state.byId || {}
|
state => state.byId || {}
|
||||||
); */
|
); */
|
||||||
export const selectCommentsByUri = createSelector(selectState, state => {
|
export const selectCommentsByUri = createSelector(selectState, (state) => {
|
||||||
const byUri = state.commentsByUri || {};
|
const byUri = state.commentsByUri || {};
|
||||||
const comments = {};
|
const comments = {};
|
||||||
Object.keys(byUri).forEach(uri => {
|
Object.keys(byUri).forEach((uri) => {
|
||||||
const claimId = byUri[uri];
|
const claimId = byUri[uri];
|
||||||
if (claimId === null) {
|
if (claimId === null) {
|
||||||
comments[uri] = null;
|
comments[uri] = null;
|
||||||
|
@ -99,16 +104,16 @@ export const makeSelectCommentIdsForUri = (uri: string) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectMyReactionsForComment = (commentId: string) =>
|
export const makeSelectMyReactionsForComment = (commentId: string) =>
|
||||||
createSelector(selectState, state => {
|
createSelector(selectState, (state) => {
|
||||||
return state.myReactsByCommentId[commentId] || [];
|
return state.myReactsByCommentId[commentId] || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectOthersReactionsForComment = (commentId: string) =>
|
export const makeSelectOthersReactionsForComment = (commentId: string) =>
|
||||||
createSelector(selectState, state => {
|
createSelector(selectState, (state) => {
|
||||||
return state.othersReactsByCommentId[commentId];
|
return state.othersReactsByCommentId[commentId];
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectPendingCommentReacts = createSelector(selectState, state => state.pendingCommentReactions);
|
export const selectPendingCommentReacts = createSelector(selectState, (state) => state.pendingCommentReactions);
|
||||||
|
|
||||||
export const makeSelectCommentsForUri = (uri: string) =>
|
export const makeSelectCommentsForUri = (uri: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
|
@ -116,7 +121,7 @@ export const makeSelectCommentsForUri = (uri: string) =>
|
||||||
selectCommentsByUri,
|
selectCommentsByUri,
|
||||||
selectClaimsById,
|
selectClaimsById,
|
||||||
selectMyActiveClaims,
|
selectMyActiveClaims,
|
||||||
selectBlockedChannels,
|
selectMutedChannels,
|
||||||
selectBlacklistedOutpointMap,
|
selectBlacklistedOutpointMap,
|
||||||
selectFilteredOutpointMap,
|
selectFilteredOutpointMap,
|
||||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||||
|
@ -125,7 +130,12 @@ export const makeSelectCommentsForUri = (uri: string) =>
|
||||||
const comments = byClaimId && byClaimId[claimId];
|
const comments = byClaimId && byClaimId[claimId];
|
||||||
|
|
||||||
return comments
|
return comments
|
||||||
? comments.filter(comment => {
|
? comments.filter((comment) => {
|
||||||
|
if (!comment) {
|
||||||
|
// It may have been recently deleted after being blocked
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const channelClaim = claimsById[comment.channel_id];
|
const channelClaim = claimsById[comment.channel_id];
|
||||||
|
|
||||||
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
||||||
|
@ -162,7 +172,7 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) =>
|
||||||
selectCommentsByUri,
|
selectCommentsByUri,
|
||||||
selectClaimsById,
|
selectClaimsById,
|
||||||
selectMyActiveClaims,
|
selectMyActiveClaims,
|
||||||
selectBlockedChannels,
|
selectMutedChannels,
|
||||||
selectBlacklistedOutpointMap,
|
selectBlacklistedOutpointMap,
|
||||||
selectFilteredOutpointMap,
|
selectFilteredOutpointMap,
|
||||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||||
|
@ -171,7 +181,7 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) =>
|
||||||
const comments = byClaimId && byClaimId[claimId];
|
const comments = byClaimId && byClaimId[claimId];
|
||||||
|
|
||||||
return comments
|
return comments
|
||||||
? comments.filter(comment => {
|
? comments.filter((comment) => {
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -212,7 +222,7 @@ export const makeSelectRepliesForParentId = (id: string) =>
|
||||||
selectCommentsById,
|
selectCommentsById,
|
||||||
selectClaimsById,
|
selectClaimsById,
|
||||||
selectMyActiveClaims,
|
selectMyActiveClaims,
|
||||||
selectBlockedChannels,
|
selectMutedChannels,
|
||||||
selectBlacklistedOutpointMap,
|
selectBlacklistedOutpointMap,
|
||||||
selectFilteredOutpointMap,
|
selectFilteredOutpointMap,
|
||||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||||
|
@ -223,13 +233,13 @@ export const makeSelectRepliesForParentId = (id: string) =>
|
||||||
if (!replyIdsForParent.length) return null;
|
if (!replyIdsForParent.length) return null;
|
||||||
|
|
||||||
const comments = [];
|
const comments = [];
|
||||||
replyIdsForParent.forEach(cid => {
|
replyIdsForParent.forEach((cid) => {
|
||||||
comments.push(commentsById[cid]);
|
comments.push(commentsById[cid]);
|
||||||
});
|
});
|
||||||
// const comments = byParentId && byParentId[id];
|
// const comments = byParentId && byParentId[id];
|
||||||
|
|
||||||
return comments
|
return comments
|
||||||
? comments.filter(comment => {
|
? comments.filter((comment) => {
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -265,6 +275,20 @@ export const makeSelectRepliesForParentId = (id: string) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const makeSelectTotalCommentsCountForUri = (uri: string) =>
|
export const makeSelectTotalCommentsCountForUri = (uri: string) =>
|
||||||
createSelector(makeSelectCommentsForUri(uri), comments => {
|
createSelector(makeSelectCommentsForUri(uri), (comments) => {
|
||||||
return comments ? comments.length : 0;
|
return comments ? comments.length : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const makeSelectChannelIsBlocked = (uri: string) =>
|
||||||
|
createSelector(selectModerationBlockList, (blockedChannelUris) => {
|
||||||
|
if (!blockedChannelUris || !blockedChannelUris) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockedChannelUris.includes(uri);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) =>
|
||||||
|
createSelector(selectBlockingByUri, selectUnBlockingByUri, (blockingByUri, unBlockingByUri) => {
|
||||||
|
return blockingByUri[uri] || unBlockingByUri[uri];
|
||||||
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
makeSelectFileNameForUri,
|
makeSelectFileNameForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
|
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
|
@ -81,7 +81,7 @@ export const makeSelectNextUnplayedRecommended = (uri: string) =>
|
||||||
selectHistory,
|
selectHistory,
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
selectAllCostInfoByUri,
|
selectAllCostInfoByUri,
|
||||||
selectBlockedChannels,
|
selectMutedChannels,
|
||||||
(
|
(
|
||||||
recommendedForUri: Array<string>,
|
recommendedForUri: Array<string>,
|
||||||
history: Array<{ uri: string }>,
|
history: Array<{ uri: string }>,
|
||||||
|
|
|
@ -372,9 +372,9 @@ $metadata-z-index: 1;
|
||||||
.channel-staked__wrapper {
|
.channel-staked__wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0.25rem;
|
padding: 0.2rem;
|
||||||
bottom: -0.75rem;
|
bottom: -0.75rem;
|
||||||
left: -0.75rem;
|
left: -0.8rem;
|
||||||
background-color: var(--color-card-background);
|
background-color: var(--color-card-background);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
@ -390,6 +390,16 @@ $metadata-z-index: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-staked__wrapper--inline {
|
||||||
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
display: inline-block;
|
||||||
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
top: var(--spacing-xs);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-staked__indicator {
|
.channel-staked__indicator {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
|
|
@ -64,6 +64,12 @@
|
||||||
.claim-preview__wrapper {
|
.claim-preview__wrapper {
|
||||||
padding: var(--spacing-m);
|
padding: var(--spacing-m);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.claim__menu-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-preview__wrapper--notice {
|
.claim-preview__wrapper--notice {
|
||||||
|
@ -247,17 +253,6 @@
|
||||||
|
|
||||||
.claim-preview-info {
|
.claim-preview-info {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
.channel-thumbnail {
|
|
||||||
display: none;
|
|
||||||
@include handleChannelGif(1.4rem);
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: var(--spacing-s);
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-preview-info,
|
.claim-preview-info,
|
||||||
|
@ -408,6 +403,10 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
.claim__menu-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-large) {
|
@media (min-width: $breakpoint-large) {
|
||||||
|
@ -456,12 +455,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-tile__title {
|
.claim-tile__title {
|
||||||
margin: var(--spacing-s);
|
position: relative;
|
||||||
|
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
|
|
||||||
|
.claim__menu-button {
|
||||||
|
right: 0.2rem;
|
||||||
|
top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
@ -571,3 +581,36 @@
|
||||||
.claim-preview__null-label {
|
.claim-preview__null-label {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.claim-preview__channel-staked {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.channel-thumbnail {
|
||||||
|
@include handleChannelGif(1.4rem);
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim__menu-button {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--spacing-xs);
|
||||||
|
right: var(--spacing-xs);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
stroke: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([aria-expanded='true']) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim__menu-button--inline {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
right: auto;
|
||||||
|
top: auto;
|
||||||
|
@extend .button--alt;
|
||||||
|
padding: 0 var(--spacing-xxs);
|
||||||
|
}
|
||||||
|
|
|
@ -43,6 +43,15 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu__button {
|
||||||
|
&:hover {
|
||||||
|
.icon {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menu__title {
|
.menu__title {
|
||||||
&[aria-expanded='true'] {
|
&[aria-expanded='true'] {
|
||||||
background-color: var(--color-primary-alt);
|
background-color: var(--color-primary-alt);
|
||||||
|
@ -54,16 +63,7 @@
|
||||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||||
border-bottom-left-radius: var(--border-radius);
|
border-bottom-left-radius: var(--border-radius);
|
||||||
border-bottom-right-radius: var(--border-radius);
|
border-bottom-right-radius: var(--border-radius);
|
||||||
}
|
|
||||||
|
|
||||||
.menu__list--header {
|
|
||||||
@extend .menu__list;
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
margin-top: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu__list--comments {
|
|
||||||
@extend .menu__list;
|
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: var(--spacing-xs) 0;
|
padding: var(--spacing-xs) 0;
|
||||||
|
@ -73,6 +73,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu__list--header {
|
||||||
|
@extend .menu__list;
|
||||||
|
margin-top: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__list--comments {
|
||||||
|
@extend .menu__list;
|
||||||
|
}
|
||||||
|
|
||||||
.menu__link {
|
.menu__link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
--color-button-primary-bg-hover: var(--color-primary-alt-2);
|
--color-button-primary-bg-hover: var(--color-primary-alt-2);
|
||||||
--color-button-primary-hover-text: var(--color-primary-alt);
|
--color-button-primary-hover-text: var(--color-primary-alt);
|
||||||
--color-button-secondary-bg: var(--color-secondary-alt);
|
--color-button-secondary-bg: var(--color-secondary-alt);
|
||||||
--color-button-secondary-border: #c5d7ed;
|
--color-button-secondary-border: var(--color-secondary-alt);
|
||||||
--color-button-secondary-text: var(--color-secondary);
|
--color-button-secondary-text: var(--color-secondary);
|
||||||
--color-button-secondary-bg-hover: #b9d0e9;
|
--color-button-secondary-bg-hover: #b9d0e9;
|
||||||
--color-button-alt-bg: var(--color-gray-1);
|
--color-button-alt-bg: var(--color-gray-1);
|
||||||
|
|
|
@ -262,6 +262,12 @@ textarea {
|
||||||
background-color: var(--color-help-warning-bg);
|
background-color: var(--color-help-warning-bg);
|
||||||
color: var(--color-help-warning-text);
|
color: var(--color-help-warning-text);
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help--notice {
|
||||||
|
@extend .help--warning;
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.help--inline {
|
.help--inline {
|
||||||
|
|
|
@ -24,9 +24,9 @@ function isNotFunction(object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBulkThunkMiddleware() {
|
function createBulkThunkMiddleware() {
|
||||||
return ({ dispatch, getState }) => next => action => {
|
return ({ dispatch, getState }) => (next) => (action) => {
|
||||||
if (action.type === 'BATCH_ACTIONS') {
|
if (action.type === 'BATCH_ACTIONS') {
|
||||||
action.actions.filter(isFunction).map(actionFn => actionFn(dispatch, getState));
|
action.actions.filter(isFunction).map((actionFn) => actionFn(dispatch, getState));
|
||||||
}
|
}
|
||||||
return next(action);
|
return next(action);
|
||||||
};
|
};
|
||||||
|
@ -68,7 +68,6 @@ const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
||||||
const blockedFilter = createFilter('blocked', ['blockedChannels']);
|
const blockedFilter = createFilter('blocked', ['blockedChannels']);
|
||||||
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
|
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
|
||||||
const whiteListedReducers = [
|
const whiteListedReducers = [
|
||||||
'comments',
|
|
||||||
'fileInfo',
|
'fileInfo',
|
||||||
'publish',
|
'publish',
|
||||||
'wallet',
|
'wallet',
|
||||||
|
@ -143,7 +142,7 @@ const sharedStateFilters = {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
source: 'subscriptions',
|
source: 'subscriptions',
|
||||||
property: 'subscriptions',
|
property: 'subscriptions',
|
||||||
transform: function(value) {
|
transform: (value) => {
|
||||||
return value.map(({ uri }) => uri);
|
return value.map(({ uri }) => uri);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -162,7 +161,7 @@ const sharedStateCb = ({ dispatch, getState }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const populateAuthTokenHeader = () => {
|
const populateAuthTokenHeader = () => {
|
||||||
return next => action => {
|
return (next) => (action) => {
|
||||||
if (
|
if (
|
||||||
(action.type === ACTIONS.USER_FETCH_SUCCESS || action.type === ACTIONS.AUTHENTICATION_SUCCESS) &&
|
(action.type === ACTIONS.USER_FETCH_SUCCESS || action.type === ACTIONS.AUTHENTICATION_SUCCESS) &&
|
||||||
action.data.user.has_verified_email === true
|
action.data.user.has_verified_email === true
|
||||||
|
|
|
@ -1,8 +1,46 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
export function toHex(str: string): string {
|
export function toHex(str: string): string {
|
||||||
var result = '';
|
const array = Array.from(str);
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
result += str.charCodeAt(i).toString(16);
|
let result = '';
|
||||||
|
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
const val = array[i];
|
||||||
|
const utf = toUTF8Array(val)
|
||||||
|
.map((num) => num.toString(16))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
result += utf;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://gist.github.com/joni/3760795
|
||||||
|
// See comment that fixes an issue in the original gist
|
||||||
|
function toUTF8Array(str: string): Array<number> {
|
||||||
|
var utf8 = [];
|
||||||
|
for (var i = 0; i < str.length; i++) {
|
||||||
|
var charcode = str.charCodeAt(i);
|
||||||
|
if (charcode < 0x80) utf8.push(charcode);
|
||||||
|
else if (charcode < 0x800) {
|
||||||
|
utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
|
||||||
|
} else if (charcode < 0xd800 || charcode >= 0xe000) {
|
||||||
|
utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f));
|
||||||
|
}
|
||||||
|
// surrogate pair
|
||||||
|
else {
|
||||||
|
i++;
|
||||||
|
charcode = (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)) + 0x010000;
|
||||||
|
utf8.push(
|
||||||
|
0xf0 | (charcode >> 18),
|
||||||
|
0x80 | ((charcode >> 12) & 0x3f),
|
||||||
|
0x80 | ((charcode >> 6) & 0x3f),
|
||||||
|
0x80 | (charcode & 0x3f)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utf8;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue