1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { WebSocketService, UserService } from '../services';
14 AddModToCommunityForm,
17 TransferCommunityForm,
18 } from '../interfaces';
19 import { MomentTime } from './moment-time';
20 import { PostForm } from './post-form';
28 pictshareAvatarThumbnail,
32 import { i18n } from '../i18next';
34 interface PostListingState {
36 showRemoveDialog: boolean;
38 showBanDialog: boolean;
42 showConfirmTransferSite: boolean;
43 showConfirmTransferCommunity: boolean;
44 imageExpanded: boolean;
46 upvoteLoading: boolean;
47 downvoteLoading: boolean;
50 interface PostListingProps {
52 showCommunity?: boolean;
54 moderators?: Array<CommunityUser>;
55 admins?: Array<UserView>;
58 export class PostListing extends Component<PostListingProps, PostListingState> {
59 private emptyState: PostListingState = {
61 showRemoveDialog: false,
66 banType: BanType.Community,
67 showConfirmTransferSite: false,
68 showConfirmTransferCommunity: false,
71 upvoteLoading: this.props.post.upvoteLoading,
72 downvoteLoading: this.props.post.downvoteLoading,
75 constructor(props: any, context: any) {
76 super(props, context);
78 this.state = this.emptyState;
79 this.handlePostLike = this.handlePostLike.bind(this);
80 this.handlePostDisLike = this.handlePostDisLike.bind(this);
81 this.handleEditPost = this.handleEditPost.bind(this);
82 this.handleEditCancel = this.handleEditCancel.bind(this);
85 componentWillReceiveProps(nextProps: PostListingProps) {
87 nextProps.post.upvoteLoading !== this.state.upvoteLoading ||
88 nextProps.post.downvoteLoading !== this.state.downvoteLoading
92 downvoteLoading: false,
100 {!this.state.showEdit ? (
105 post={this.props.post}
106 onEdit={this.handleEditPost}
107 onCancel={this.handleEditCancel}
116 let post = this.props.post;
118 <div class="listing col-12">
119 <div className={`vote-bar mr-2 float-left small text-center`}>
121 className={`btn btn-link p-0 ${
122 post.my_vote == 1 ? 'text-info' : 'text-muted'
124 onClick={linkEvent(this, this.handlePostLike)}
126 {this.state.upvoteLoading ? (
127 <svg class="icon icon-spinner spin upvote">
128 <use xlinkHref="#icon-spinner"></use>
131 <svg class="icon upvote">
132 <use xlinkHref="#icon-arrow-up"></use>
136 <div class={`font-weight-bold text-muted`}>{post.score}</div>
137 {WebSocketService.Instance.site.enable_downvotes && (
139 className={`btn btn-link p-0 ${
140 post.my_vote == -1 ? 'text-danger' : 'text-muted'
142 onClick={linkEvent(this, this.handlePostDisLike)}
144 {this.state.downvoteLoading ? (
145 <svg class="icon icon-spinner spin downvote">
146 <use xlinkHref="#icon-spinner"></use>
149 <svg class="icon downvote">
150 <use xlinkHref="#icon-arrow-down"></use>
156 {post.url && isImage(post.url) && !this.state.imageExpanded && (
158 title={i18n.t('expand_here')}
160 onClick={linkEvent(this, this.handleImageExpandClick)}
163 className={`mx-2 mt-1 float-left img-fluid thumbnail rounded ${(post.nsfw ||
164 post.community_nsfw) &&
166 src={imageThumbnailer(post.url)}
170 {post.url && isVideo(post.url) && (
176 class="mx-2 mt-1 float-left"
180 <source src={post.url} type="video/mp4" />
183 <div className="ml-4">
184 <div className="post-title text-wrap-truncate">
185 <h5 className="mb-0 d-inline">
188 className="text-body"
197 className="text-body"
198 to={`/post/${post.id}`}
199 title={i18n.t('comments')}
208 className="ml-2 text-muted font-italic"
213 {new URL(post.url).hostname}
217 {post.url && isImage(post.url) && (
219 {!this.state.imageExpanded ? (
221 class="text-monospace pointer ml-2 text-muted small"
222 title={i18n.t('expand_here')}
223 onClick={linkEvent(this, this.handleImageExpandClick)}
230 class="text-monospace pointer ml-2 text-muted small"
231 onClick={linkEvent(this, this.handleImageExpandClick)}
238 onClick={linkEvent(this, this.handleImageExpandClick)}
240 <img class="img-fluid" src={post.url} />
248 <small className="ml-2 text-muted font-italic">
253 <small className="ml-2 text-muted font-italic">
258 <small className="ml-2 text-muted font-italic">
263 <small className="ml-2 text-muted font-italic">
268 <small className="ml-2 text-muted font-italic">
274 <div className="details ml-4">
275 <ul class="list-inline mb-0 text-muted small">
276 <li className="list-inline-item">
277 <span>{i18n.t('by')} </span>
278 <Link className="text-info" to={`/u/${post.creator_name}`}>
279 {post.creator_avatar && showAvatars() && (
283 src={pictshareAvatarThumbnail(post.creator_avatar)}
284 class="rounded-circle mr-1"
287 <span>{post.creator_name}</span>
290 <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
293 <span className="mx-1 badge badge-light">
297 {(post.banned_from_community || post.banned) && (
298 <span className="mx-1 badge badge-danger">
302 {this.props.showCommunity && (
304 <span> {i18n.t('to')} </span>
305 <Link to={`/c/${post.community_name}`}>
306 {post.community_name}
311 <li className="list-inline-item">
313 <MomentTime data={post} />
316 <li className="list-inline-item">
318 (<span className="text-info">+{post.upvotes}</span>
320 <span className="text-danger">-{post.downvotes}</span>
324 <li className="list-inline-item">
325 <Link className="text-muted" to={`/post/${post.id}`}>
326 {i18n.t('number_of_comments', {
327 count: post.number_of_comments,
332 <ul class="list-inline mb-1 text-muted small">
333 {this.props.post.duplicates && (
335 <li className="list-inline-item mr-2">cross-posted to:</li>
336 {this.props.post.duplicates.map(post => (
337 <li className="list-inline-item mr-2">
338 <Link to={`/post/${post.id}`}>{post.community_name}</Link>
344 <ul class="list-inline mb-1 text-muted small font-weight-bold">
345 {UserService.Instance.user && (
347 {this.props.showBody && (
349 <li className="list-inline-item mr-2">
352 onClick={linkEvent(this, this.handleSavePostClick)}
354 {post.saved ? i18n.t('unsave') : i18n.t('save')}
357 <li className="list-inline-item mr-2">
359 className="text-muted"
360 to={`/create_post${this.crossPostParams}`}
362 {i18n.t('cross_post')}
367 {this.myPost && this.props.showBody && (
369 <li className="list-inline-item">
372 onClick={linkEvent(this, this.handleEditClick)}
377 <li className="list-inline-item mr-2">
380 onClick={linkEvent(this, this.handleDeleteClick)}
382 {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
387 {this.canModOnSelf && (
389 <li className="list-inline-item">
392 onClick={linkEvent(this, this.handleModLock)}
394 {post.locked ? i18n.t('unlock') : i18n.t('lock')}
397 <li className="list-inline-item">
400 onClick={linkEvent(this, this.handleModSticky)}
402 {post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}
407 {/* Mods can ban from community, and appoint as mods to community */}
408 {(this.canMod || this.canAdmin) && (
409 <li className="list-inline-item">
413 onClick={linkEvent(this, this.handleModRemoveShow)}
420 onClick={linkEvent(this, this.handleModRemoveSubmit)}
430 <li className="list-inline-item">
431 {!post.banned_from_community ? (
436 this.handleModBanFromCommunityShow
446 this.handleModBanFromCommunitySubmit
454 {!post.banned_from_community && (
455 <li className="list-inline-item">
460 this.handleAddModToCommunity
464 ? i18n.t('remove_as_mod')
465 : i18n.t('appoint_as_mod')}
471 {/* Community creators and admins can transfer community to another mod */}
472 {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
473 <li className="list-inline-item">
474 {!this.state.showConfirmTransferCommunity ? (
479 this.handleShowConfirmTransferCommunity
482 {i18n.t('transfer_community')}
486 <span class="d-inline-block mr-1">
487 {i18n.t('are_you_sure')}
490 class="pointer d-inline-block mr-1"
493 this.handleTransferCommunity
499 class="pointer d-inline-block"
502 this.handleCancelShowConfirmTransferCommunity
511 {/* Admins can ban from all, and appoint other admins */}
515 <li className="list-inline-item">
519 onClick={linkEvent(this, this.handleModBanShow)}
521 {i18n.t('ban_from_site')}
526 onClick={linkEvent(this, this.handleModBanSubmit)}
528 {i18n.t('unban_from_site')}
534 <li className="list-inline-item">
537 onClick={linkEvent(this, this.handleAddAdmin)}
540 ? i18n.t('remove_as_admin')
541 : i18n.t('appoint_as_admin')}
547 {/* Site Creator can transfer to another admin */}
548 {this.amSiteCreator && this.isAdmin && (
549 <li className="list-inline-item">
550 {!this.state.showConfirmTransferSite ? (
555 this.handleShowConfirmTransferSite
558 {i18n.t('transfer_site')}
562 <span class="d-inline-block mr-1">
563 {i18n.t('are_you_sure')}
566 class="pointer d-inline-block mr-1"
567 onClick={linkEvent(this, this.handleTransferSite)}
572 class="pointer d-inline-block"
575 this.handleCancelShowConfirmTransferSite
586 {this.props.showBody && post.body && (
587 <li className="list-inline-item">
590 onClick={linkEvent(this, this.handleViewSource)}
592 {i18n.t('view_source')}
597 {this.state.showRemoveDialog && (
600 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
604 class="form-control mr-2"
605 placeholder={i18n.t('reason')}
606 value={this.state.removeReason}
607 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
609 <button type="submit" class="btn btn-secondary">
610 {i18n.t('remove_post')}
614 {this.state.showBanDialog && (
615 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
616 <div class="form-group row">
617 <label class="col-form-label" htmlFor="post-listing-reason">
622 id="post-listing-reason"
623 class="form-control mr-2"
624 placeholder={i18n.t('reason')}
625 value={this.state.banReason}
626 onInput={linkEvent(this, this.handleModBanReasonChange)}
629 {/* TODO hold off on expires until later */}
630 {/* <div class="form-group row"> */}
631 {/* <label class="col-form-label">Expires</label> */}
632 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
634 <div class="form-group row">
635 <button type="submit" class="btn btn-secondary">
636 {i18n.t('ban')} {post.creator_name}
641 {this.props.showBody && post.body && (
643 {this.state.viewSource ? (
644 <pre>{post.body}</pre>
648 dangerouslySetInnerHTML={mdToHtml(post.body)}
658 private get myPost(): boolean {
660 UserService.Instance.user &&
661 this.props.post.creator_id == UserService.Instance.user.id
665 get isMod(): boolean {
667 this.props.moderators &&
669 this.props.moderators.map(m => m.user_id),
670 this.props.post.creator_id
675 get isAdmin(): boolean {
679 this.props.admins.map(a => a.id),
680 this.props.post.creator_id
685 get canMod(): boolean {
686 if (this.props.admins && this.props.moderators) {
687 let adminsThenMods = this.props.admins
689 .concat(this.props.moderators.map(m => m.user_id));
692 UserService.Instance.user,
694 this.props.post.creator_id
701 get canModOnSelf(): boolean {
702 if (this.props.admins && this.props.moderators) {
703 let adminsThenMods = this.props.admins
705 .concat(this.props.moderators.map(m => m.user_id));
708 UserService.Instance.user,
710 this.props.post.creator_id,
718 get canAdmin(): boolean {
722 UserService.Instance.user,
723 this.props.admins.map(a => a.id),
724 this.props.post.creator_id
729 get amCommunityCreator(): boolean {
731 this.props.moderators &&
732 UserService.Instance.user &&
733 this.props.post.creator_id != UserService.Instance.user.id &&
734 UserService.Instance.user.id == this.props.moderators[0].user_id
738 get amSiteCreator(): boolean {
741 UserService.Instance.user &&
742 this.props.post.creator_id != UserService.Instance.user.id &&
743 UserService.Instance.user.id == this.props.admins[0].id
747 handlePostLike(i: PostListing) {
748 if (UserService.Instance.user) {
749 i.setState({ upvoteLoading: true });
752 let form: CreatePostLikeForm = {
753 post_id: i.props.post.id,
754 score: i.props.post.my_vote == 1 ? 0 : 1,
757 WebSocketService.Instance.likePost(form);
760 handlePostDisLike(i: PostListing) {
761 if (UserService.Instance.user) {
762 i.setState({ downvoteLoading: true });
765 let form: CreatePostLikeForm = {
766 post_id: i.props.post.id,
767 score: i.props.post.my_vote == -1 ? 0 : -1,
769 WebSocketService.Instance.likePost(form);
772 handleEditClick(i: PostListing) {
773 i.state.showEdit = true;
778 this.state.showEdit = false;
779 this.setState(this.state);
782 // The actual editing is done in the recieve for post
784 this.state.showEdit = false;
785 this.setState(this.state);
788 handleDeleteClick(i: PostListing) {
789 let deleteForm: PostFormI = {
790 body: i.props.post.body,
791 community_id: i.props.post.community_id,
792 name: i.props.post.name,
793 url: i.props.post.url,
794 edit_id: i.props.post.id,
795 creator_id: i.props.post.creator_id,
796 deleted: !i.props.post.deleted,
797 nsfw: i.props.post.nsfw,
800 WebSocketService.Instance.editPost(deleteForm);
803 handleSavePostClick(i: PostListing) {
804 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
805 let form: SavePostForm = {
806 post_id: i.props.post.id,
810 WebSocketService.Instance.savePost(form);
813 get crossPostParams(): string {
814 let params = `?title=${this.props.post.name}`;
815 if (this.props.post.url) {
816 params += `&url=${this.props.post.url}`;
818 if (this.props.post.body) {
819 params += `&body=${this.props.post.body}`;
824 handleModRemoveShow(i: PostListing) {
825 i.state.showRemoveDialog = true;
829 handleModRemoveReasonChange(i: PostListing, event: any) {
830 i.state.removeReason = event.target.value;
834 handleModRemoveSubmit(i: PostListing) {
835 event.preventDefault();
836 let form: PostFormI = {
837 name: i.props.post.name,
838 community_id: i.props.post.community_id,
839 edit_id: i.props.post.id,
840 creator_id: i.props.post.creator_id,
841 removed: !i.props.post.removed,
842 reason: i.state.removeReason,
843 nsfw: i.props.post.nsfw,
846 WebSocketService.Instance.editPost(form);
848 i.state.showRemoveDialog = false;
852 handleModLock(i: PostListing) {
853 let form: PostFormI = {
854 name: i.props.post.name,
855 community_id: i.props.post.community_id,
856 edit_id: i.props.post.id,
857 creator_id: i.props.post.creator_id,
858 nsfw: i.props.post.nsfw,
859 locked: !i.props.post.locked,
862 WebSocketService.Instance.editPost(form);
865 handleModSticky(i: PostListing) {
866 let form: PostFormI = {
867 name: i.props.post.name,
868 community_id: i.props.post.community_id,
869 edit_id: i.props.post.id,
870 creator_id: i.props.post.creator_id,
871 nsfw: i.props.post.nsfw,
872 stickied: !i.props.post.stickied,
875 WebSocketService.Instance.editPost(form);
878 handleModBanFromCommunityShow(i: PostListing) {
879 i.state.showBanDialog = true;
880 i.state.banType = BanType.Community;
884 handleModBanShow(i: PostListing) {
885 i.state.showBanDialog = true;
886 i.state.banType = BanType.Site;
890 handleModBanReasonChange(i: PostListing, event: any) {
891 i.state.banReason = event.target.value;
895 handleModBanExpiresChange(i: PostListing, event: any) {
896 i.state.banExpires = event.target.value;
900 handleModBanFromCommunitySubmit(i: PostListing) {
901 i.state.banType = BanType.Community;
903 i.handleModBanBothSubmit(i);
906 handleModBanSubmit(i: PostListing) {
907 i.state.banType = BanType.Site;
909 i.handleModBanBothSubmit(i);
912 handleModBanBothSubmit(i: PostListing) {
913 event.preventDefault();
915 if (i.state.banType == BanType.Community) {
916 let form: BanFromCommunityForm = {
917 user_id: i.props.post.creator_id,
918 community_id: i.props.post.community_id,
919 ban: !i.props.post.banned_from_community,
920 reason: i.state.banReason,
921 expires: getUnixTime(i.state.banExpires),
923 WebSocketService.Instance.banFromCommunity(form);
925 let form: BanUserForm = {
926 user_id: i.props.post.creator_id,
927 ban: !i.props.post.banned,
928 reason: i.state.banReason,
929 expires: getUnixTime(i.state.banExpires),
931 WebSocketService.Instance.banUser(form);
934 i.state.showBanDialog = false;
938 handleAddModToCommunity(i: PostListing) {
939 let form: AddModToCommunityForm = {
940 user_id: i.props.post.creator_id,
941 community_id: i.props.post.community_id,
944 WebSocketService.Instance.addModToCommunity(form);
948 handleAddAdmin(i: PostListing) {
949 let form: AddAdminForm = {
950 user_id: i.props.post.creator_id,
953 WebSocketService.Instance.addAdmin(form);
957 handleShowConfirmTransferCommunity(i: PostListing) {
958 i.state.showConfirmTransferCommunity = true;
962 handleCancelShowConfirmTransferCommunity(i: PostListing) {
963 i.state.showConfirmTransferCommunity = false;
967 handleTransferCommunity(i: PostListing) {
968 let form: TransferCommunityForm = {
969 community_id: i.props.post.community_id,
970 user_id: i.props.post.creator_id,
972 WebSocketService.Instance.transferCommunity(form);
973 i.state.showConfirmTransferCommunity = false;
977 handleShowConfirmTransferSite(i: PostListing) {
978 i.state.showConfirmTransferSite = true;
982 handleCancelShowConfirmTransferSite(i: PostListing) {
983 i.state.showConfirmTransferSite = false;
987 handleTransferSite(i: PostListing) {
988 let form: TransferSiteForm = {
989 user_id: i.props.post.creator_id,
991 WebSocketService.Instance.transferSite(form);
992 i.state.showConfirmTransferSite = false;
996 handleImageExpandClick(i: PostListing) {
997 i.state.imageExpanded = !i.state.imageExpanded;
1001 handleViewSource(i: PostListing) {
1002 i.state.viewSource = !i.state.viewSource;
1003 i.setState(i.state);