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;
52 interface PostListingProps {
54 showCommunity?: boolean;
56 moderators?: Array<CommunityUser>;
57 admins?: Array<UserView>;
60 export class PostListing extends Component<PostListingProps, PostListingState> {
61 private emptyState: PostListingState = {
63 showRemoveDialog: false,
68 banType: BanType.Community,
69 showConfirmTransferSite: false,
70 showConfirmTransferCommunity: false,
73 my_vote: this.props.post.my_vote,
74 score: this.props.post.score,
75 upvotes: this.props.post.upvotes,
76 downvotes: this.props.post.downvotes,
79 constructor(props: any, context: any) {
80 super(props, context);
82 this.state = this.emptyState;
83 this.handlePostLike = this.handlePostLike.bind(this);
84 this.handlePostDisLike = this.handlePostDisLike.bind(this);
85 this.handleEditPost = this.handleEditPost.bind(this);
86 this.handleEditCancel = this.handleEditCancel.bind(this);
89 componentWillReceiveProps(nextProps: PostListingProps) {
90 this.state.my_vote = nextProps.post.my_vote;
91 this.state.upvotes = nextProps.post.upvotes;
92 this.state.downvotes = nextProps.post.downvotes;
93 this.state.score = nextProps.post.score;
94 this.setState(this.state);
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={`vote-animate btn btn-link p-0 ${
122 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
124 onClick={linkEvent(this, this.handlePostLike)}
126 <svg class="icon upvote">
127 <use xlinkHref="#icon-arrow-up"></use>
130 <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
131 {WebSocketService.Instance.site.enable_downvotes && (
133 className={`vote-animate btn btn-link p-0 ${
134 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
136 onClick={linkEvent(this, this.handlePostDisLike)}
138 <svg class="icon downvote">
139 <use xlinkHref="#icon-arrow-down"></use>
144 {post.url && isImage(post.url) && !this.state.imageExpanded && (
146 title={i18n.t('expand_here')}
148 onClick={linkEvent(this, this.handleImageExpandClick)}
151 className={`mx-2 mt-1 float-left img-fluid thumbnail rounded ${(post.nsfw ||
152 post.community_nsfw) &&
154 src={imageThumbnailer(post.url)}
158 {post.url && isVideo(post.url) && (
164 class="mx-2 mt-1 float-left"
168 <source src={post.url} type="video/mp4" />
171 <div className="ml-4">
172 <div className="post-title text-wrap-truncate">
173 <h5 className="mb-0 d-inline">
176 className="text-body"
185 className="text-body"
186 to={`/post/${post.id}`}
187 title={i18n.t('comments')}
196 className="ml-2 text-muted font-italic"
201 {new URL(post.url).hostname}
205 {post.url && isImage(post.url) && (
207 {!this.state.imageExpanded ? (
209 class="text-monospace pointer ml-2 text-muted small"
210 title={i18n.t('expand_here')}
211 onClick={linkEvent(this, this.handleImageExpandClick)}
218 class="text-monospace pointer ml-2 text-muted small"
219 onClick={linkEvent(this, this.handleImageExpandClick)}
226 onClick={linkEvent(this, this.handleImageExpandClick)}
228 <img class="img-fluid img-expanded" src={post.url} />
236 <small className="ml-2 text-muted font-italic">
241 <small className="ml-2 text-muted font-italic">
246 <small className="ml-2 text-muted font-italic">
251 <small className="ml-2 text-muted font-italic">
256 <small className="ml-2 text-muted font-italic">
262 <div className="details ml-4">
263 <ul class="list-inline mb-0 text-muted small">
264 <li className="list-inline-item">
265 <span>{i18n.t('by')} </span>
266 <Link className="text-info" to={`/u/${post.creator_name}`}>
267 {post.creator_avatar && showAvatars() && (
271 src={pictshareAvatarThumbnail(post.creator_avatar)}
272 class="rounded-circle mr-1"
275 <span>{post.creator_name}</span>
278 <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
281 <span className="mx-1 badge badge-light">
285 {(post.banned_from_community || post.banned) && (
286 <span className="mx-1 badge badge-danger">
290 {this.props.showCommunity && (
292 <span> {i18n.t('to')} </span>
293 <Link to={`/c/${post.community_name}`}>
294 {post.community_name}
299 <li className="list-inline-item">
301 <MomentTime data={post} />
304 <li className="list-inline-item">
306 (<span className="text-info">+{this.state.upvotes}</span>
308 <span className="text-danger">-{this.state.downvotes}</span>
312 <li className="list-inline-item">
313 <Link className="text-muted" to={`/post/${post.id}`}>
314 {i18n.t('number_of_comments', {
315 count: post.number_of_comments,
320 <ul class="list-inline mb-1 text-muted small">
321 {this.props.post.duplicates && (
323 <li className="list-inline-item mr-2">
324 {i18n.t('cross_posted_to')}
326 {this.props.post.duplicates.map(post => (
327 <li className="list-inline-item mr-2">
328 <Link to={`/post/${post.id}`}>{post.community_name}</Link>
334 <ul class="list-inline mb-1 text-muted small font-weight-bold">
335 {UserService.Instance.user && (
337 {this.props.showBody && (
339 <li className="list-inline-item mr-2">
342 onClick={linkEvent(this, this.handleSavePostClick)}
344 {post.saved ? i18n.t('unsave') : i18n.t('save')}
347 <li className="list-inline-item mr-2">
349 className="text-muted"
350 to={`/create_post${this.crossPostParams}`}
352 {i18n.t('cross_post')}
357 {this.myPost && this.props.showBody && (
359 <li className="list-inline-item">
362 onClick={linkEvent(this, this.handleEditClick)}
367 <li className="list-inline-item mr-2">
370 onClick={linkEvent(this, this.handleDeleteClick)}
372 {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
377 {this.canModOnSelf && (
379 <li className="list-inline-item">
382 onClick={linkEvent(this, this.handleModLock)}
384 {post.locked ? i18n.t('unlock') : i18n.t('lock')}
387 <li className="list-inline-item">
390 onClick={linkEvent(this, this.handleModSticky)}
392 {post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}
397 {/* Mods can ban from community, and appoint as mods to community */}
398 {(this.canMod || this.canAdmin) && (
399 <li className="list-inline-item">
403 onClick={linkEvent(this, this.handleModRemoveShow)}
410 onClick={linkEvent(this, this.handleModRemoveSubmit)}
420 <li className="list-inline-item">
421 {!post.banned_from_community ? (
426 this.handleModBanFromCommunityShow
436 this.handleModBanFromCommunitySubmit
444 {!post.banned_from_community && (
445 <li className="list-inline-item">
450 this.handleAddModToCommunity
454 ? i18n.t('remove_as_mod')
455 : i18n.t('appoint_as_mod')}
461 {/* Community creators and admins can transfer community to another mod */}
462 {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
463 <li className="list-inline-item">
464 {!this.state.showConfirmTransferCommunity ? (
469 this.handleShowConfirmTransferCommunity
472 {i18n.t('transfer_community')}
476 <span class="d-inline-block mr-1">
477 {i18n.t('are_you_sure')}
480 class="pointer d-inline-block mr-1"
483 this.handleTransferCommunity
489 class="pointer d-inline-block"
492 this.handleCancelShowConfirmTransferCommunity
501 {/* Admins can ban from all, and appoint other admins */}
505 <li className="list-inline-item">
509 onClick={linkEvent(this, this.handleModBanShow)}
511 {i18n.t('ban_from_site')}
516 onClick={linkEvent(this, this.handleModBanSubmit)}
518 {i18n.t('unban_from_site')}
524 <li className="list-inline-item">
527 onClick={linkEvent(this, this.handleAddAdmin)}
530 ? i18n.t('remove_as_admin')
531 : i18n.t('appoint_as_admin')}
537 {/* Site Creator can transfer to another admin */}
538 {this.amSiteCreator && this.isAdmin && (
539 <li className="list-inline-item">
540 {!this.state.showConfirmTransferSite ? (
545 this.handleShowConfirmTransferSite
548 {i18n.t('transfer_site')}
552 <span class="d-inline-block mr-1">
553 {i18n.t('are_you_sure')}
556 class="pointer d-inline-block mr-1"
557 onClick={linkEvent(this, this.handleTransferSite)}
562 class="pointer d-inline-block"
565 this.handleCancelShowConfirmTransferSite
576 {this.props.showBody && post.body && (
577 <li className="list-inline-item">
580 onClick={linkEvent(this, this.handleViewSource)}
582 {i18n.t('view_source')}
587 {this.state.showRemoveDialog && (
590 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
594 class="form-control mr-2"
595 placeholder={i18n.t('reason')}
596 value={this.state.removeReason}
597 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
599 <button type="submit" class="btn btn-secondary">
600 {i18n.t('remove_post')}
604 {this.state.showBanDialog && (
605 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
606 <div class="form-group row">
607 <label class="col-form-label" htmlFor="post-listing-reason">
612 id="post-listing-reason"
613 class="form-control mr-2"
614 placeholder={i18n.t('reason')}
615 value={this.state.banReason}
616 onInput={linkEvent(this, this.handleModBanReasonChange)}
619 {/* TODO hold off on expires until later */}
620 {/* <div class="form-group row"> */}
621 {/* <label class="col-form-label">Expires</label> */}
622 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
624 <div class="form-group row">
625 <button type="submit" class="btn btn-secondary">
626 {i18n.t('ban')} {post.creator_name}
631 {this.props.showBody && post.body && (
633 {this.state.viewSource ? (
634 <pre>{post.body}</pre>
638 dangerouslySetInnerHTML={mdToHtml(post.body)}
648 private get myPost(): boolean {
650 UserService.Instance.user &&
651 this.props.post.creator_id == UserService.Instance.user.id
655 get isMod(): boolean {
657 this.props.moderators &&
659 this.props.moderators.map(m => m.user_id),
660 this.props.post.creator_id
665 get isAdmin(): boolean {
669 this.props.admins.map(a => a.id),
670 this.props.post.creator_id
675 get canMod(): boolean {
676 if (this.props.admins && this.props.moderators) {
677 let adminsThenMods = this.props.admins
679 .concat(this.props.moderators.map(m => m.user_id));
682 UserService.Instance.user,
684 this.props.post.creator_id
691 get canModOnSelf(): boolean {
692 if (this.props.admins && this.props.moderators) {
693 let adminsThenMods = this.props.admins
695 .concat(this.props.moderators.map(m => m.user_id));
698 UserService.Instance.user,
700 this.props.post.creator_id,
708 get canAdmin(): boolean {
712 UserService.Instance.user,
713 this.props.admins.map(a => a.id),
714 this.props.post.creator_id
719 get amCommunityCreator(): boolean {
721 this.props.moderators &&
722 UserService.Instance.user &&
723 this.props.post.creator_id != UserService.Instance.user.id &&
724 UserService.Instance.user.id == this.props.moderators[0].user_id
728 get amSiteCreator(): boolean {
731 UserService.Instance.user &&
732 this.props.post.creator_id != UserService.Instance.user.id &&
733 UserService.Instance.user.id == this.props.admins[0].id
737 handlePostLike(i: PostListing) {
738 let new_vote = i.state.my_vote == 1 ? 0 : 1;
740 if (i.state.my_vote == 1) {
743 } else if (i.state.my_vote == -1) {
752 i.state.my_vote = new_vote;
754 let form: CreatePostLikeForm = {
755 post_id: i.props.post.id,
756 score: i.state.my_vote,
759 WebSocketService.Instance.likePost(form);
763 handlePostDisLike(i: PostListing) {
764 let new_vote = i.state.my_vote == -1 ? 0 : -1;
766 if (i.state.my_vote == 1) {
770 } else if (i.state.my_vote == -1) {
778 i.state.my_vote = new_vote;
780 let form: CreatePostLikeForm = {
781 post_id: i.props.post.id,
782 score: i.state.my_vote,
785 WebSocketService.Instance.likePost(form);
789 handleEditClick(i: PostListing) {
790 i.state.showEdit = true;
795 this.state.showEdit = false;
796 this.setState(this.state);
799 // The actual editing is done in the recieve for post
801 this.state.showEdit = false;
802 this.setState(this.state);
805 handleDeleteClick(i: PostListing) {
806 let deleteForm: PostFormI = {
807 body: i.props.post.body,
808 community_id: i.props.post.community_id,
809 name: i.props.post.name,
810 url: i.props.post.url,
811 edit_id: i.props.post.id,
812 creator_id: i.props.post.creator_id,
813 deleted: !i.props.post.deleted,
814 nsfw: i.props.post.nsfw,
817 WebSocketService.Instance.editPost(deleteForm);
820 handleSavePostClick(i: PostListing) {
821 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
822 let form: SavePostForm = {
823 post_id: i.props.post.id,
827 WebSocketService.Instance.savePost(form);
830 get crossPostParams(): string {
831 let params = `?title=${this.props.post.name}`;
832 if (this.props.post.url) {
833 params += `&url=${this.props.post.url}`;
835 if (this.props.post.body) {
836 params += `&body=${this.props.post.body}`;
841 handleModRemoveShow(i: PostListing) {
842 i.state.showRemoveDialog = true;
846 handleModRemoveReasonChange(i: PostListing, event: any) {
847 i.state.removeReason = event.target.value;
851 handleModRemoveSubmit(i: PostListing) {
852 event.preventDefault();
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 removed: !i.props.post.removed,
859 reason: i.state.removeReason,
860 nsfw: i.props.post.nsfw,
863 WebSocketService.Instance.editPost(form);
865 i.state.showRemoveDialog = false;
869 handleModLock(i: PostListing) {
870 let form: PostFormI = {
871 name: i.props.post.name,
872 community_id: i.props.post.community_id,
873 edit_id: i.props.post.id,
874 creator_id: i.props.post.creator_id,
875 nsfw: i.props.post.nsfw,
876 locked: !i.props.post.locked,
879 WebSocketService.Instance.editPost(form);
882 handleModSticky(i: PostListing) {
883 let form: PostFormI = {
884 name: i.props.post.name,
885 community_id: i.props.post.community_id,
886 edit_id: i.props.post.id,
887 creator_id: i.props.post.creator_id,
888 nsfw: i.props.post.nsfw,
889 stickied: !i.props.post.stickied,
892 WebSocketService.Instance.editPost(form);
895 handleModBanFromCommunityShow(i: PostListing) {
896 i.state.showBanDialog = true;
897 i.state.banType = BanType.Community;
901 handleModBanShow(i: PostListing) {
902 i.state.showBanDialog = true;
903 i.state.banType = BanType.Site;
907 handleModBanReasonChange(i: PostListing, event: any) {
908 i.state.banReason = event.target.value;
912 handleModBanExpiresChange(i: PostListing, event: any) {
913 i.state.banExpires = event.target.value;
917 handleModBanFromCommunitySubmit(i: PostListing) {
918 i.state.banType = BanType.Community;
920 i.handleModBanBothSubmit(i);
923 handleModBanSubmit(i: PostListing) {
924 i.state.banType = BanType.Site;
926 i.handleModBanBothSubmit(i);
929 handleModBanBothSubmit(i: PostListing) {
930 event.preventDefault();
932 if (i.state.banType == BanType.Community) {
933 let form: BanFromCommunityForm = {
934 user_id: i.props.post.creator_id,
935 community_id: i.props.post.community_id,
936 ban: !i.props.post.banned_from_community,
937 reason: i.state.banReason,
938 expires: getUnixTime(i.state.banExpires),
940 WebSocketService.Instance.banFromCommunity(form);
942 let form: BanUserForm = {
943 user_id: i.props.post.creator_id,
944 ban: !i.props.post.banned,
945 reason: i.state.banReason,
946 expires: getUnixTime(i.state.banExpires),
948 WebSocketService.Instance.banUser(form);
951 i.state.showBanDialog = false;
955 handleAddModToCommunity(i: PostListing) {
956 let form: AddModToCommunityForm = {
957 user_id: i.props.post.creator_id,
958 community_id: i.props.post.community_id,
961 WebSocketService.Instance.addModToCommunity(form);
965 handleAddAdmin(i: PostListing) {
966 let form: AddAdminForm = {
967 user_id: i.props.post.creator_id,
970 WebSocketService.Instance.addAdmin(form);
974 handleShowConfirmTransferCommunity(i: PostListing) {
975 i.state.showConfirmTransferCommunity = true;
979 handleCancelShowConfirmTransferCommunity(i: PostListing) {
980 i.state.showConfirmTransferCommunity = false;
984 handleTransferCommunity(i: PostListing) {
985 let form: TransferCommunityForm = {
986 community_id: i.props.post.community_id,
987 user_id: i.props.post.creator_id,
989 WebSocketService.Instance.transferCommunity(form);
990 i.state.showConfirmTransferCommunity = false;
994 handleShowConfirmTransferSite(i: PostListing) {
995 i.state.showConfirmTransferSite = true;
999 handleCancelShowConfirmTransferSite(i: PostListing) {
1000 i.state.showConfirmTransferSite = false;
1001 i.setState(i.state);
1004 handleTransferSite(i: PostListing) {
1005 let form: TransferSiteForm = {
1006 user_id: i.props.post.creator_id,
1008 WebSocketService.Instance.transferSite(form);
1009 i.state.showConfirmTransferSite = false;
1010 i.setState(i.state);
1013 handleImageExpandClick(i: PostListing) {
1014 i.state.imageExpanded = !i.state.imageExpanded;
1015 i.setState(i.state);
1018 handleViewSource(i: PostListing) {
1019 i.state.viewSource = !i.state.viewSource;
1020 i.setState(i.state);