forked from hexbear-collective/hexbear-frontend
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2127 lines
61 KiB
2127 lines
61 KiB
import React, { Component, useEffect, useRef, useState } from 'react';
|
|
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
|
|
import clone from 'clone';
|
|
import { WebSocketService, UserService } from '../services';
|
|
import {
|
|
Post,
|
|
CreatePostLikeForm,
|
|
PostForm as PostFormI,
|
|
SavePostForm,
|
|
CommunityUser,
|
|
UserView,
|
|
BanType,
|
|
BanFromCommunityForm,
|
|
BanUserForm,
|
|
AddModToCommunityForm,
|
|
AddAdminForm,
|
|
AddSitemodForm,
|
|
TransferSiteForm,
|
|
TransferCommunityForm,
|
|
DeletePostForm,
|
|
RemovePostForm,
|
|
LockPostForm,
|
|
StickyPostForm,
|
|
} from '../interfaces';
|
|
import { MomentTime } from './moment-time';
|
|
import { PostForm } from './post-form';
|
|
import { IFramelyCard } from './iframely-card';
|
|
import { UserListing } from './user-listing';
|
|
import { CommunityLink } from './community-link';
|
|
import {
|
|
mdToHtml,
|
|
canMod,
|
|
isMod,
|
|
isImage,
|
|
isVideo,
|
|
isValidEmbed,
|
|
getUnixTime,
|
|
pictrsImage,
|
|
setupTippy,
|
|
hostname,
|
|
} from '../utils';
|
|
import { i18n } from '../i18next';
|
|
import { Icon } from './icon';
|
|
import { RoleBadge } from './RoleBadge';
|
|
import { linkEvent } from '../linkEvent';
|
|
import {
|
|
Box,
|
|
Flex,
|
|
Heading,
|
|
Text,
|
|
Image,
|
|
Link as ThemeLink,
|
|
useThemeUI,
|
|
} from 'theme-ui';
|
|
import Button, { CustomButtonProps } from './elements/Button';
|
|
import Block from './elements/Block';
|
|
import StyledLink from '../StyledLink';
|
|
import Tooltip from './Tooltip';
|
|
|
|
const ENABLE_NEW_POST_LISTING = true;
|
|
|
|
function crossPostParams(post: Post): string {
|
|
let params = `?title=${post.name}`;
|
|
|
|
if (post.url) {
|
|
params += `&url=${post.url}`;
|
|
}
|
|
if (post.body) {
|
|
params += `&body=${post.body}`;
|
|
}
|
|
if (post.community_id) {
|
|
params += `&community_id=${post.community_id}`;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
interface PostListingState {
|
|
showEdit: boolean;
|
|
showRemoveDialog: boolean;
|
|
removeReason: string;
|
|
showBanDialog: boolean;
|
|
banReason: string;
|
|
banExpires: string;
|
|
banType: BanType;
|
|
reportReason: string;
|
|
showReportDialog: boolean;
|
|
showConfirmTransferSite: boolean;
|
|
showConfirmTransferCommunity: boolean;
|
|
imageExpanded: boolean;
|
|
viewSource: boolean;
|
|
showAdvanced: boolean;
|
|
my_vote: number;
|
|
score: number;
|
|
upvotes: number;
|
|
downvotes: number;
|
|
localPostSaved: boolean;
|
|
innerWidth: number;
|
|
}
|
|
|
|
interface PostListingProps {
|
|
post: Post;
|
|
showCommunity?: boolean;
|
|
showBody?: boolean;
|
|
moderators?: Array<CommunityUser>;
|
|
admins?: Array<UserView>;
|
|
sitemods?: Array<UserView>;
|
|
enableDownvotes: boolean;
|
|
enableNsfw: boolean;
|
|
}
|
|
|
|
export function PostBody({ body }: { body: string }) {
|
|
return (
|
|
<div
|
|
className="md-div post-listing-body"
|
|
dangerouslySetInnerHTML={mdToHtml(body)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const PostListingButton = React.forwardRef(
|
|
(
|
|
{
|
|
// as: Element = Button,
|
|
onClick,
|
|
children,
|
|
...props
|
|
}: {
|
|
// as?: any;
|
|
onClick?: () => void;
|
|
children: React.ReactNode;
|
|
},
|
|
ref
|
|
) => {
|
|
return (
|
|
<Button
|
|
ref={ref}
|
|
variant="borderless"
|
|
px={2}
|
|
onClick={onClick}
|
|
type="button"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Button>
|
|
);
|
|
}
|
|
);
|
|
|
|
// export const PostListingButton = ({
|
|
// as: Element = Button,
|
|
// onClick,
|
|
// children,
|
|
// ...props
|
|
// }: {
|
|
// as?: any;
|
|
// onClick?: () => void;
|
|
// children: React.ReactNode;
|
|
// }) => {
|
|
// return (
|
|
// <Element
|
|
// variant="borderless"
|
|
// px={2}
|
|
// onClick={onClick}
|
|
// type="button"
|
|
// {...props}
|
|
// >
|
|
// {children}
|
|
// </Element>
|
|
// );
|
|
// };
|
|
|
|
const PostTitle = props => (
|
|
<Heading
|
|
as="h5"
|
|
color="text"
|
|
css={{
|
|
fontWeight: 400,
|
|
fontSize: '18px',
|
|
wordBreak: 'break-word',
|
|
lineHeight: '22px',
|
|
}}
|
|
{...props}
|
|
/>
|
|
);
|
|
|
|
export enum VoteButtonTypes {
|
|
Upvote,
|
|
Downvote,
|
|
}
|
|
|
|
interface VoteButtonProps extends CustomButtonProps {
|
|
voteType: VoteButtonTypes;
|
|
my_vote: number;
|
|
onClick: () => void;
|
|
}
|
|
|
|
export const VoteButton = ({
|
|
voteType,
|
|
// handlePostLike,
|
|
my_vote,
|
|
onClick,
|
|
...props
|
|
}: VoteButtonProps) => {
|
|
const { theme } = useThemeUI();
|
|
|
|
const upvoteColor = theme?.colors?.accent ?? theme?.colors?.primary;
|
|
const downvoteColor = theme?.colors?.accent
|
|
? theme?.colors?.primary
|
|
: theme?.colors?.secondary;
|
|
|
|
if (voteType === VoteButtonTypes.Upvote) {
|
|
return (
|
|
<Tooltip aria-label={i18n.t('upvote')} label={i18n.t('upvote')}>
|
|
<Button
|
|
as="button"
|
|
p={1}
|
|
backgroundColor="transparent"
|
|
// color="text"
|
|
onClick={onClick}
|
|
color={my_vote === 1 ? upvoteColor : 'text'}
|
|
css={{
|
|
':hover': {
|
|
backgroundColor: 'transparent',
|
|
color: upvoteColor,
|
|
},
|
|
}}
|
|
{...props}
|
|
>
|
|
<Icon name="upvote" className="icon upvote" />
|
|
</Button>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tooltip label={i18n.t('downvote')} aria-label={i18n.t('downvote')}>
|
|
<Button
|
|
p={1}
|
|
backgroundColor="transparent"
|
|
onClick={onClick}
|
|
color={my_vote === -1 ? downvoteColor : 'text'}
|
|
css={{
|
|
':hover': {
|
|
backgroundColor: 'transparent',
|
|
color: downvoteColor,
|
|
},
|
|
}}
|
|
>
|
|
<Icon name="downvote" className="icon downvote" />
|
|
</Button>
|
|
</Tooltip>
|
|
);
|
|
};
|
|
|
|
const VoteButtons = ({
|
|
my_vote,
|
|
handlePostLike,
|
|
handlePostDisLike,
|
|
enableDownvotes,
|
|
score,
|
|
pointsTippy,
|
|
}: any) => {
|
|
const { theme } = useThemeUI();
|
|
|
|
const upvoteColor = theme?.colors?.accent ?? theme?.colors?.primary;
|
|
const downvoteColor = theme?.colors?.accent
|
|
? theme.colors.primary
|
|
: theme.colors.secondary;
|
|
|
|
return (
|
|
<>
|
|
<VoteButton
|
|
onClick={handlePostLike}
|
|
voteType={VoteButtonTypes.Upvote}
|
|
my_vote={my_vote}
|
|
/>
|
|
<div
|
|
className="unselectable pointer font-weight-bold text-muted px-0 py-0"
|
|
data-tippy-content={pointsTippy}
|
|
>
|
|
{score}
|
|
</div>
|
|
{enableDownvotes && (
|
|
<VoteButton
|
|
onClick={handlePostDisLike}
|
|
voteType={VoteButtonTypes.Downvote}
|
|
my_vote={my_vote}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const PostActionButton = props => (
|
|
<Button p={2} variant="muted" bg="transparent" {...props} />
|
|
);
|
|
|
|
const PostIcon = props => (
|
|
<Box
|
|
p={1}
|
|
bg="background"
|
|
css={{ opacity: '60%' }}
|
|
className="mobile-icon-overlay"
|
|
>
|
|
<Icon size="18px" className="icon" {...props} />
|
|
</Box>
|
|
);
|
|
|
|
const MobilePostIcon = ({ post }: { post: Post }) => {
|
|
if (isImage(post.url)) {
|
|
return <PostIcon name="image" />;
|
|
} else if (post.thumbnail_url) {
|
|
return <PostIcon name="link" />;
|
|
} else if (post.embed_html !== null && isValidEmbed(post.url)) {
|
|
return <PostIcon name="link" />;
|
|
} else if (post.url) {
|
|
return <PostIcon name="link" />;
|
|
}
|
|
};
|
|
|
|
const MobilePostListing = ({
|
|
post,
|
|
my_vote,
|
|
handlePostLike,
|
|
handlePostDisLike,
|
|
enableDownvotes,
|
|
score,
|
|
pointsTippy,
|
|
localPostSaved,
|
|
handleEditClick,
|
|
handleSavePostClick,
|
|
thumbnail,
|
|
handleReportPost,
|
|
showBody,
|
|
handleShowAdvanced,
|
|
}) => {
|
|
const [actionsVisible, setActionsVisible] = useState(false);
|
|
const [imageExpanded, setImageExpanded] = useState(false);
|
|
const [contentExpanded, setContentExpanded] = useState(false);
|
|
const { theme } = useThemeUI();
|
|
|
|
function toggleContentExpanded() {
|
|
setContentExpanded(prevState => !prevState);
|
|
}
|
|
|
|
const longPost = post.body && post.body.length > 300;
|
|
|
|
// @TODO: Figure this out later...
|
|
// useEffect(() => {
|
|
// if (imageRef.current) {
|
|
// console.log(imageRef.current.offsetHeight)
|
|
// setImageHeight(imageRef.current.offsetHeight);
|
|
// }
|
|
// }, [imageRef])
|
|
|
|
return (
|
|
<Flex
|
|
css={{ flexDirection: 'column', overflow: 'hidden' }}
|
|
mb={showBody ? 1 : '20px'}
|
|
>
|
|
<Link
|
|
to={{
|
|
pathname: `/post/${post.id}`,
|
|
state: { post: { ...post, my_vote: my_vote } },
|
|
}}
|
|
color="text"
|
|
>
|
|
<PostTitle>{post.name}</PostTitle>
|
|
{post.nsfw && (
|
|
<span className="badge mb-2 nsfw-badge">{i18n.t('nsfw')}</span>
|
|
)}
|
|
</Link>
|
|
<Box css={{ fontSize: '14px' }} mt={1} mb={2}>
|
|
<Block as="span" color="#999">
|
|
{i18n.t('to')}{' '}
|
|
</Block>
|
|
<CommunityLink
|
|
community={{
|
|
name: post.community_name,
|
|
id: post.community_id,
|
|
local: post.community_local,
|
|
actor_id: post.community_actor_id,
|
|
}}
|
|
/>{' '}
|
|
<Block as="span" color="#999">
|
|
{i18n.t('by')}{' '}
|
|
</Block>
|
|
<UserListing
|
|
user={{
|
|
name: post.creator_name,
|
|
avatar: post.creator_avatar,
|
|
id: post.creator_id,
|
|
local: post.creator_local,
|
|
actor_id: post.creator_actor_id,
|
|
}}
|
|
/>
|
|
{/* {isAdmin && (
|
|
<RoleBadge role="admin" tooltipText={i18n.t('admin')}>
|
|
{i18n.t('admin')[0]}
|
|
</RoleBadge>
|
|
)}
|
|
{isMod && !isAdmin && (
|
|
<RoleBadge role="mod" tooltipText={i18n.t('mod')}>
|
|
{i18n.t('mod')[0]}
|
|
</RoleBadge>
|
|
)} */}
|
|
{post.creator_tags?.pronouns ? (
|
|
<span className="badge comment-badge pronouns-badge">
|
|
{post.creator_tags.pronouns.split(',').join('/')}
|
|
</span>
|
|
) : null}
|
|
<Box as="span" mx={1}>
|
|
• <MomentTime data={post} />
|
|
</Box>
|
|
{post.stickied && (
|
|
<Tooltip label={i18n.t('stickied')} aria-label={i18n.t('stickied')}>
|
|
<span className="pointer font-italic">
|
|
<Icon className="icon text-success" name="pin" />
|
|
</span>
|
|
</Tooltip>
|
|
)}
|
|
{post.locked && (
|
|
<small
|
|
className="unselectable pointer ml-1 text-muted font-italic"
|
|
data-tippy-content={i18n.t('locked')}
|
|
>
|
|
<svg className="icon custom-icon text-danger">
|
|
<use xlinkHref="#icon-lock" />
|
|
</svg>
|
|
</small>
|
|
)}
|
|
</Box>
|
|
{isImage(post.url) && (
|
|
<Block
|
|
position="relative"
|
|
// only collapse images that are taller than 300px
|
|
// className={`collapsed-content collapsed-image ${
|
|
// (contentExpanded || (imageHeight && imageHeight < 300)) && 'expanded'
|
|
// }`}
|
|
className={`collapsed-content collapsed-image ${
|
|
(imageExpanded || showBody) && 'expanded'
|
|
}`}
|
|
color="background"
|
|
my={1}
|
|
onClick={() => setImageExpanded(prevState => !prevState)}
|
|
// style={{ maxHeight: imageHeight ?? 'initial' }}
|
|
>
|
|
<Image
|
|
className={`img-fluid rounded ${
|
|
((post.nsfw && !imageExpanded) ||
|
|
(post.community_nsfw && !imageExpanded)) &&
|
|
'img-blur'
|
|
}`}
|
|
alt={post.name}
|
|
src={post.url}
|
|
/>
|
|
{!showBody && !imageExpanded && <MobilePostIcon post={post} />}
|
|
</Block>
|
|
)}
|
|
{isVideo(post.url) && (
|
|
<div className="embed-responsive embed-responsive-16by9">
|
|
<video
|
|
playsInline
|
|
muted
|
|
loop
|
|
controls
|
|
className="embed-responsive-item"
|
|
>
|
|
<source src={post.url} type="video/mp4" />
|
|
</video>
|
|
</div>
|
|
)}
|
|
{/* don't show both embed and IFramelyCard */}
|
|
{post.embed_html !== null && isValidEmbed(post.url) ? (
|
|
<div
|
|
dangerouslySetInnerHTML={{
|
|
__html: post.embed_html,
|
|
}}
|
|
/>
|
|
) : (
|
|
<>
|
|
{post.url && post.embed_title && (
|
|
<Block position="relative">
|
|
<IFramelyCard post={post} />
|
|
</Block>
|
|
)}
|
|
</>
|
|
)}
|
|
{post.url && !(hostname(post.url) === window.location.hostname) && (
|
|
<ThemeLink
|
|
className="text-muted font-italic post-link"
|
|
href={post.url}
|
|
target="_blank"
|
|
title={post.url}
|
|
rel="noreferrer"
|
|
color="gray"
|
|
css={{
|
|
fontStyle: 'italic',
|
|
color: 'muted',
|
|
fontSize: '14px',
|
|
}}
|
|
>
|
|
{hostname(post.url)}
|
|
<Icon className="ml-1 icon icon-inline" name="link" size="14px" />
|
|
</ThemeLink>
|
|
)}
|
|
{(post.body || showBody) && (
|
|
<Text
|
|
onClick={toggleContentExpanded}
|
|
className={`collapsed-content ${
|
|
(contentExpanded || !longPost || showBody) && 'expanded'
|
|
}`}
|
|
css={{
|
|
borderRadius: '4px',
|
|
fontSize: '16px',
|
|
}}
|
|
color="text"
|
|
my={2}
|
|
>
|
|
<PostBody body={post.body} />
|
|
</Text>
|
|
)}
|
|
<Flex
|
|
css={{
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
borderTop: '1px solid #303030',
|
|
borderBottom: '1px solid #303030',
|
|
}}
|
|
py={1}
|
|
mt={2}
|
|
>
|
|
<Flex css={{ alignItems: 'center' }}>
|
|
<VoteButton
|
|
voteType={VoteButtonTypes.Upvote}
|
|
onClick={handlePostLike}
|
|
my_vote={my_vote}
|
|
/>
|
|
<Box css={{ fontSize: '14px', fontWeight: 600 }} mx={2}>
|
|
{score}
|
|
</Box>
|
|
{enableDownvotes && (
|
|
<VoteButton
|
|
voteType={VoteButtonTypes.Downvote}
|
|
onClick={handlePostDisLike}
|
|
my_vote={my_vote}
|
|
/>
|
|
)}
|
|
</Flex>
|
|
<PostActionButton
|
|
onClick={handleSavePostClick}
|
|
data-tippy-content={post.saved ? i18n.t('unsave') : i18n.t('save')}
|
|
>
|
|
<Icon
|
|
name={localPostSaved ? 'star' : 'starOutline'}
|
|
className={`icon icon-inline ${localPostSaved && 'text-warning'}`}
|
|
/>
|
|
</PostActionButton>
|
|
<Box mx={3}>
|
|
<PostActionButton
|
|
as={Link}
|
|
title={i18n.t('number_of_comments', {
|
|
count: post.number_of_comments,
|
|
})}
|
|
to={{
|
|
pathname: `/post/${post.id}`,
|
|
state: { post: { ...post, my_vote: my_vote } },
|
|
}}
|
|
>
|
|
<Icon name="comment" className="icon mr-1" />
|
|
{post.number_of_comments}
|
|
</PostActionButton>
|
|
</Box>
|
|
<PostActionButton
|
|
onClick={() =>
|
|
showBody ? handleShowAdvanced() : setActionsVisible(prev => !prev)
|
|
}
|
|
>
|
|
<Icon name="more" />
|
|
</PostActionButton>
|
|
</Flex>
|
|
{actionsVisible && !showBody && (
|
|
<Flex my={2}>
|
|
<Link
|
|
to={`/create_post${crossPostParams(post)}`}
|
|
title={i18n.t('cross_post')}
|
|
>
|
|
<PostListingButton>
|
|
<svg className="icon icon-inline">
|
|
<use xlinkHref="#icon-copy" />
|
|
</svg>
|
|
</PostListingButton>
|
|
</Link>
|
|
|
|
<PostActionButton onClick={handleReportPost}>
|
|
<Icon name="report" />
|
|
</PostActionButton>
|
|
</Flex>
|
|
)}
|
|
</Flex>
|
|
);
|
|
};
|
|
|
|
export class BasePostListing extends Component<
|
|
PostListingProps & RouteComponentProps,
|
|
PostListingState
|
|
> {
|
|
private emptyState: PostListingState = {
|
|
showEdit: false,
|
|
showRemoveDialog: false,
|
|
removeReason: '',
|
|
showBanDialog: false,
|
|
banReason: '',
|
|
banExpires: '',
|
|
banType: BanType.Community,
|
|
reportReason: '',
|
|
showReportDialog: false,
|
|
showConfirmTransferSite: false,
|
|
showConfirmTransferCommunity: false,
|
|
imageExpanded: false,
|
|
viewSource: false,
|
|
showAdvanced: false,
|
|
my_vote: this.props.post.my_vote,
|
|
score: this.props.post.score,
|
|
upvotes: this.props.post.upvotes,
|
|
downvotes: this.props.post.downvotes,
|
|
// @TODO: Debug why this isn't being passed down on update
|
|
localPostSaved: this.props.post.saved,
|
|
innerWidth: window.innerWidth,
|
|
};
|
|
|
|
state = this.emptyState;
|
|
|
|
componentDidMount() {
|
|
// scroll to top of page when loading post listing
|
|
// window.scrollTo(0, 0);
|
|
this.setState({
|
|
my_vote: this.props.post.my_vote,
|
|
});
|
|
window.addEventListener('resize', this.handleResize);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener('resize', this.handleResize);
|
|
}
|
|
|
|
handleResize = () => {
|
|
this.setState({ innerWidth: window.innerWidth });
|
|
};
|
|
|
|
// @TODO: Check if this problem still persists
|
|
// UNSAFE_componentWillReceiveProps(nextProps: PostListingProps) {
|
|
// this.setState({
|
|
// my_vote: nextProps.post.my_vote,
|
|
// upvotes: nextProps.post.upvotes,
|
|
// downvotes: nextProps.post.downvotes,
|
|
// score: nextProps.post.score,
|
|
// // imageExpanded: this.props.post.id === nextProps.post.id
|
|
// });
|
|
// }
|
|
|
|
render() {
|
|
return (
|
|
<>
|
|
{!this.state.showEdit ? (
|
|
<>
|
|
{this.listing()}
|
|
{this.body()}
|
|
</>
|
|
) : (
|
|
<div className="col-12">
|
|
<PostForm
|
|
post={this.props.post}
|
|
onEdit={this.handleEditPost}
|
|
onCancel={this.handleEditCancel}
|
|
enableNsfw={this.props.enableNsfw}
|
|
enableDownvotes={this.props.enableDownvotes}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
body() {
|
|
const isMobile = this.state.innerWidth < 768;
|
|
|
|
if (isMobile) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="row">
|
|
<div className="col-12">
|
|
{this.props.post.url &&
|
|
this.props.showBody &&
|
|
this.props.post.embed_title && (
|
|
<IFramelyCard post={this.props.post} />
|
|
)}
|
|
{this.props.showBody && this.props.post.body && (
|
|
<>
|
|
{this.state.viewSource ? (
|
|
<pre>{this.props.post.body}</pre>
|
|
) : (
|
|
<PostBody body={this.props.post.body} />
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
imgThumb(src: string) {
|
|
let post = this.props.post;
|
|
return (
|
|
<img
|
|
className={`img-fluid thumbnail rounded ${
|
|
(post.nsfw || post.community_nsfw) && 'img-blur'
|
|
}`}
|
|
alt={post.name}
|
|
src={src}
|
|
/>
|
|
);
|
|
}
|
|
|
|
getImage(thumbnail = false) {
|
|
let post = this.props.post;
|
|
if (isImage(post.url)) {
|
|
if (post.url.includes('pictrs')) {
|
|
return pictrsImage(post.url, thumbnail);
|
|
} else if (post.thumbnail_url) {
|
|
return pictrsImage(post.thumbnail_url, thumbnail);
|
|
} else {
|
|
return post.url;
|
|
}
|
|
} else if (post.thumbnail_url) {
|
|
return pictrsImage(post.thumbnail_url, thumbnail);
|
|
}
|
|
}
|
|
|
|
thumbnail = () => {
|
|
let post = this.props.post;
|
|
|
|
if (isImage(post.url)) {
|
|
return (
|
|
<span
|
|
className="text-body pointer"
|
|
data-tippy-content={i18n.t('expand_here')}
|
|
onClick={this.handleImageExpandClick}
|
|
>
|
|
{this.imgThumb(this.getImage(true))}
|
|
<Icon name="image" className="icon mini-overlay" />
|
|
</span>
|
|
);
|
|
} else if (post.thumbnail_url) {
|
|
return (
|
|
<>
|
|
{post.embed_html !== null && isValidEmbed(post.url) ? (
|
|
<span
|
|
className="text-body pointer"
|
|
data-tippy-content={i18n.t('expand_here')}
|
|
onClick={this.handleImageExpandClick}
|
|
>
|
|
{this.imgThumb(this.getImage(true))}
|
|
<Icon className="icon mini-overlay" name="link" />
|
|
</span>
|
|
) : (
|
|
<a
|
|
className="text-body"
|
|
href={post.url}
|
|
target="_blank"
|
|
title={post.url}
|
|
rel="noreferrer"
|
|
>
|
|
{this.imgThumb(this.getImage(true))}
|
|
<Icon className="icon mini-overlay" name="link" />
|
|
</a>
|
|
)}
|
|
</>
|
|
);
|
|
} else if (post.embed_html !== null && isValidEmbed(post.url)) {
|
|
return (
|
|
<span
|
|
className="text-body pointer"
|
|
data-tippy-content={i18n.t('expand_here')}
|
|
onClick={this.handleImageExpandClick}
|
|
>
|
|
<Icon
|
|
className="icon thumbnail mini-overlay"
|
|
style={{ marginTop: '4px' }}
|
|
name="link"
|
|
/>
|
|
</span>
|
|
);
|
|
} else if (post.url) {
|
|
if (isVideo(post.url)) {
|
|
return (
|
|
<div className="embed-responsive embed-responsive-16by9">
|
|
<video
|
|
playsInline
|
|
muted
|
|
loop
|
|
controls
|
|
className="embed-responsive-item"
|
|
>
|
|
<source src={post.url} type="video/mp4" />
|
|
</video>
|
|
</div>
|
|
);
|
|
} else {
|
|
return (
|
|
<div className="post-listing-box">
|
|
<a
|
|
className="text-body"
|
|
href={post.url}
|
|
target="_blank"
|
|
title={post.url}
|
|
rel="noreferrer"
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100%',
|
|
}}
|
|
>
|
|
<Icon className="icon thumbnail" size="40px" name="link" />
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
} else {
|
|
return (
|
|
<Button
|
|
className="post-body-expand-button post-listing-box"
|
|
title={i18n.t('expand_here')}
|
|
color="text"
|
|
bg="background"
|
|
css={{
|
|
'&:hover': {
|
|
bg: 'var(--theme-ui-colors-background,#A4288B)',
|
|
},
|
|
}}
|
|
// onClick={() =>
|
|
// !this.props.showBody && linkEvent(this, this.handleImageExpandClick)
|
|
// }
|
|
onClick={() => !this.props.showBody && this.handleImageExpandClick()}
|
|
>
|
|
{post.nsfw ? (
|
|
<svg className="icon thumbnail" style={{ marginTop: '-3px' }}>
|
|
<use xlinkHref="#icon-warning-post" />
|
|
</svg>
|
|
) : (
|
|
<svg className="icon thumbnail">
|
|
<use xlinkHref="#icon-message-square" />
|
|
</svg>
|
|
)}
|
|
</Button>
|
|
);
|
|
}
|
|
};
|
|
|
|
listingActions = () => {
|
|
let post = this.props.post;
|
|
|
|
const isMobile = this.state.innerWidth < 768;
|
|
|
|
return (
|
|
<div className="details col-12">
|
|
<ul className="list-inline mb-0 text-muted small">
|
|
<li className="list-inline-item">
|
|
<span>{i18n.t('by')} </span>
|
|
<UserListing
|
|
user={{
|
|
name: post.creator_name,
|
|
avatar: post.creator_avatar,
|
|
id: post.creator_id,
|
|
local: post.creator_local,
|
|
actor_id: post.creator_actor_id,
|
|
}}
|
|
/>
|
|
{this.isAdmin && (
|
|
<RoleBadge role="admin" tooltipText={i18n.t('admin')}>
|
|
{i18n.t('admin')[0]}
|
|
</RoleBadge>
|
|
)}
|
|
{this.isMod && !this.isAdmin && (
|
|
<RoleBadge role="mod" tooltipText={i18n.t('mod')}>
|
|
{i18n.t('mod')[0]}
|
|
</RoleBadge>
|
|
)}
|
|
{post.creator_tags?.pronouns ? (
|
|
<span className="badge mx-1 comment-badge pronouns-badge">
|
|
{post.creator_tags.pronouns.split(',').join('/')}
|
|
</span>
|
|
) : null}
|
|
{(post.banned_from_community || post.banned) && (
|
|
<span className="mx-1 badge badge-danger">
|
|
{i18n.t('banned')}
|
|
</span>
|
|
)}
|
|
{this.props.showCommunity && (
|
|
<span>
|
|
<span> {i18n.t('to')} </span>
|
|
<CommunityLink
|
|
community={{
|
|
name: post.community_name,
|
|
id: post.community_id,
|
|
local: post.community_local,
|
|
actor_id: post.community_actor_id,
|
|
}}
|
|
/>
|
|
</span>
|
|
)}
|
|
</li>
|
|
<li className="list-inline-item">•</li>
|
|
<li className="list-inline-item">
|
|
<Box color="text" as="span" mx={1}>
|
|
<MomentTime data={post} />
|
|
</Box>
|
|
</li>
|
|
{post.stickied && (
|
|
<Tooltip label={i18n.t('stickied')} aria-label={i18n.t('stickied')}>
|
|
<small className="unselectable pointer ml-1 font-italic">
|
|
<Icon className="icon text-success" name="pin" />
|
|
</small>
|
|
</Tooltip>
|
|
)}
|
|
{post.locked && (
|
|
<small
|
|
className="unselectable pointer ml-1 text-muted font-italic"
|
|
data-tippy-content={i18n.t('locked')}
|
|
>
|
|
<svg className="icon custom-icon text-danger">
|
|
<use xlinkHref="#icon-lock" />
|
|
</svg>
|
|
</small>
|
|
)}
|
|
</ul>
|
|
{this.props.post.duplicates && (
|
|
<ul className="list-inline mb-1 small text-muted">
|
|
<>
|
|
<li className="list-inline-item mr-2">
|
|
{i18n.t('cross_posted_to')}
|
|
</li>
|
|
{this.props.post.duplicates.map(post => (
|
|
<li className="list-inline-item mr-2" key={post.id}>
|
|
<Link
|
|
to={{
|
|
pathname: `/post/${post.id}`,
|
|
state: { post: { ...post, my_vote: this.state.my_vote } },
|
|
}}
|
|
>
|
|
{post.community_name}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</>
|
|
</ul>
|
|
)}
|
|
{this.listingForms()}
|
|
<div className="post-listing-details">
|
|
<StyledLink
|
|
color="text"
|
|
title={i18n.t('number_of_comments', {
|
|
count: post.number_of_comments,
|
|
})}
|
|
to={{
|
|
pathname: `/post/${post.id}`,
|
|
state: { post: { ...post, my_vote: this.state.my_vote } },
|
|
}}
|
|
>
|
|
<Icon name="comment" className="icon mr-1" />
|
|
{isMobile
|
|
? post.number_of_comments
|
|
: i18n.t('number_of_comments', {
|
|
count: post.number_of_comments,
|
|
})}
|
|
</StyledLink>
|
|
{/* {this.state.upvotes !== this.state.score && (
|
|
|
|
)} */}
|
|
{UserService.Instance.user && (
|
|
<>
|
|
{!this.props.showBody && (
|
|
<>
|
|
<li className="list-inline-item">
|
|
<Tooltip
|
|
label={post.saved ? i18n.t('unsave') : i18n.t('save')}
|
|
aria-label={
|
|
post.saved ? i18n.t('unsave') : i18n.t('save')
|
|
}
|
|
>
|
|
<PostListingButton onClick={this.handleSavePostClick}>
|
|
<Icon
|
|
name={
|
|
this.state.localPostSaved ? 'star' : 'starOutline'
|
|
}
|
|
className={`icon icon-inline ${
|
|
this.state.localPostSaved && 'text-warning'
|
|
}`}
|
|
/>
|
|
</PostListingButton>
|
|
</Tooltip>
|
|
</li>
|
|
<li className="list-inline-item">
|
|
<Link
|
|
to={`/create_post${this.crossPostParams}`}
|
|
title={i18n.t('cross_post')}
|
|
>
|
|
<PostListingButton>
|
|
<svg className="icon icon-inline">
|
|
<use xlinkHref="#icon-copy" />
|
|
</svg>
|
|
</PostListingButton>
|
|
</Link>
|
|
</li>
|
|
<li className="list-inline-item">
|
|
<Tooltip
|
|
label="Report"
|
|
aria-label={`Report User ${post.creator_name}`}
|
|
>
|
|
<PostListingButton
|
|
onClick={this.handleReportPost}
|
|
data-tippy-content={i18n.t('snitch')}
|
|
>
|
|
<Icon name="report" />
|
|
</PostListingButton>
|
|
</Tooltip>
|
|
</li>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
<li className="list-inline-item">
|
|
<Block display="flex" alignItems="center" mt={1} color="text">
|
|
<Icon name="upDownArrow" />
|
|
<Block mx={2}>{this.state.upvotes}</Block>
|
|
<Block as="span" fontSize="8px">
|
|
•
|
|
</Block>
|
|
<Block mx={2}>{this.state.downvotes}</Block>
|
|
</Block>
|
|
</li>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
listingForms = () => {
|
|
const { post } = this.props;
|
|
return (
|
|
<>
|
|
{this.state.showRemoveDialog && (
|
|
<form className="form-inline" onSubmit={this.handleModRemoveSubmit}>
|
|
<input
|
|
type="text"
|
|
className="form-control mr-2"
|
|
placeholder={i18n.t('reason')}
|
|
value={this.state.removeReason}
|
|
onChange={this.handleModRemoveReasonChange}
|
|
/>
|
|
<button type="submit" className="btn btn-secondary">
|
|
{i18n.t('remove_post')}
|
|
</button>
|
|
</form>
|
|
)}
|
|
{this.state.showBanDialog && (
|
|
<form onSubmit={this.handleModBanBothSubmit}>
|
|
<div className="form-group row">
|
|
<label className="col-form-label" htmlFor="post-listing-reason">
|
|
{i18n.t('reason')}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="post-listing-reason"
|
|
className="form-control mr-2"
|
|
placeholder={i18n.t('reason')}
|
|
value={this.state.banReason}
|
|
onChange={this.handleModBanReasonChange}
|
|
/>
|
|
</div>
|
|
<div className="form-group row">
|
|
<button type="submit" className="btn btn-secondary">
|
|
{i18n.t('ban')} {post.creator_name}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
{this.state.showReportDialog && (
|
|
<form
|
|
className="mt-2"
|
|
onSubmit={linkEvent(this, this.handleReportSubmit)}
|
|
>
|
|
<div className="form-group row">
|
|
<label
|
|
className="col-form-label"
|
|
htmlFor="post-listing-report-reason"
|
|
>
|
|
{i18n.t('reason')}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="post-listing-report-reason"
|
|
className="form-control mr-2"
|
|
placeholder={i18n.t('reason')}
|
|
value={this.state.reportReason}
|
|
onChange={linkEvent(this, this.handleReportReasonChange)}
|
|
maxLength={600}
|
|
/>
|
|
</div>
|
|
<div className="form-group row">
|
|
<button type="submit" className="btn btn-secondary">
|
|
{i18n.t('submit_report')}
|
|
</button>
|
|
</div>
|
|
<div className="row mt-1">
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
onClick={linkEvent(this, this.handleReportPost)}
|
|
>
|
|
{i18n.t('cancel')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
listing = () => {
|
|
let post = this.props.post;
|
|
|
|
const isMobile = this.state.innerWidth < 768;
|
|
|
|
if (isMobile && ENABLE_NEW_POST_LISTING) {
|
|
return (
|
|
<>
|
|
<MobilePostListing
|
|
handleEditClick={this.handleEditClick}
|
|
my_vote={this.state.my_vote}
|
|
handleShowAdvanced={this.handleShowAdvanced}
|
|
post={post}
|
|
enableDownvotes={this.props.enableDownvotes}
|
|
score={this.state.score}
|
|
handlePostDisLike={this.handlePostDisLike}
|
|
handlePostLike={this.handlePostLike}
|
|
pointsTippy={this.pointsTippy}
|
|
thumbnail={this.thumbnail}
|
|
localPostSaved={this.state.localPostSaved}
|
|
handleSavePostClick={this.handleSavePostClick}
|
|
handleReportPost={this.handleReportPost}
|
|
showBody={this.props.showBody}
|
|
/>
|
|
<Flex>{this.advancedActions()}</Flex>
|
|
{this.listingForms()}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Block
|
|
className="row post-listing-row"
|
|
borderColor="muted"
|
|
borderWidth={0}
|
|
borderBottomWidth="1px"
|
|
borderStyle="solid"
|
|
>
|
|
<div
|
|
className={`vote-bar small text-center ${
|
|
post.stickied ? 'stickied-border' : ''
|
|
}`}
|
|
>
|
|
<VoteButtons
|
|
my_vote={this.state.my_vote}
|
|
enableDownvotes={this.props.enableDownvotes}
|
|
score={this.state.score}
|
|
handlePostDisLike={this.handlePostDisLike}
|
|
handlePostLike={this.handlePostLike}
|
|
pointsTippy={this.pointsTippy}
|
|
/>
|
|
</div>
|
|
{/* show thumbnail when not expanded or content is a video */}
|
|
{(!isMobile ||
|
|
(isMobile &&
|
|
!this.state.imageExpanded &&
|
|
(post.body || post.url)) ||
|
|
isVideo(post.url) ||
|
|
post.embed_html !== null ||
|
|
// if it's a text post (doesn't have URL) always show thumbnail when expanded
|
|
(this.state.imageExpanded && post.body && !post.url)) && (
|
|
<div className="col-3 col-sm-2 pr-0 mt-1 thumbnail-wrapper">
|
|
<div className="position-relative">{this.thumbnail()}</div>
|
|
</div>
|
|
)}
|
|
<div
|
|
className={`${
|
|
this.state.imageExpanded ? 'col-sm-12 col-md-8' : 'col-8 col-sm-9'
|
|
} mt post-content`}
|
|
>
|
|
<div className="row">
|
|
<div className="col-12">
|
|
<div className="post-title">
|
|
<PostTitle>
|
|
{this.props.showBody && post.url ? (
|
|
<Block
|
|
as="a"
|
|
className="post-title-link"
|
|
color="text"
|
|
css={{
|
|
':visited': {
|
|
color: 'gray',
|
|
},
|
|
}}
|
|
// @ts-ignore
|
|
href={post.url}
|
|
target="_blank"
|
|
title={post.url}
|
|
rel="noreferrer"
|
|
>
|
|
{post.name}
|
|
</Block>
|
|
) : (
|
|
<StyledLink
|
|
color="text"
|
|
css={{
|
|
':visited': {
|
|
color: 'gray',
|
|
},
|
|
}}
|
|
to={{
|
|
pathname: `/post/${post.id}`,
|
|
state: {
|
|
post: { ...post, my_vote: this.state.my_vote },
|
|
},
|
|
}}
|
|
title={i18n.t('comments')}
|
|
>
|
|
{post.name}
|
|
</StyledLink>
|
|
)}
|
|
</PostTitle>
|
|
{post.url &&
|
|
!(hostname(post.url) === window.location.hostname) && (
|
|
<small className="d-inline-block">
|
|
<ThemeLink
|
|
as="a" // @ts-ignore
|
|
className="text-muted font-italic post-listing-url"
|
|
href={post.url}
|
|
target="_blank"
|
|
title={post.url}
|
|
rel="noreferrer"
|
|
>
|
|
{hostname(post.url)}
|
|
<Icon
|
|
className="ml-1 icon icon-inline"
|
|
name="link"
|
|
size="14px"
|
|
/>
|
|
</ThemeLink>
|
|
</small>
|
|
)}
|
|
{(isImage(post.url) ||
|
|
(post.embed_html !== null && isValidEmbed(post.url)) ||
|
|
this.props.post.thumbnail_url) && (
|
|
<>
|
|
{!this.state.imageExpanded ? (
|
|
<span
|
|
className="text-monospace unselectable pointer ml-2 text-muted small"
|
|
data-tippy-content={i18n.t('expand_here')}
|
|
onClick={this.handleImageExpandClick}
|
|
>
|
|
{/* keeping this for accessibility reasons */}
|
|
</span>
|
|
) : (
|
|
<span>
|
|
<span
|
|
className="text-monospace unselectable pointer ml-2 text-muted small"
|
|
onClick={this.handleImageExpandClick}
|
|
>
|
|
{/* keeping this for accessibility reasons */}
|
|
</span>
|
|
<div>
|
|
<span
|
|
className="pointer"
|
|
onClick={this.handleImageExpandClick}
|
|
>
|
|
{post.embed_html !== null &&
|
|
isValidEmbed(post.url) ? (
|
|
<div
|
|
dangerouslySetInnerHTML={{
|
|
__html: post.embed_html,
|
|
}}
|
|
/>
|
|
) : (
|
|
<img
|
|
className="img-fluid img-expanded mt-2"
|
|
alt={`expanded for post ${post.name}`}
|
|
src={this.getImage()}
|
|
/>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
{post.removed && (
|
|
<small className="ml-2 text-muted font-italic">
|
|
{i18n.t('removed')}
|
|
</small>
|
|
)}
|
|
{post.deleted && (
|
|
<small
|
|
className="unselectable pointer ml-2 text-muted font-italic"
|
|
data-tippy-content={i18n.t('deleted')}
|
|
>
|
|
<svg className="icon icon-inline text-danger">
|
|
<use xlinkHref="#icon-trash" />
|
|
</svg>
|
|
</small>
|
|
)}
|
|
{post.nsfw && (
|
|
<div className="badge ml-2 mb-2 nsfw-badge">
|
|
{i18n.t('nsfw')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="row">{this.listingActions()}</div>
|
|
</div>
|
|
</Block>
|
|
<ul className="list-inline mb-1 text-muted font-weight-bold">
|
|
{UserService.Instance.user && <>{this.advancedActions()}</>}
|
|
</ul>
|
|
{post.body && this.state.imageExpanded && !this.props.showBody && (
|
|
<PostBody body={post.body} />
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
advancedActions = () => {
|
|
const { post } = this.props;
|
|
|
|
return (
|
|
<>
|
|
{this.props.showBody && (
|
|
<>
|
|
<li className="list-inline-item">
|
|
<PostListingButton
|
|
onClick={this.handleSavePostClick}
|
|
data-tippy-content={
|
|
post.saved ? i18n.t('unsave') : i18n.t('save')
|
|
}
|
|
>
|
|
<Icon
|
|
name={this.state.localPostSaved ? 'star' : 'starOutline'}
|
|
className={`icon icon-inline ${
|
|
this.state.localPostSaved && 'text-warning'
|
|
}`}
|
|
/>
|
|
</PostListingButton>
|
|
</li>
|
|
<li className="list-inline-item">
|
|
<Link
|
|
to={`/create_post${this.crossPostParams}`}
|
|
title={i18n.t('cross_post')}
|
|
>
|
|
<PostListingButton>
|
|
<svg className="icon icon-inline">
|
|
<use xlinkHref="#icon-copy" />
|
|
</svg>
|
|
</PostListingButton>
|
|
</Link>
|
|
</li>
|
|
<li className="list-inline-item">
|
|
<PostListingButton
|
|
onClick={this.handleReportPost}
|
|
data-tippy-content={i18n.t('snitch')}
|
|
>
|
|
<Icon name="report" />
|
|
</PostListingButton>
|
|
</li>
|
|
</>
|
|
)}
|
|
{this.myPost && this.props.showBody && (
|
|
<>
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
onClick={this.handleEditClick}
|
|
data-tippy-content={i18n.t('edit')}
|
|
>
|
|
<Icon name="edit" />
|
|
</PostActionButton>
|
|
</li>
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
onClick={this.handleDeleteClick}
|
|
data-tippy-content={
|
|
!post.deleted ? i18n.t('delete') : i18n.t('restore')
|
|
}
|
|
>
|
|
<svg
|
|
className={`icon icon-inline ${
|
|
post.deleted && 'text-danger'
|
|
}`}
|
|
>
|
|
<use xlinkHref="#icon-trash" />
|
|
</svg>
|
|
</PostActionButton>
|
|
</li>
|
|
</>
|
|
)}
|
|
{!this.state.showAdvanced && this.props.showBody ? (
|
|
<li
|
|
className={`list-inline-item ${
|
|
ENABLE_NEW_POST_LISTING && 'only-desktop'
|
|
}`}
|
|
>
|
|
<button
|
|
className="btn btn-sm btn-link btn-animate text-muted"
|
|
onClick={this.handleShowAdvanced}
|
|
data-tippy-content={i18n.t('more')}
|
|
>
|
|
<Icon name="more" />
|
|
</button>
|
|
</li>
|
|
) : (
|
|
<>
|
|
{this.props.showBody && post.body && (
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
// className="btn btn-sm btn-link btn-animate text-muted"
|
|
onClick={linkEvent(this, this.handleViewSource)}
|
|
data-tippy-content={i18n.t('view_source')}
|
|
>
|
|
<svg
|
|
className={`icon icon-inline ${
|
|
this.state.viewSource && 'text-success'
|
|
}`}
|
|
>
|
|
<use xlinkHref="#icon-file-text" />
|
|
</svg>
|
|
</PostActionButton>
|
|
</li>
|
|
)}
|
|
{this.canModOnSelf && (
|
|
<>
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
// className="btn btn-sm btn-link btn-animate text-muted"
|
|
onClick={this.handleModLock}
|
|
data-tippy-content={
|
|
post.locked ? i18n.t('unlock') : i18n.t('lock')
|
|
}
|
|
>
|
|
<svg
|
|
className={`icon icon-inline ${
|
|
post.locked && 'text-danger'
|
|
}`}
|
|
>
|
|
<use xlinkHref="#icon-lock" />
|
|
</svg>
|
|
</PostActionButton>
|
|
</li>
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
// className="btn btn-sm btn-link btn-animate text-muted"
|
|
onClick={this.handleModSticky}
|
|
data-tippy-content={
|
|
post.stickied ? i18n.t('unsticky') : i18n.t('sticky')
|
|
}
|
|
>
|
|
<svg
|
|
className={`icon icon-inline ${
|
|
post.stickied && 'text-success'
|
|
}`}
|
|
>
|
|
<use xlinkHref="#icon-pin" />
|
|
</svg>
|
|
</PostActionButton>
|
|
</li>
|
|
</>
|
|
)}
|
|
{/* Mods can ban from community, and appoint as mods to community */}
|
|
{(this.canMod || this.canAdmin) && (
|
|
<li className="list-inline-item">
|
|
{!post.removed ? (
|
|
<PostActionButton
|
|
className="pointer"
|
|
onClick={this.handleModRemoveShow}
|
|
>
|
|
{i18n.t('remove')}
|
|
</PostActionButton>
|
|
) : (
|
|
<PostActionButton
|
|
className="pointer"
|
|
onClick={this.handleModRemoveSubmit}
|
|
>
|
|
{i18n.t('restore')}
|
|
</PostActionButton>
|
|
)}
|
|
</li>
|
|
)}
|
|
{this.canMod && (
|
|
<>
|
|
{!this.isMod && (
|
|
<li className="list-inline-item">
|
|
{!post.banned_from_community ? (
|
|
<PostActionButton
|
|
onClick={this.handleModBanFromCommunityShow}
|
|
>
|
|
{i18n.t('ban')}
|
|
</PostActionButton>
|
|
) : (
|
|
<PostActionButton
|
|
onClick={linkEvent(
|
|
this,
|
|
this.handleModBanFromCommunitySubmit
|
|
)}
|
|
>
|
|
{i18n.t('unban')}
|
|
</PostActionButton>
|
|
)}
|
|
</li>
|
|
)}
|
|
{!post.banned_from_community && (
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
onClick={linkEvent(this, this.handleAddModToCommunity)}
|
|
>
|
|
{this.isMod
|
|
? i18n.t('remove_as_mod')
|
|
: i18n.t('appoint_as_mod')}
|
|
</PostActionButton>
|
|
</li>
|
|
)}
|
|
</>
|
|
)}
|
|
{/* Community creators and admins can transfer community to another mod */}
|
|
{(this.amCommunityCreator || this.canAdmin) && this.isMod && (
|
|
<li className="list-inline-item">
|
|
{!this.state.showConfirmTransferCommunity ? (
|
|
<PostActionButton
|
|
onClick={linkEvent(
|
|
this,
|
|
this.handleShowConfirmTransferCommunity
|
|
)}
|
|
>
|
|
{i18n.t('transfer_community')}
|
|
</PostActionButton>
|
|
) : (
|
|
<>
|
|
<span className="d-inline-block mr-1">
|
|
{i18n.t('are_you_sure')}
|
|
</span>
|
|
<span
|
|
className="pointer d-inline-block mr-1"
|
|
onClick={linkEvent(this, this.handleTransferCommunity)}
|
|
>
|
|
{i18n.t('yes')}
|
|
</span>
|
|
<span
|
|
className="pointer d-inline-block"
|
|
onClick={linkEvent(
|
|
this,
|
|
this.handleCancelShowConfirmTransferCommunity
|
|
)}
|
|
>
|
|
{i18n.t('no')}
|
|
</span>
|
|
</>
|
|
)}
|
|
</li>
|
|
)}
|
|
{/* Admins can ban from all, and appoint other admins */}
|
|
{this.canAdmin && (
|
|
<>
|
|
{!this.isAdmin && (
|
|
<li className="list-inline-item">
|
|
{!post.banned ? (
|
|
<PostActionButton onClick={this.handleModBanShow}>
|
|
{i18n.t('ban_from_site')}
|
|
</PostActionButton>
|
|
) : (
|
|
<PostActionButton onClick={this.handleModBanSubmit}>
|
|
{i18n.t('unban_from_site')}
|
|
</PostActionButton>
|
|
)}
|
|
</li>
|
|
)}
|
|
{!post.banned && (
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
onClick={linkEvent(this, this.handleAddAdmin)}
|
|
>
|
|
{this.isAdmin
|
|
? i18n.t('remove_as_admin')
|
|
: i18n.t('appoint_as_admin')}
|
|
</PostActionButton>
|
|
</li>
|
|
)}
|
|
{!post.banned && (
|
|
<li className="list-inline-item">
|
|
<PostActionButton
|
|
onClick={linkEvent(this, this.handleAddSitemod)}
|
|
>
|
|
{this.isSitemod
|
|
? i18n.t('remove_as_sitemod')
|
|
: i18n.t('appoint_as_sitemod')}
|
|
</PostActionButton>
|
|
</li>
|
|
)}
|
|
</>
|
|
)}
|
|
{/* Site Creator can transfer to another admin */}
|
|
{this.amSiteCreator && this.isAdmin && (
|
|
<li className="list-inline-item">
|
|
{!this.state.showConfirmTransferSite ? (
|
|
<PostActionButton
|
|
onClick={linkEvent(
|
|
this,
|
|
this.handleShowConfirmTransferSite
|
|
)}
|
|
>
|
|
{i18n.t('transfer_site')}
|
|
</PostActionButton>
|
|
) : (
|
|
<>
|
|
<span className="d-inline-block mr-1">
|
|
{i18n.t('are_you_sure')}
|
|
</span>
|
|
<span
|
|
className="pointer d-inline-block mr-1"
|
|
onClick={linkEvent(this, this.handleTransferSite)}
|
|
>
|
|
{i18n.t('yes')}
|
|
</span>
|
|
<span
|
|
className="pointer d-inline-block"
|
|
onClick={linkEvent(
|
|
this,
|
|
this.handleCancelShowConfirmTransferSite
|
|
)}
|
|
>
|
|
{i18n.t('no')}
|
|
</span>
|
|
</>
|
|
)}
|
|
</li>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
private get myPost(): boolean {
|
|
return (
|
|
UserService.Instance.user &&
|
|
this.props.post.creator_id === UserService.Instance.user.id
|
|
);
|
|
}
|
|
|
|
get isMod(): boolean {
|
|
return (
|
|
this.props.moderators &&
|
|
isMod(
|
|
this.props.moderators.map(m => m.user_id),
|
|
this.props.post.creator_id
|
|
)
|
|
);
|
|
}
|
|
|
|
get isAdmin(): boolean {
|
|
return (
|
|
this.props.admins &&
|
|
isMod(
|
|
this.props.admins.map(a => a.id),
|
|
this.props.post.creator_id
|
|
)
|
|
);
|
|
}
|
|
|
|
get isSitemod(): boolean {
|
|
return (
|
|
this.props.sitemods &&
|
|
isMod(
|
|
this.props.sitemods.map(s => s.id),
|
|
this.props.post.creator_id
|
|
)
|
|
);
|
|
}
|
|
|
|
get canMod(): boolean {
|
|
if (this.props.admins && this.props.sitemods && this.props.moderators) {
|
|
let adminsThenMods = this.props.admins
|
|
.map(a => a.id)
|
|
.concat(this.props.sitemods.map(s => s.id))
|
|
.concat(this.props.moderators.map(m => m.user_id));
|
|
|
|
return canMod(
|
|
UserService.Instance.user,
|
|
adminsThenMods,
|
|
this.props.post.creator_id
|
|
);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
get canModOnSelf(): boolean {
|
|
if (this.props.admins && this.props.sitemods && this.props.moderators) {
|
|
let adminsThenMods = this.props.admins
|
|
.map(a => a.id)
|
|
.concat(this.props.sitemods.map(s => s.id))
|
|
.concat(this.props.moderators.map(m => m.user_id));
|
|
|
|
return canMod(
|
|
UserService.Instance.user,
|
|
adminsThenMods,
|
|
this.props.post.creator_id,
|
|
true
|
|
);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
get canAdmin(): boolean {
|
|
return (
|
|
this.props.admins &&
|
|
canMod(
|
|
UserService.Instance.user,
|
|
this.props.admins.map(a => a.id),
|
|
this.props.post.creator_id
|
|
)
|
|
);
|
|
}
|
|
|
|
get amCommunityCreator(): boolean {
|
|
return (
|
|
this.props.moderators &&
|
|
UserService.Instance.user &&
|
|
this.props.post.creator_id !== UserService.Instance.user.id &&
|
|
UserService.Instance.user.id === this.props.moderators[0].user_id
|
|
);
|
|
}
|
|
|
|
get amSiteCreator(): boolean {
|
|
return (
|
|
this.props.admins &&
|
|
UserService.Instance.user &&
|
|
this.props.post.creator_id !== UserService.Instance.user.id &&
|
|
UserService.Instance.user.id === this.props.admins[0].id
|
|
);
|
|
}
|
|
|
|
handlePostLike = () => {
|
|
if (!UserService.Instance.user) {
|
|
this.props.history.push(`/login`);
|
|
}
|
|
|
|
let new_vote = this.state.my_vote === 1 ? 0 : 1;
|
|
const newState = clone(this.state);
|
|
|
|
if (this.state.my_vote === 1) {
|
|
newState.score--;
|
|
newState.upvotes--;
|
|
} else if (this.state.my_vote === -1) {
|
|
newState.downvotes--;
|
|
newState.upvotes++;
|
|
newState.score += 2;
|
|
} else {
|
|
newState.upvotes++;
|
|
newState.score++;
|
|
}
|
|
|
|
newState.my_vote = new_vote;
|
|
|
|
let form: CreatePostLikeForm = {
|
|
post_id: this.props.post.id,
|
|
score: newState.my_vote,
|
|
};
|
|
|
|
WebSocketService.Instance.likePost(form);
|
|
this.setState(newState);
|
|
setupTippy();
|
|
};
|
|
|
|
handlePostDisLike = () => {
|
|
if (!UserService.Instance.user) {
|
|
this.props.history.push(`/login`);
|
|
}
|
|
|
|
let new_vote = this.state.my_vote === -1 ? 0 : -1;
|
|
const newState = clone(this.state);
|
|
|
|
if (this.state.my_vote === 1) {
|
|
newState.score -= 2;
|
|
newState.upvotes--;
|
|
newState.downvotes++;
|
|
} else if (this.state.my_vote === -1) {
|
|
newState.downvotes--;
|
|
newState.score++;
|
|
} else {
|
|
newState.downvotes++;
|
|
newState.score--;
|
|
}
|
|
|
|
newState.my_vote = new_vote;
|
|
|
|
let form: CreatePostLikeForm = {
|
|
post_id: this.props.post.id,
|
|
score: newState.my_vote,
|
|
};
|
|
|
|
WebSocketService.Instance.likePost(form);
|
|
this.setState(newState);
|
|
setupTippy();
|
|
};
|
|
|
|
handleEditClick = () => {
|
|
this.setState({
|
|
showEdit: true,
|
|
});
|
|
};
|
|
|
|
handleEditCancel = () => {
|
|
this.setState({
|
|
showEdit: false,
|
|
});
|
|
};
|
|
|
|
// The actual editing is done in the recieve for post
|
|
handleEditPost = () => {
|
|
this.setState({
|
|
showEdit: false,
|
|
});
|
|
};
|
|
|
|
handleDeleteClick = () => {
|
|
let deleteForm: DeletePostForm = {
|
|
edit_id: this.props.post.id,
|
|
deleted: !this.props.post.deleted,
|
|
auth: null,
|
|
};
|
|
WebSocketService.Instance.deletePost(deleteForm);
|
|
};
|
|
|
|
handleSavePostClick = () => {
|
|
let saved =
|
|
this.props.post.saved === undefined ? true : !this.props.post.saved;
|
|
let form: SavePostForm = {
|
|
post_id: this.props.post.id,
|
|
save: saved,
|
|
};
|
|
|
|
WebSocketService.Instance.savePost(form);
|
|
this.setState({ localPostSaved: !this.state.localPostSaved });
|
|
};
|
|
|
|
get crossPostParams(): string {
|
|
let params = `?title=${this.props.post.name}`;
|
|
let post = this.props.post;
|
|
|
|
if (post.url) {
|
|
params += `&url=${post.url}`;
|
|
}
|
|
if (this.props.post.body) {
|
|
params += `&body=${this.props.post.body}`;
|
|
}
|
|
if (post.community_id) {
|
|
params += `&community_id=${this.props.post.community_id}`;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
handleModRemoveShow = () => {
|
|
this.setState({
|
|
showRemoveDialog: true,
|
|
});
|
|
};
|
|
|
|
handleModRemoveReasonChange = (event: any) => {
|
|
this.setState({
|
|
removeReason: event.target.value,
|
|
});
|
|
};
|
|
|
|
handleModRemoveSubmit = (event: React.SyntheticEvent) => {
|
|
event.preventDefault();
|
|
let form: RemovePostForm = {
|
|
edit_id: this.props.post.id,
|
|
removed: !this.props.post.removed,
|
|
reason: this.state.removeReason,
|
|
auth: null,
|
|
};
|
|
WebSocketService.Instance.removePost(form);
|
|
this.setState({
|
|
showRemoveDialog: false,
|
|
});
|
|
};
|
|
|
|
handleModLock = () => {
|
|
let form: LockPostForm = {
|
|
edit_id: this.props.post.id,
|
|
locked: !this.props.post.locked,
|
|
auth: null,
|
|
};
|
|
WebSocketService.Instance.lockPost(form);
|
|
};
|
|
|
|
handleModSticky = () => {
|
|
let form: StickyPostForm = {
|
|
edit_id: this.props.post.id,
|
|
stickied: !this.props.post.stickied,
|
|
auth: null,
|
|
};
|
|
WebSocketService.Instance.stickyPost(form);
|
|
};
|
|
|
|
handleModBanFromCommunityShow = () => {
|
|
this.setState({
|
|
showBanDialog: true,
|
|
banType: BanType.Community,
|
|
});
|
|
};
|
|
|
|
handleModBanShow = () => {
|
|
this.setState({
|
|
showBanDialog: true,
|
|
banType: BanType.Site,
|
|
});
|
|
};
|
|
|
|
handleModBanReasonChange = (event: any) => {
|
|
this.setState({
|
|
banReason: event.target.value,
|
|
});
|
|
};
|
|
|
|
handleModBanExpiresChange = (event: any) => {
|
|
this.setState({
|
|
banReason: event.target.value,
|
|
});
|
|
};
|
|
|
|
handleModBanFromCommunitySubmit = () => {
|
|
this.setState(
|
|
{
|
|
banType: BanType.Community,
|
|
},
|
|
() => {
|
|
this.handleModBanBothSubmit();
|
|
}
|
|
);
|
|
};
|
|
|
|
handleModBanSubmit = () => {
|
|
this.setState(
|
|
{
|
|
banType: BanType.Site,
|
|
},
|
|
() => {
|
|
this.handleModBanBothSubmit();
|
|
}
|
|
);
|
|
};
|
|
|
|
handleModBanBothSubmit = () => {
|
|
if (this.state.banType === BanType.Community) {
|
|
let form: BanFromCommunityForm = {
|
|
user_id: this.props.post.creator_id,
|
|
community_id: this.props.post.community_id,
|
|
ban: !this.props.post.banned_from_community,
|
|
reason: this.state.banReason,
|
|
expires: getUnixTime(this.state.banExpires),
|
|
};
|
|
WebSocketService.Instance.banFromCommunity(form);
|
|
} else {
|
|
let form: BanUserForm = {
|
|
user_id: this.props.post.creator_id,
|
|
ban: !this.props.post.banned,
|
|
reason: this.state.banReason,
|
|
expires: getUnixTime(this.state.banExpires),
|
|
};
|
|
WebSocketService.Instance.banUser(form);
|
|
}
|
|
|
|
this.setState({
|
|
showBanDialog: false,
|
|
});
|
|
};
|
|
|
|
handleAddModToCommunity(i: BasePostListing) {
|
|
let form: AddModToCommunityForm = {
|
|
user_id: i.props.post.creator_id,
|
|
community_id: i.props.post.community_id,
|
|
added: !i.isMod,
|
|
};
|
|
WebSocketService.Instance.addModToCommunity(form);
|
|
i.setState(i.state);
|
|
}
|
|
|
|
handleAddAdmin(i: BasePostListing) {
|
|
let form: AddAdminForm = {
|
|
user_id: i.props.post.creator_id,
|
|
added: !i.isAdmin,
|
|
};
|
|
WebSocketService.Instance.addAdmin(form);
|
|
i.setState(i.state);
|
|
}
|
|
|
|
handleAddSitemod(i: BasePostListing) {
|
|
let form: AddSitemodForm = {
|
|
user_id: i.props.post.creator_id,
|
|
added: !i.isSitemod,
|
|
};
|
|
WebSocketService.Instance.addSitemod(form);
|
|
i.setState(i.state);
|
|
}
|
|
|
|
handleShowConfirmTransferCommunity(i: BasePostListing) {
|
|
i.setState({ showConfirmTransferCommunity: true });
|
|
}
|
|
|
|
handleCancelShowConfirmTransferCommunity(i: BasePostListing) {
|
|
i.setState({ showConfirmTransferCommunity: false });
|
|
}
|
|
|
|
handleTransferCommunity(i: BasePostListing) {
|
|
let form: TransferCommunityForm = {
|
|
community_id: i.props.post.community_id,
|
|
user_id: i.props.post.creator_id,
|
|
};
|
|
WebSocketService.Instance.transferCommunity(form);
|
|
i.setState({ showConfirmTransferCommunity: false });
|
|
}
|
|
|
|
handleShowConfirmTransferSite(i: BasePostListing) {
|
|
i.setState({ showConfirmTransferCommunity: true });
|
|
}
|
|
|
|
handleCancelShowConfirmTransferSite(i: BasePostListing) {
|
|
i.setState({ showConfirmTransferSite: false });
|
|
}
|
|
|
|
handleTransferSite(i: BasePostListing) {
|
|
let form: TransferSiteForm = {
|
|
user_id: i.props.post.creator_id,
|
|
};
|
|
WebSocketService.Instance.transferSite(form);
|
|
i.setState({ showConfirmTransferSite: false });
|
|
}
|
|
|
|
handleImageExpandClick = () => {
|
|
this.setState({
|
|
imageExpanded: !this.state.imageExpanded,
|
|
});
|
|
};
|
|
|
|
handleViewSource(i: BasePostListing) {
|
|
i.setState(prevState => ({
|
|
viewSource: !prevState.viewSource,
|
|
}));
|
|
}
|
|
|
|
handleReportPost = () => {
|
|
this.setState(prevState => ({
|
|
showReportDialog: !prevState.showReportDialog,
|
|
}));
|
|
};
|
|
|
|
handleReportReasonChange(i: BasePostListing, event: any) {
|
|
i.setState({
|
|
reportReason: event.target.value,
|
|
});
|
|
}
|
|
|
|
handleReportSubmit(i: BasePostListing, e: any) {
|
|
e.preventDefault();
|
|
|
|
WebSocketService.Instance.createPostReport({
|
|
post: i.props.post.id,
|
|
reason: i.state.reportReason,
|
|
});
|
|
|
|
i.setState({
|
|
reportReason: null,
|
|
showReportDialog: false,
|
|
});
|
|
}
|
|
|
|
handleShowAdvanced = () => {
|
|
this.setState(
|
|
prevState => ({
|
|
showAdvanced: !prevState.showAdvanced,
|
|
}),
|
|
() => {
|
|
setupTippy();
|
|
}
|
|
);
|
|
};
|
|
|
|
get pointsTippy(): string {
|
|
let points = i18n.t('number_of_points', {
|
|
count: this.state.score,
|
|
});
|
|
|
|
let upvotes = i18n.t('number_of_upvotes', {
|
|
count: this.state.upvotes,
|
|
});
|
|
|
|
let downvotes = i18n.t('number_of_downvotes', {
|
|
count: this.state.downvotes,
|
|
});
|
|
|
|
return `${points} • ${upvotes} • ${downvotes}`;
|
|
}
|
|
}
|
|
|
|
export const PostListing = withRouter(BasePostListing);
|