import {
Post,
CreatePostLikeForm,
- PostForm as PostFormI,
+ DeletePostForm,
+ RemovePostForm,
+ LockPostForm,
+ StickyPostForm,
SavePostForm,
CommunityUser,
UserView,
- BanType,
BanFromCommunityForm,
BanUserForm,
AddModToCommunityForm,
AddAdminForm,
TransferSiteForm,
TransferCommunityForm,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { BanType } 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 {
+ md,
mdToHtml,
canMod,
isMod,
isImage,
isVideo,
getUnixTime,
- pictshareAvatarThumbnail,
- showAvatars,
- pictshareImage,
+ pictrsImage,
setupTippy,
+ hostname,
+ previewLines,
} from '../utils';
import { i18n } from '../i18next';
showRemoveDialog: boolean;
removeReason: string;
showBanDialog: boolean;
+ removeData: boolean;
banReason: string;
banExpires: string;
banType: BanType;
showBody?: boolean;
moderators?: Array<CommunityUser>;
admins?: Array<UserView>;
+ enableDownvotes: boolean;
+ enableNsfw: boolean;
}
export class PostListing extends Component<PostListingProps, PostListingState> {
showRemoveDialog: false,
removeReason: null,
showBanDialog: false,
+ removeData: null,
banReason: null,
banExpires: null,
banType: BanType.Community,
this.state.upvotes = nextProps.post.upvotes;
this.state.downvotes = nextProps.post.downvotes;
this.state.score = nextProps.post.score;
+ if (this.props.post.id !== nextProps.post.id) {
+ this.state.imageExpanded = false;
+ }
this.setState(this.state);
}
post={this.props.post}
onEdit={this.handleEditPost}
onCancel={this.handleEditCancel}
+ enableNsfw={this.props.enableNsfw}
+ enableDownvotes={this.props.enableDownvotes}
/>
</div>
)}
let post = this.props.post;
return (
<img
- className={`img-fluid thumbnail rounded ${(post.nsfw ||
- post.community_nsfw) &&
- 'img-blur'}`}
+ className={`img-fluid thumbnail rounded ${
+ post.nsfw || post.community_nsfw ? 'img-blur' : ''
+ }`}
src={src}
/>
);
getImage(thumbnail: boolean = false) {
let post = this.props.post;
if (isImage(post.url)) {
- if (post.url.includes('pictshare')) {
- return pictshareImage(post.url, thumbnail);
+ 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 pictshareImage(post.thumbnail_url, thumbnail);
+ return pictrsImage(post.thumbnail_url, thumbnail);
}
}
if (isImage(post.url)) {
return (
- <span
- class="text-body pointer"
+ <div
+ class="float-right text-body pointer d-inline-block position-relative"
data-tippy-content={i18n.t('expand_here')}
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<svg class="icon mini-overlay">
<use xlinkHref="#icon-image"></use>
</svg>
- </span>
+ </div>
);
} else if (post.thumbnail_url) {
return (
<a
- className="text-body"
+ class="float-right text-body d-inline-block position-relative"
href={post.url}
target="_blank"
+ rel="noopener"
title={post.url}
>
{this.imgThumb(this.getImage(true))}
href={post.url}
target="_blank"
title={post.url}
+ rel="noopener"
>
- <svg class="icon thumbnail">
- <use xlinkHref="#icon-external-link"></use>
- </svg>
+ <div class="thumbnail rounded bg-light d-flex justify-content-center">
+ <svg class="icon d-flex align-items-center">
+ <use xlinkHref="#icon-external-link"></use>
+ </svg>
+ </div>
</a>
);
}
to={`/post/${post.id}`}
title={i18n.t('comments')}
>
- <svg class="icon thumbnail">
- <use xlinkHref="#icon-message-square"></use>
- </svg>
+ <div class="thumbnail rounded bg-light d-flex justify-content-center">
+ <svg class="icon d-flex align-items-center">
+ <use xlinkHref="#icon-message-square"></use>
+ </svg>
+ </div>
</Link>
);
}
}
- listing() {
+ createdLine() {
let post = this.props.post;
return (
- <div class="row">
- <div className={`vote-bar col-1 pr-0 small text-center`}>
+ <ul class="list-inline mb-1 text-muted small">
+ <li className="list-inline-item">
+ <UserListing
+ user={{
+ name: post.creator_name,
+ preferred_username: post.creator_preferred_username,
+ avatar: post.creator_avatar,
+ id: post.creator_id,
+ local: post.creator_local,
+ actor_id: post.creator_actor_id,
+ published: post.creator_published,
+ }}
+ />
+
+ {this.isMod && (
+ <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
+ )}
+ {this.isAdmin && (
+ <span className="mx-1 badge badge-light">{i18n.t('admin')}</span>
+ )}
+ {(post.banned_from_community || post.banned) && (
+ <span className="mx-1 badge badge-danger">{i18n.t('banned')}</span>
+ )}
+ {this.props.showCommunity && (
+ <span>
+ <span class="mx-1"> {i18n.t('to')} </span>
+ <CommunityLink
+ community={{
+ name: post.community_name,
+ id: post.community_id,
+ local: post.community_local,
+ actor_id: post.community_actor_id,
+ icon: post.community_icon,
+ }}
+ />
+ </span>
+ )}
+ </li>
+ <li className="list-inline-item">•</li>
+ {post.url && !(hostname(post.url) == window.location.hostname) && (
+ <>
+ <li className="list-inline-item">
+ <a
+ className="text-muted font-italic"
+ href={post.url}
+ target="_blank"
+ title={post.url}
+ rel="noopener"
+ >
+ {hostname(post.url)}
+ </a>
+ </li>
+ <li className="list-inline-item">•</li>
+ </>
+ )}
+ <li className="list-inline-item">
+ <span>
+ <MomentTime data={post} />
+ </span>
+ </li>
+ {post.body && (
+ <>
+ <li className="list-inline-item">•</li>
+ <li className="list-inline-item">
+ {/* Using a link with tippy doesn't work on touch devices unfortunately */}
+ <Link
+ className="text-muted"
+ data-tippy-content={md.render(previewLines(post.body))}
+ data-tippy-allowHtml={true}
+ to={`/post/${post.id}`}
+ >
+ <svg class="mr-1 icon icon-inline">
+ <use xlinkHref="#icon-book-open"></use>
+ </svg>
+ </Link>
+ </li>
+ </>
+ )}
+ </ul>
+ );
+ }
+
+ voteBar() {
+ return (
+ <div className={`vote-bar col-1 pr-0 small text-center`}>
+ <button
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+ }`}
+ onClick={linkEvent(this, this.handlePostLike)}
+ data-tippy-content={i18n.t('upvote')}
+ >
+ <svg class="icon upvote">
+ <use xlinkHref="#icon-arrow-up1"></use>
+ </svg>
+ </button>
+ <div
+ class={`unselectable pointer font-weight-bold text-muted px-1`}
+ data-tippy-content={this.pointsTippy}
+ >
+ {this.state.score}
+ </div>
+ {this.props.enableDownvotes && (
<button
- className={`btn-animate btn btn-link btn-lg p-0 ${
- this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
- onClick={linkEvent(this, this.handlePostLike)}
- data-tippy-content={i18n.t('upvote')}
+ onClick={linkEvent(this, this.handlePostDisLike)}
+ data-tippy-content={i18n.t('downvote')}
>
- <svg class="icon upvote">
- <use xlinkHref="#icon-arrow-up"></use>
+ <svg class="icon downvote">
+ <use xlinkHref="#icon-arrow-down1"></use>
</svg>
</button>
- <div
- class={`unselectable pointer font-weight-bold text-muted px-1`}
- data-tippy-content={this.pointsTippy}
- >
- {this.state.score}
- </div>
- {WebSocketService.Instance.site.enable_downvotes && (
- <button
- className={`btn-animate btn btn-link btn-lg p-0 ${
- this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
- }`}
- onClick={linkEvent(this, this.handlePostDisLike)}
- data-tippy-content={i18n.t('downvote')}
+ )}
+ </div>
+ );
+ }
+
+ postTitleLine() {
+ let post = this.props.post;
+ return (
+ <div className="post-title overflow-hidden">
+ <h5>
+ {this.props.showBody && post.url ? (
+ <a
+ className={!post.stickied ? 'text-body' : 'text-primary'}
+ href={post.url}
+ target="_blank"
+ title={post.url}
+ rel="noopener"
+ >
+ {post.name}
+ </a>
+ ) : (
+ <Link
+ className={!post.stickied ? 'text-body' : 'text-primary'}
+ to={`/post/${post.id}`}
+ title={i18n.t('comments')}
>
- <svg class="icon downvote">
- <use xlinkHref="#icon-arrow-down"></use>
+ {post.name}
+ </Link>
+ )}
+ {(isImage(post.url) || this.props.post.thumbnail_url) && (
+ <>
+ {!this.state.imageExpanded ? (
+ <span
+ class="text-monospace unselectable pointer ml-2 text-muted small"
+ data-tippy-content={i18n.t('expand_here')}
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-plus-square"></use>
+ </svg>
+ </span>
+ ) : (
+ <span>
+ <span
+ class="text-monospace unselectable pointer ml-2 text-muted small"
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-minus-square"></use>
+ </svg>
+ </span>
+ <div>
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ <img
+ class="img-fluid img-expanded"
+ 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 class={`icon icon-inline text-danger`}>
+ <use xlinkHref="#icon-trash"></use>
</svg>
- </button>
+ </small>
)}
- </div>
- {!this.state.imageExpanded && (
- <div class="col-3 col-sm-2 pr-0 mt-1">
- <div class="position-relative">{this.thumbnail()}</div>
- </div>
+ {post.locked && (
+ <small
+ className="unselectable pointer ml-2 text-muted font-italic"
+ data-tippy-content={i18n.t('locked')}
+ >
+ <svg class={`icon icon-inline text-danger`}>
+ <use xlinkHref="#icon-lock"></use>
+ </svg>
+ </small>
+ )}
+ {post.stickied && (
+ <small
+ className="unselectable pointer ml-2 text-muted font-italic"
+ data-tippy-content={i18n.t('stickied')}
+ >
+ <svg class={`icon icon-inline text-primary`}>
+ <use xlinkHref="#icon-pin"></use>
+ </svg>
+ </small>
+ )}
+ {post.nsfw && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('nsfw')}
+ </small>
+ )}
+ </h5>
+ </div>
+ );
+ }
+
+ commentsLine(showVotes: boolean = false) {
+ let post = this.props.post;
+ return (
+ <ul class="d-flex align-items-center list-inline mb-1 text-muted small">
+ <li className="list-inline-item">
+ <Link
+ className="text-muted"
+ title={i18n.t('number_of_comments', {
+ count: post.number_of_comments,
+ })}
+ to={`/post/${post.id}`}
+ >
+ <svg class="mr-1 icon icon-inline">
+ <use xlinkHref="#icon-message-square"></use>
+ </svg>
+ {i18n.t('number_of_comments', {
+ count: post.number_of_comments,
+ })}
+ </Link>
+ </li>
+ {(showVotes || this.state.upvotes !== this.state.score) && (
+ <>
+ <span
+ class="unselectable pointer ml-3"
+ data-tippy-content={this.pointsTippy}
+ >
+ <li className="list-inline-item">
+ <a
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+ }`}
+ onClick={linkEvent(this, this.handlePostLike)}
+ >
+ <svg class="small icon icon-inline mx-1">
+ <use xlinkHref="#icon-arrow-up1"></use>
+ </svg>
+ {this.state.upvotes}
+ </a>
+ </li>
+ <li className="list-inline-item">
+ <a
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
+ }`}
+ onClick={linkEvent(this, this.handlePostDisLike)}
+ >
+ <svg class="small icon icon-inline mx-1">
+ <use xlinkHref="#icon-arrow-down1"></use>
+ </svg>
+ {this.state.downvotes}
+ </a>
+ </li>
+ </span>
+ </>
)}
- <div
- class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
- >
- <div class="row">
- <div className="col-12">
- <div className="post-title">
- <h5 className="mb-0 d-inline">
- {this.props.showBody && post.url ? (
- <a
- className="text-body"
- href={post.url}
- target="_blank"
- title={post.url}
+ </ul>
+ );
+ }
+
+ duplicatesLine() {
+ return (
+ this.props.post.duplicates && (
+ <ul class="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">
+ <Link to={`/post/${post.id}`}>{post.community_name}</Link>
+ </li>
+ ))}
+ </>
+ </ul>
+ )
+ );
+ }
+
+ postActions() {
+ let post = this.props.post;
+ return (
+ <ul class="list-inline mb-1 text-muted font-weight-bold">
+ {UserService.Instance.user && (
+ <>
+ {this.props.showBody && (
+ <>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleSavePostClick)}
+ data-tippy-content={
+ post.saved ? i18n.t('unsave') : i18n.t('save')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${post.saved && 'text-warning'}`}
+ >
+ <use xlinkHref="#icon-star"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <Link
+ class="btn btn-link btn-animate text-muted"
+ to={`/create_post${this.crossPostParams}`}
+ title={i18n.t('cross_post')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-copy"></use>
+ </svg>
+ </Link>
+ </li>
+ </>
+ )}
+ {this.myPost && this.props.showBody && (
+ <>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleEditClick)}
+ data-tippy-content={i18n.t('edit')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleDeleteClick)}
+ data-tippy-content={
+ !post.deleted ? i18n.t('delete') : i18n.t('restore')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ post.deleted && 'text-danger'
+ }`}
>
- {post.name}
- </a>
- ) : (
- <Link
- className="text-body"
- to={`/post/${post.id}`}
- title={i18n.t('comments')}
+ <use xlinkHref="#icon-trash"></use>
+ </svg>
+ </button>
+ </li>
+ </>
+ )}
+
+ {!this.state.showAdvanced && this.props.showBody ? (
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleShowAdvanced)}
+ data-tippy-content={i18n.t('more')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-more-vertical"></use>
+ </svg>
+ </button>
+ </li>
+ ) : (
+ <>
+ {this.props.showBody && post.body && (
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleViewSource)}
+ data-tippy-content={i18n.t('view_source')}
>
- {post.name}
- </Link>
- )}
- </h5>
- {post.url &&
- !(new URL(post.url).hostname == window.location.hostname) && (
- <small class="d-inline-block">
- <a
- className="ml-2 text-muted font-italic"
- href={post.url}
- target="_blank"
- title={post.url}
+ <svg
+ class={`icon icon-inline ${
+ this.state.viewSource && 'text-success'
+ }`}
>
- {new URL(post.url).hostname}
- <svg class="ml-1 icon icon-inline">
- <use xlinkHref="#icon-external-link"></use>
- </svg>
- </a>
- </small>
- )}
- {(isImage(post.url) || this.props.post.thumbnail_url) && (
+ <use xlinkHref="#icon-file-text"></use>
+ </svg>
+ </button>
+ </li>
+ )}
+ {this.canModOnSelf && (
<>
- {!this.state.imageExpanded ? (
- <span
- class="text-monospace unselectable pointer ml-2 text-muted small"
- data-tippy-content={i18n.t('expand_here')}
- onClick={linkEvent(this, this.handleImageExpandClick)}
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleModLock)}
+ data-tippy-content={
+ post.locked ? i18n.t('unlock') : i18n.t('lock')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ post.locked && 'text-danger'
+ }`}
+ >
+ <use xlinkHref="#icon-lock"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleModSticky)}
+ data-tippy-content={
+ post.stickied ? i18n.t('unsticky') : i18n.t('sticky')
+ }
>
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-plus-square"></use>
+ <svg
+ class={`icon icon-inline ${
+ post.stickied && 'text-success'
+ }`}
+ >
+ <use xlinkHref="#icon-pin"></use>
</svg>
+ </button>
+ </li>
+ </>
+ )}
+ {/* Mods can ban from community, and appoint as mods to community */}
+ {(this.canMod || this.canAdmin) && (
+ <li className="list-inline-item">
+ {!post.removed ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModRemoveShow)}
+ >
+ {i18n.t('remove')}
</span>
) : (
- <span>
- <span
- class="text-monospace unselectable pointer ml-2 text-muted small"
- onClick={linkEvent(this, this.handleImageExpandClick)}
- >
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-minus-square"></use>
- </svg>
- </span>
- <div>
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModRemoveSubmit)}
+ >
+ {i18n.t('restore')}
+ </span>
+ )}
+ </li>
+ )}
+ {this.canMod && (
+ <>
+ {!this.isMod && (
+ <li className="list-inline-item">
+ {!post.banned_from_community ? (
<span
class="pointer"
onClick={linkEvent(
this,
- this.handleImageExpandClick
+ this.handleModBanFromCommunityShow
)}
>
- <img
- class="img-fluid img-expanded"
- src={this.getImage()}
- />
+ {i18n.t('ban')}
</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 class={`icon icon-inline text-danger`}>
- <use xlinkHref="#icon-trash"></use>
- </svg>
- </small>
- )}
- {post.locked && (
- <small
- className="unselectable pointer ml-2 text-muted font-italic"
- data-tippy-content={i18n.t('locked')}
- >
- <svg class={`icon icon-inline text-danger`}>
- <use xlinkHref="#icon-lock"></use>
- </svg>
- </small>
- )}
- {post.stickied && (
- <small
- className="unselectable pointer ml-2 text-muted font-italic"
- data-tippy-content={i18n.t('stickied')}
- >
- <svg class={`icon icon-inline text-success`}>
- <use xlinkHref="#icon-pin"></use>
- </svg>
- </small>
- )}
- {post.nsfw && (
- <small className="ml-2 text-muted font-italic">
- {i18n.t('nsfw')}
- </small>
- )}
- </div>
- </div>
- </div>
- <div class="row">
- <div className="details col-12">
- <ul class="list-inline mb-0 text-muted small">
- <li className="list-inline-item">
- <span>{i18n.t('by')} </span>
- <Link
- className="text-body font-weight-bold"
- to={`/u/${post.creator_name}`}
- >
- {post.creator_avatar && showAvatars() && (
- <img
- height="32"
- width="32"
- src={pictshareAvatarThumbnail(post.creator_avatar)}
- class="rounded-circle mr-1"
- />
- )}
- <span>{post.creator_name}</span>
- </Link>
- {this.isMod && (
- <span className="mx-1 badge badge-light">
- {i18n.t('mod')}
- </span>
- )}
- {this.isAdmin && (
- <span className="mx-1 badge badge-light">
- {i18n.t('admin')}
- </span>
- )}
- {(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>
- <Link to={`/c/${post.community_name}`}>
- {post.community_name}
- </Link>
- </span>
- )}
- </li>
- <li className="list-inline-item">•</li>
- <li className="list-inline-item">
- <span>
- <MomentTime data={post} />
- </span>
- </li>
- <li className="list-inline-item">•</li>
- {this.state.upvotes !== this.state.score && (
- <>
- <span
- class="unselectable pointer mr-2"
- data-tippy-content={this.pointsTippy}
- >
- <li className="list-inline-item">
- <span className="text-muted">
- <svg class="small icon icon-inline mr-1">
- <use xlinkHref="#icon-arrow-up"></use>
- </svg>
- {this.state.upvotes}
- </span>
+ ) : (
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleModBanFromCommunitySubmit
+ )}
+ >
+ {i18n.t('unban')}
+ </span>
+ )}
</li>
+ )}
+ {!post.banned_from_community && post.creator_local && (
<li className="list-inline-item">
- <span className="text-muted">
- <svg class="small icon icon-inline mr-1">
- <use xlinkHref="#icon-arrow-down"></use>
- </svg>
- {this.state.downvotes}
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleAddModToCommunity
+ )}
+ >
+ {this.isMod
+ ? i18n.t('remove_as_mod')
+ : i18n.t('appoint_as_mod')}
</span>
</li>
- </span>
- <li className="list-inline-item">•</li>
+ )}
</>
)}
- <li className="list-inline-item">
- <Link
- className="text-muted"
- title={i18n.t('number_of_comments', {
- count: post.number_of_comments,
- })}
- to={`/post/${post.id}`}
- >
- <svg class="mr-1 icon icon-inline">
- <use xlinkHref="#icon-message-square"></use>
- </svg>
- {post.number_of_comments}
- </Link>
- </li>
- </ul>
- {this.props.post.duplicates && (
- <ul class="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">
- <Link to={`/post/${post.id}`}>
- {post.community_name}
- </Link>
- </li>
- ))}
- </>
- </ul>
- )}
- <ul class="list-inline mb-1 text-muted font-weight-bold">
- {UserService.Instance.user && (
- <>
- {this.props.showBody && (
- <>
- <li className="list-inline-item">
- <button
- class="btn btn-sm btn-link btn-animate text-muted"
- onClick={linkEvent(this, this.handleSavePostClick)}
- data-tippy-content={
- post.saved ? i18n.t('unsave') : i18n.t('save')
- }
+ {/* Community creators and admins can transfer community to another mod */}
+ {(this.amCommunityCreator || this.canAdmin) &&
+ this.isMod &&
+ post.creator_local && (
+ <li className="list-inline-item">
+ {!this.state.showConfirmTransferCommunity ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmTransferCommunity
+ )}
+ >
+ {i18n.t('transfer_community')}
+ </span>
+ ) : (
+ <>
+ <span class="d-inline-block mr-1">
+ {i18n.t('are_you_sure')}
+ </span>
+ <span
+ class="pointer d-inline-block mr-1"
+ onClick={linkEvent(
+ this,
+ this.handleTransferCommunity
+ )}
>
- <svg
- class={`icon icon-inline ${post.saved &&
- 'text-warning'}`}
- >
- <use xlinkHref="#icon-star"></use>
- </svg>
- </button>
- </li>
- <li className="list-inline-item">
- <Link
- class="btn btn-sm btn-link btn-animate text-muted"
- to={`/create_post${this.crossPostParams}`}
- title={i18n.t('cross_post')}
+ {i18n.t('yes')}
+ </span>
+ <span
+ class="pointer d-inline-block"
+ onClick={linkEvent(
+ this,
+ this.handleCancelShowConfirmTransferCommunity
+ )}
>
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-copy"></use>
- </svg>
- </Link>
- </li>
- </>
- )}
- {this.myPost && this.props.showBody && (
- <>
- <li className="list-inline-item">
- <button
- class="btn btn-sm btn-link btn-animate text-muted"
- onClick={linkEvent(this, this.handleEditClick)}
- data-tippy-content={i18n.t('edit')}
+ {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 ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModBanShow)}
>
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-edit"></use>
- </svg>
- </button>
- </li>
- <li className="list-inline-item">
- <button
- class="btn btn-sm btn-link btn-animate text-muted"
- onClick={linkEvent(this, this.handleDeleteClick)}
- data-tippy-content={
- !post.deleted
- ? i18n.t('delete')
- : i18n.t('restore')
- }
+ {i18n.t('ban_from_site')}
+ </span>
+ ) : (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModBanSubmit)}
>
- <svg
- class={`icon icon-inline ${post.deleted &&
- 'text-danger'}`}
- >
- <use xlinkHref="#icon-trash"></use>
- </svg>
- </button>
- </li>
- </>
+ {i18n.t('unban_from_site')}
+ </span>
+ )}
+ </li>
)}
-
- {!this.state.showAdvanced && this.props.showBody ? (
+ {!post.banned && post.creator_local && (
<li className="list-inline-item">
- <button
- class="btn btn-sm btn-link btn-animate text-muted"
- onClick={linkEvent(this, this.handleShowAdvanced)}
- data-tippy-content={i18n.t('more')}
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleAddAdmin)}
>
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-more-vertical"></use>
- </svg>
- </button>
+ {this.isAdmin
+ ? i18n.t('remove_as_admin')
+ : i18n.t('appoint_as_admin')}
+ </span>
</li>
+ )}
+ </>
+ )}
+ {/* Site Creator can transfer to another admin */}
+ {this.amSiteCreator && this.isAdmin && (
+ <li className="list-inline-item">
+ {!this.state.showConfirmTransferSite ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmTransferSite
+ )}
+ >
+ {i18n.t('transfer_site')}
+ </span>
) : (
<>
- {this.props.showBody && post.body && (
- <li className="list-inline-item">
- <button
- class="btn btn-sm btn-link btn-animate text-muted"
- onClick={linkEvent(this, this.handleViewSource)}
- data-tippy-content={i18n.t('view_source')}
- >
- <svg
- class={`icon icon-inline ${this.state
- .viewSource && 'text-success'}`}
- >
- <use xlinkHref="#icon-file-text"></use>
- </svg>
- </button>
- </li>
- )}
- {this.canModOnSelf && (
- <>
- <li className="list-inline-item">
- <button
- class="btn btn-sm btn-link btn-animate text-muted"
- onClick={linkEvent(this, this.handleModLock)}
- data-tippy-content={
- post.locked
- ? i18n.t('unlock')
- : i18n.t('lock')
- }
- >
- <svg
- class={`icon icon-inline ${post.locked &&
- 'text-danger'}`}
- >
- <use xlinkHref="#icon-lock"></use>
- </svg>
- </button>
- </li>
- <li className="list-inline-item">
- <button
- class="btn btn-sm btn-link btn-animate text-muted"
- onClick={linkEvent(this, this.handleModSticky)}
- data-tippy-content={
- post.stickied
- ? i18n.t('unsticky')
- : i18n.t('sticky')
- }
- >
- <svg
- class={`icon icon-inline ${post.stickied &&
- 'text-success'}`}
- >
- <use xlinkHref="#icon-pin"></use>
- </svg>
- </button>
- </li>
- </>
- )}
- {/* Mods can ban from community, and appoint as mods to community */}
- {(this.canMod || this.canAdmin) && (
- <li className="list-inline-item">
- {!post.removed ? (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleModRemoveShow
- )}
- >
- {i18n.t('remove')}
- </span>
- ) : (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleModRemoveSubmit
- )}
- >
- {i18n.t('restore')}
- </span>
- )}
- </li>
- )}
- {this.canMod && (
- <>
- {!this.isMod && (
- <li className="list-inline-item">
- {!post.banned_from_community ? (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleModBanFromCommunityShow
- )}
- >
- {i18n.t('ban')}
- </span>
- ) : (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleModBanFromCommunitySubmit
- )}
- >
- {i18n.t('unban')}
- </span>
- )}
- </li>
- )}
- {!post.banned_from_community && (
- <li className="list-inline-item">
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleAddModToCommunity
- )}
- >
- {this.isMod
- ? i18n.t('remove_as_mod')
- : i18n.t('appoint_as_mod')}
- </span>
- </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 ? (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleShowConfirmTransferCommunity
- )}
- >
- {i18n.t('transfer_community')}
- </span>
- ) : (
- <>
- <span class="d-inline-block mr-1">
- {i18n.t('are_you_sure')}
- </span>
- <span
- class="pointer d-inline-block mr-1"
- onClick={linkEvent(
- this,
- this.handleTransferCommunity
- )}
- >
- {i18n.t('yes')}
- </span>
- <span
- class="pointer d-inline-block"
- onClick={linkEvent(
- this,
- this
- .handleCancelShowConfirmTransferCommunity
- )}
- >
- {i18n.t('no')}
- </span>
- </>
- )}
- </li>
+ <span class="d-inline-block mr-1">
+ {i18n.t('are_you_sure')}
+ </span>
+ <span
+ class="pointer d-inline-block mr-1"
+ onClick={linkEvent(this, this.handleTransferSite)}
+ >
+ {i18n.t('yes')}
+ </span>
+ <span
+ class="pointer d-inline-block"
+ onClick={linkEvent(
+ this,
+ this.handleCancelShowConfirmTransferSite
)}
- {/* Admins can ban from all, and appoint other admins */}
- {this.canAdmin && (
- <>
- {!this.isAdmin && (
- <li className="list-inline-item">
- {!post.banned ? (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleModBanShow
- )}
- >
- {i18n.t('ban_from_site')}
- </span>
- ) : (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleModBanSubmit
- )}
- >
- {i18n.t('unban_from_site')}
- </span>
- )}
- </li>
- )}
- {!post.banned && (
- <li className="list-inline-item">
- <span
- class="pointer"
- onClick={linkEvent(this, this.handleAddAdmin)}
- >
- {this.isAdmin
- ? i18n.t('remove_as_admin')
- : i18n.t('appoint_as_admin')}
- </span>
- </li>
- )}
- </>
- )}
- {/* Site Creator can transfer to another admin */}
- {this.amSiteCreator && this.isAdmin && (
- <li className="list-inline-item">
- {!this.state.showConfirmTransferSite ? (
- <span
- class="pointer"
- onClick={linkEvent(
- this,
- this.handleShowConfirmTransferSite
- )}
- >
- {i18n.t('transfer_site')}
- </span>
- ) : (
- <>
- <span class="d-inline-block mr-1">
- {i18n.t('are_you_sure')}
- </span>
- <span
- class="pointer d-inline-block mr-1"
- onClick={linkEvent(
- this,
- this.handleTransferSite
- )}
- >
- {i18n.t('yes')}
- </span>
- <span
- class="pointer d-inline-block"
- onClick={linkEvent(
- this,
- this.handleCancelShowConfirmTransferSite
- )}
- >
- {i18n.t('no')}
- </span>
- </>
- )}
- </li>
- )}
+ >
+ {i18n.t('no')}
+ </span>
</>
)}
- </>
+ </li>
)}
- </ul>
- {this.state.showRemoveDialog && (
- <form
- class="form-inline"
- onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
- >
+ </>
+ )}
+ </>
+ )}
+ </ul>
+ );
+ }
+
+ removeAndBanDialogs() {
+ let post = this.props.post;
+ return (
+ <>
+ {this.state.showRemoveDialog && (
+ <form
+ class="form-inline"
+ onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
+ >
+ <input
+ type="text"
+ class="form-control mr-2"
+ placeholder={i18n.t('reason')}
+ value={this.state.removeReason}
+ onInput={linkEvent(this, this.handleModRemoveReasonChange)}
+ />
+ <button type="submit" class="btn btn-secondary">
+ {i18n.t('remove_post')}
+ </button>
+ </form>
+ )}
+ {this.state.showBanDialog && (
+ <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
+ <div class="form-group row">
+ <label class="col-form-label" htmlFor="post-listing-reason">
+ {i18n.t('reason')}
+ </label>
+ <input
+ type="text"
+ id="post-listing-reason"
+ class="form-control mr-2"
+ placeholder={i18n.t('reason')}
+ value={this.state.banReason}
+ onInput={linkEvent(this, this.handleModBanReasonChange)}
+ />
+ <div class="form-group">
+ <div class="form-check">
<input
- type="text"
- class="form-control mr-2"
- placeholder={i18n.t('reason')}
- value={this.state.removeReason}
- onInput={linkEvent(this, this.handleModRemoveReasonChange)}
+ class="form-check-input"
+ id="mod-ban-remove-data"
+ type="checkbox"
+ checked={this.state.removeData}
+ onChange={linkEvent(this, this.handleModRemoveDataChange)}
/>
- <button type="submit" class="btn btn-secondary">
- {i18n.t('remove_post')}
- </button>
- </form>
- )}
- {this.state.showBanDialog && (
- <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
- <div class="form-group row">
- <label class="col-form-label" htmlFor="post-listing-reason">
- {i18n.t('reason')}
- </label>
- <input
- type="text"
- id="post-listing-reason"
- class="form-control mr-2"
- placeholder={i18n.t('reason')}
- value={this.state.banReason}
- onInput={linkEvent(this, this.handleModBanReasonChange)}
- />
- </div>
- {/* TODO hold off on expires until later */}
- {/* <div class="form-group row"> */}
- {/* <label class="col-form-label">Expires</label> */}
- {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
- {/* </div> */}
- <div class="form-group row">
- <button type="submit" class="btn btn-secondary">
- {i18n.t('ban')} {post.creator_name}
- </button>
- </div>
- </form>
- )}
+ <label class="form-check-label" htmlFor="mod-ban-remove-data">
+ {i18n.t('remove_posts_comments')}
+ </label>
+ </div>
+ </div>
</div>
- </div>
+ {/* TODO hold off on expires until later */}
+ {/* <div class="form-group row"> */}
+ {/* <label class="col-form-label">Expires</label> */}
+ {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+ {/* </div> */}
+ <div class="form-group row">
+ <button type="submit" class="btn btn-secondary">
+ {i18n.t('ban')} {post.creator_name}
+ </button>
+ </div>
+ </form>
+ )}
+ </>
+ );
+ }
+
+ mobileThumbnail() {
+ return this.props.post.thumbnail_url || isImage(this.props.post.url) ? (
+ <div class="row">
+ <div className={`${this.state.imageExpanded ? 'col-12' : 'col-8'}`}>
+ {this.postTitleLine()}
+ </div>
+ <div class="col-4">
+ {/* Post body prev or thumbnail */}
+ {!this.state.imageExpanded && this.thumbnail()}
</div>
</div>
+ ) : (
+ this.postTitleLine()
+ );
+ }
+
+ showMobilePreview() {
+ return (
+ this.props.post.body &&
+ !this.props.showBody && (
+ <div
+ className="md-div mb-1"
+ dangerouslySetInnerHTML={{
+ __html: md.render(previewLines(this.props.post.body)),
+ }}
+ />
+ )
+ );
+ }
+
+ listing() {
+ return (
+ <>
+ {/* The mobile view*/}
+ <div class="d-block d-sm-none">
+ <div class="row">
+ <div class="col-12">
+ {this.createdLine()}
+
+ {/* If it has a thumbnail, do a right aligned thumbnail */}
+ {this.mobileThumbnail()}
+
+ {/* Show a preview of the post body */}
+ {this.showMobilePreview()}
+
+ {this.commentsLine(true)}
+ {this.duplicatesLine()}
+ {this.postActions()}
+ {this.removeAndBanDialogs()}
+ </div>
+ </div>
+ </div>
+
+ {/* The larger view*/}
+ <div class="d-none d-sm-block">
+ <div class="row">
+ {this.voteBar()}
+ {!this.state.imageExpanded && (
+ <div class="col-sm-2 pr-0">
+ <div class="">{this.thumbnail()}</div>
+ </div>
+ )}
+ <div
+ class={`${
+ this.state.imageExpanded ? 'col-12' : 'col-12 col-sm-9'
+ }`}
+ >
+ <div class="row">
+ <div className="col-12">
+ {this.postTitleLine()}
+ {this.createdLine()}
+ {this.commentsLine()}
+ {this.duplicatesLine()}
+ {this.postActions()}
+ {this.removeAndBanDialogs()}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </>
);
}
}
handlePostLike(i: PostListing) {
+ if (!UserService.Instance.user) {
+ this.context.router.history.push(`/login`);
+ }
+
let new_vote = i.state.my_vote == 1 ? 0 : 1;
if (i.state.my_vote == 1) {
}
handlePostDisLike(i: PostListing) {
+ if (!UserService.Instance.user) {
+ this.context.router.history.push(`/login`);
+ }
+
let new_vote = i.state.my_vote == -1 ? 0 : -1;
if (i.state.my_vote == 1) {
}
handleDeleteClick(i: PostListing) {
- let deleteForm: PostFormI = {
- body: i.props.post.body,
- community_id: i.props.post.community_id,
- name: i.props.post.name,
- url: i.props.post.url,
+ let deleteForm: DeletePostForm = {
edit_id: i.props.post.id,
- creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted,
- nsfw: i.props.post.nsfw,
auth: null,
};
- WebSocketService.Instance.editPost(deleteForm);
+ WebSocketService.Instance.deletePost(deleteForm);
}
handleSavePostClick(i: PostListing) {
let post = this.props.post;
if (post.url) {
- params += `&url=${post.url}`;
+ params += `&url=${encodeURIComponent(post.url)}`;
}
if (this.props.post.body) {
params += `&body=${this.props.post.body}`;
i.setState(i.state);
}
+ handleModRemoveDataChange(i: PostListing, event: any) {
+ i.state.removeData = event.target.checked;
+ i.setState(i.state);
+ }
+
handleModRemoveSubmit(i: PostListing) {
event.preventDefault();
- let form: PostFormI = {
- name: i.props.post.name,
- community_id: i.props.post.community_id,
+ let form: RemovePostForm = {
edit_id: i.props.post.id,
- creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
reason: i.state.removeReason,
- nsfw: i.props.post.nsfw,
auth: null,
};
- WebSocketService.Instance.editPost(form);
+ WebSocketService.Instance.removePost(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handleModLock(i: PostListing) {
- let form: PostFormI = {
- name: i.props.post.name,
- community_id: i.props.post.community_id,
+ let form: LockPostForm = {
edit_id: i.props.post.id,
- creator_id: i.props.post.creator_id,
- nsfw: i.props.post.nsfw,
locked: !i.props.post.locked,
auth: null,
};
- WebSocketService.Instance.editPost(form);
+ WebSocketService.Instance.lockPost(form);
}
handleModSticky(i: PostListing) {
- let form: PostFormI = {
- name: i.props.post.name,
- community_id: i.props.post.community_id,
+ let form: StickyPostForm = {
edit_id: i.props.post.id,
- creator_id: i.props.post.creator_id,
- nsfw: i.props.post.nsfw,
stickied: !i.props.post.stickied,
auth: null,
};
- WebSocketService.Instance.editPost(form);
+ WebSocketService.Instance.stickyPost(form);
}
handleModBanFromCommunityShow(i: PostListing) {
event.preventDefault();
if (i.state.banType == BanType.Community) {
+ // If its an unban, restore all their data
+ let ban = !i.props.post.banned_from_community;
+ if (ban == false) {
+ i.state.removeData = false;
+ }
let form: BanFromCommunityForm = {
user_id: i.props.post.creator_id,
community_id: i.props.post.community_id,
- ban: !i.props.post.banned_from_community,
+ ban,
+ remove_data: i.state.removeData,
reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires),
};
WebSocketService.Instance.banFromCommunity(form);
} else {
+ // If its an unban, restore all their data
+ let ban = !i.props.post.banned;
+ if (ban == false) {
+ i.state.removeData = false;
+ }
let form: BanUserForm = {
user_id: i.props.post.creator_id,
- ban: !i.props.post.banned,
+ ban,
+ remove_data: i.state.removeData,
reason: i.state.banReason,
expires: getUnixTime(i.state.banExpires),
};