lbry-desktop/ui/page/listBlocked/view.jsx
jessopb ca0cd2ca75
Use 'selectHasChannel' instead of the full 'selectMyChannelClaims' (#7427)
- selectMyChannelClaims depends on `byId`, which currently is always invalidated per update, so it is not memoized.

- Most of the use-cases just needs the ID or the length of the array anyways, so avoid generating a Claim array (in selectMyChannelClaims) unnecessarily -- the client need to reduce it back down to IDs again :/

- The simpler boolean also removes the need to memoize the selector, which saves a bit of memory.

Co-authored-by: infinite-persistence <inf.persistence@gmail.com>
2022-01-21 12:38:11 -05:00

285 lines
9 KiB
JavaScript

// @flow
import * as ICONS from 'constants/icons';
import { BLOCK_LEVEL } from 'constants/comment';
import React from 'react';
import classnames from 'classnames';
import moment from 'moment';
import humanizeDuration from 'humanize-duration';
import BlockList from 'component/blockList';
import ClaimPreview from 'component/claimPreview';
import Page from 'component/page';
import Spinner from 'component/spinner';
import Button from 'component/button';
import usePersistedState from 'effects/use-persisted-state';
import ChannelBlockButton from 'component/channelBlockButton';
import ChannelMuteButton from 'component/channelMuteButton';
const VIEW = {
BLOCKED: 'blocked',
ADMIN: 'admin',
MODERATOR: 'moderator',
MUTED: 'muted',
};
type Props = {
mutedUris: ?Array<string>,
personalBlockList: ?Array<string>,
adminBlockList: ?Array<string>,
moderatorBlockList: ?Array<string>,
personalTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } },
adminTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } },
moderatorTimeoutMap: { [uri: string]: { blockedAt: string, bannedFor: number, banRemaining: number } },
moderatorBlockListDelegatorsMap: { [string]: Array<string> },
fetchingModerationBlockList: boolean,
fetchModBlockedList: () => void,
fetchModAmIList: () => void,
delegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } },
myChannelClaimIds: ?Array<string>,
};
function ListBlocked(props: Props) {
const {
mutedUris,
personalBlockList,
adminBlockList,
moderatorBlockList,
personalTimeoutMap,
adminTimeoutMap,
moderatorTimeoutMap,
moderatorBlockListDelegatorsMap: delegatorsMap,
fetchingModerationBlockList,
fetchModBlockedList,
fetchModAmIList,
delegatorsById,
myChannelClaimIds,
} = props;
const [viewMode, setViewMode] = usePersistedState('blocked-muted:display', VIEW.BLOCKED);
const [localDelegatorsMap, setLocalDelegatorsMap] = React.useState(undefined);
const stringifiedDelegatorsMap = JSON.stringify(delegatorsMap);
const stringifiedLocalDelegatorsMap = JSON.stringify(localDelegatorsMap);
const isAdmin = myChannelClaimIds && myChannelClaimIds.some((id) => delegatorsById[id] && delegatorsById[id].global);
const isModerator =
myChannelClaimIds &&
myChannelClaimIds.some((id) => delegatorsById[id] && Object.keys(delegatorsById[id].delegators).length > 0);
// **************************************************************************
function getList(view) {
switch (view) {
case VIEW.BLOCKED:
return personalBlockList;
case VIEW.ADMIN:
return adminBlockList;
case VIEW.MODERATOR:
return moderatorBlockList;
case VIEW.MUTED:
return mutedUris;
}
}
function getActionButtons(uri) {
const getDurationStr = (durationNs) => {
const NANO_TO_MS = 1000000;
return humanizeDuration(durationNs / NANO_TO_MS, { round: true });
};
const getBanInfoElem = (timeoutInfo) => {
return (
<div>
<div className="help">
<blockquote>
{moment(timeoutInfo.blockedAt).format('MMMM Do, YYYY @ HH:mm')}
<br />
{getDurationStr(timeoutInfo.bannedFor)}{' '}
{__('(Remaining: %duration%) --[timeout ban duration]--', {
duration: getDurationStr(timeoutInfo.banRemaining),
})}
</blockquote>
</div>
</div>
);
};
switch (viewMode) {
case VIEW.BLOCKED:
return (
<>
<ChannelBlockButton uri={uri} />
<ChannelMuteButton uri={uri} />
{personalTimeoutMap[uri] && getBanInfoElem(personalTimeoutMap[uri])}
</>
);
case VIEW.ADMIN:
return (
<>
<ChannelBlockButton uri={uri} blockLevel={BLOCK_LEVEL.ADMIN} />
{adminTimeoutMap[uri] && getBanInfoElem(adminTimeoutMap[uri])}
</>
);
case VIEW.MODERATOR:
const delegatorUrisForBlockedUri = localDelegatorsMap && localDelegatorsMap[uri];
if (!delegatorUrisForBlockedUri) return null;
return (
<>
{delegatorUrisForBlockedUri.map((delegatorUri) => {
return (
<div className="block-list--delegator" key={delegatorUri}>
<label>{__('Blocked on behalf of:')}</label>
<ul className="section">
<div className="content__non-clickable">
<ClaimPreview uri={delegatorUri} hideMenu hideActions type="inline" properties={false} />
{moderatorTimeoutMap[uri] && getBanInfoElem(moderatorTimeoutMap[uri])}
</div>
<ChannelBlockButton uri={uri} blockLevel={BLOCK_LEVEL.MODERATOR} creatorUri={delegatorUri} />
</ul>
</div>
);
})}
</>
);
case VIEW.MUTED:
return (
<>
<ChannelMuteButton uri={uri} />
<ChannelBlockButton uri={uri} />
</>
);
}
}
function getHelpText(view) {
switch (view) {
case VIEW.BLOCKED:
return __(
"Blocked channels will be invisible to you in the app. They will not be able to comment on your content, nor reply to your comments left on other channels' content."
);
case VIEW.ADMIN:
return __('This is the global block list.');
case VIEW.MODERATOR:
return __('List of channels that you have blocked as a moderator, along with the list of delegators.');
case VIEW.MUTED:
return __(
'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.'
);
}
}
function getEmptyListTitle(view) {
switch (view) {
case VIEW.BLOCKED:
return __('You do not have any blocked channels');
case VIEW.MUTED:
return __('You do not have any muted channels');
case VIEW.ADMIN:
return __('You do not have any globally-blocked channels');
case VIEW.MODERATOR:
return __('You do not have any blocked channels as a moderator');
}
}
function getEmptyListSubtitle(view) {
switch (view) {
case VIEW.BLOCKED:
case VIEW.MUTED:
return getHelpText(view);
case VIEW.ADMIN:
case VIEW.MODERATOR:
return null;
}
}
function isSourceListLarger(source, local) {
// Comparing the length of stringified is not perfect, but what are the
// chances of having different lists with the exact same length?
return source && (!local || local.length < source.length);
}
function getViewElem(view, label, icon) {
return (
<Button
icon={icon}
button="alt"
label={__(label)}
className={classnames(`button-toggle`, {
'button-toggle--active': viewMode === view,
})}
onClick={() => setViewMode(view)}
/>
);
}
function getRefreshElem() {
return (
myChannelClaimIds && (
<Button
icon={ICONS.REFRESH}
button="alt"
label={__('Refresh')}
onClick={() => {
fetchModBlockedList();
fetchModAmIList();
}}
/>
)
);
}
// **************************************************************************
React.useEffect(() => {
if (stringifiedDelegatorsMap && isSourceListLarger(stringifiedDelegatorsMap, stringifiedLocalDelegatorsMap)) {
setLocalDelegatorsMap(JSON.parse(stringifiedDelegatorsMap));
}
}, [stringifiedDelegatorsMap, stringifiedLocalDelegatorsMap]);
// **************************************************************************
return (
<Page
noFooter
noSideNavigation
settingsPage
backout={{ title: __('Blocked and muted channels'), backLabel: __('Back') }}
>
{fetchingModerationBlockList && (
<div className="main--empty">
<Spinner />
</div>
)}
{!fetchingModerationBlockList && (
<>
<div className="section__header--actions">
<div className="section__actions--inline">
{getViewElem(VIEW.BLOCKED, 'Blocked', ICONS.BLOCK)}
{isAdmin && getViewElem(VIEW.ADMIN, 'Global', ICONS.BLOCK)}
{isModerator && getViewElem(VIEW.MODERATOR, 'Moderator', ICONS.BLOCK)}
{getViewElem(VIEW.MUTED, 'Muted', ICONS.MUTE)}
</div>
<div className="section__actions--inline">{getRefreshElem()}</div>
</div>
<BlockList
key={viewMode}
uris={getList(viewMode)}
help={getHelpText(viewMode)}
titleEmptyList={getEmptyListTitle(viewMode)}
subtitle={getEmptyListSubtitle(viewMode)}
getActionButtons={getActionButtons}
className={viewMode === VIEW.MODERATOR ? 'block-list--moderator' : undefined}
/>
</>
)}
</Page>
);
}
export default ListBlocked;