1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { WebSocketService, UserService } from '../services';
4 import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, CommunityUser, UserView } from '../interfaces';
5 import { MomentTime } from './moment-time';
6 import { PostForm } from './post-form';
7 import { mdToHtml, canMod, isMod, isImage } from '../utils';
8 import { i18n } from '../i18next';
9 import { T } from 'inferno-i18next';
11 interface PostListingState {
13 showRemoveDialog: boolean;
15 imageExpanded: boolean;
18 interface PostListingProps {
21 showCommunity?: boolean;
24 moderators?: Array<CommunityUser>;
25 admins?: Array<UserView>;
28 export class PostListing extends Component<PostListingProps, PostListingState> {
30 private emptyState: PostListingState = {
32 showRemoveDialog: false,
37 constructor(props: any, context: any) {
38 super(props, context);
40 this.state = this.emptyState;
41 this.handlePostLike = this.handlePostLike.bind(this);
42 this.handlePostDisLike = this.handlePostDisLike.bind(this);
43 this.handleEditPost = this.handleEditPost.bind(this);
44 this.handleEditCancel = this.handleEditCancel.bind(this);
49 <div class="row col-12">
52 : <PostForm post={this.props.post} onEdit={this.handleEditPost} onCancel={this.handleEditCancel}/>
59 let post = this.props.post;
62 <div className={`vote-bar mr-1 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
63 <button className={`btn btn-sm p-0 ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}>
64 <svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
66 <div class={`font-weight-bold text-muted`}>{post.score}</div>
67 <button className={`btn btn-sm p-0 ${post.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostDisLike)}>
68 <svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
71 {post.url && isImage(post.url) &&
72 <span title={i18n.t('expand_here')} class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 mt-1 float-left img-fluid thumbnail rounded" src={post.url} /></span>
74 <div className="ml-4">
75 <div className="post-title">
76 <h5 className="mb-0 d-inline">
78 <a className="text-white" href={post.url} target="_blank" title={post.url}>{post.name}</a> :
79 <Link className="text-white" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link>
84 <a className="ml-2 text-muted font-italic" href={post.url} target="_blank" title={post.url}>{(new URL(post.url)).hostname}</a>
87 { post.url && isImage(post.url) &&
89 { !this.state.imageExpanded
90 ? <span class="text-monospace pointer ml-2 text-muted small" title={i18n.t('expand_here')} onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span>
93 <span class="text-monospace pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>[-]</span>
95 <span class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="img-fluid" src={post.url} /></span>
102 <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
105 <small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
108 <small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small>
111 <small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
115 <div className="details ml-4">
116 <ul class="list-inline mb-0 text-muted small">
117 <li className="list-inline-item">
119 <Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link>
121 <span className="mx-1 badge badge-light"><T i18nKey="mod">#</T></span>
124 <span className="mx-1 badge badge-light"><T i18nKey="admin">#</T></span>
126 {this.props.showCommunity &&
129 <Link to={`/c/${post.community_name}`}>{post.community_name}</Link>
133 <li className="list-inline-item">
134 <span><MomentTime data={post} /></span>
136 <li className="list-inline-item">
138 <span className="text-info">+{post.upvotes}</span>
140 <span className="text-danger">-{post.downvotes}</span>
144 <li className="list-inline-item">
145 <Link className="text-muted" to={`/post/${post.id}`}><T i18nKey="number_of_comments" interpolation={{count: post.number_of_comments}}>#</T></Link>
148 {UserService.Instance.user && this.props.editable &&
149 <ul class="list-inline mb-1 text-muted small font-weight-bold">
150 <li className="list-inline-item mr-2">
151 <span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? i18n.t('unsave') : i18n.t('save')}</span>
153 <li className="list-inline-item mr-2">
154 <Link className="text-muted" to={`/create_post${this.crossPostParams}`}><T i18nKey="cross_post">#</T></Link>
158 <li className="list-inline-item">
159 <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
161 <li className="list-inline-item mr-2">
162 <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
163 {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
170 <li className="list-inline-item">
171 {!this.props.post.removed ?
172 <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
173 <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
176 <li className="list-inline-item">
177 <span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{this.props.post.locked ? i18n.t('unlock') : i18n.t('lock')}</span>
183 {this.state.showRemoveDialog &&
184 <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
185 <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
186 <button type="submit" class="btn btn-secondary"><T i18nKey="remove_post">#</T></button>
189 {this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />}
195 private get myPost(): boolean {
196 return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id;
199 get isMod(): boolean {
200 return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id);
203 get isAdmin(): boolean {
204 return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id);
207 get canMod(): boolean {
209 if (this.props.editable) {
210 let adminsThenMods = this.props.admins.map(a => a.id)
211 .concat(this.props.moderators.map(m => m.user_id));
213 return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id);
218 handlePostLike(i: PostListing) {
220 let form: CreatePostLikeForm = {
221 post_id: i.props.post.id,
222 score: (i.props.post.my_vote == 1) ? 0 : 1
224 WebSocketService.Instance.likePost(form);
227 handlePostDisLike(i: PostListing) {
228 let form: CreatePostLikeForm = {
229 post_id: i.props.post.id,
230 score: (i.props.post.my_vote == -1) ? 0 : -1
232 WebSocketService.Instance.likePost(form);
235 handleEditClick(i: PostListing) {
236 i.state.showEdit = true;
241 this.state.showEdit = false;
242 this.setState(this.state);
245 // The actual editing is done in the recieve for post
247 this.state.showEdit = false;
248 this.setState(this.state);
251 handleDeleteClick(i: PostListing) {
252 let deleteForm: PostFormI = {
253 body: i.props.post.body,
254 community_id: i.props.post.community_id,
255 name: i.props.post.name,
256 url: i.props.post.url,
257 edit_id: i.props.post.id,
258 creator_id: i.props.post.creator_id,
259 deleted: !i.props.post.deleted,
260 nsfw: i.props.post.nsfw,
263 WebSocketService.Instance.editPost(deleteForm);
266 handleSavePostClick(i: PostListing) {
267 let saved = (i.props.post.saved == undefined) ? true : !i.props.post.saved;
268 let form: SavePostForm = {
269 post_id: i.props.post.id,
273 WebSocketService.Instance.savePost(form);
276 get crossPostParams(): string {
277 let params = `?name=${this.props.post.name}`;
278 if (this.props.post.url) {
279 params += `&url=${this.props.post.url}`;
281 if (this.props.post.body) {
282 params += `&body=${this.props.post.body}`;
287 handleModRemoveShow(i: PostListing) {
288 i.state.showRemoveDialog = true;
292 handleModRemoveReasonChange(i: PostListing, event: any) {
293 i.state.removeReason = event.target.value;
297 handleModRemoveSubmit(i: PostListing) {
298 event.preventDefault();
299 let form: PostFormI = {
300 name: i.props.post.name,
301 community_id: i.props.post.community_id,
302 edit_id: i.props.post.id,
303 creator_id: i.props.post.creator_id,
304 removed: !i.props.post.removed,
305 reason: i.state.removeReason,
306 nsfw: i.props.post.nsfw,
309 WebSocketService.Instance.editPost(form);
311 i.state.showRemoveDialog = false;
315 handleModLock(i: PostListing) {
316 let form: PostFormI = {
317 name: i.props.post.name,
318 community_id: i.props.post.community_id,
319 edit_id: i.props.post.id,
320 creator_id: i.props.post.creator_id,
321 nsfw: i.props.post.nsfw,
322 locked: !i.props.post.locked,
325 WebSocketService.Instance.editPost(form);
328 handleImageExpandClick(i: PostListing) {
329 i.state.imageExpanded = !i.state.imageExpanded;