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 font-weight-bold">
333 {UserService.Instance.user && (
335 {this.props.showBody && (
337 <li className="list-inline-item mr-2">
340 onClick={linkEvent(this, this.handleSavePostClick)}
342 {post.saved ? i18n.t('unsave') : i18n.t('save')}
345 <li className="list-inline-item mr-2">
347 className="text-muted"
348 to={`/create_post${this.crossPostParams}`}
350 {i18n.t('cross_post')}
355 {this.myPost && this.props.showBody && (
357 <li className="list-inline-item">
360 onClick={linkEvent(this, this.handleEditClick)}
365 <li className="list-inline-item mr-2">
368 onClick={linkEvent(this, this.handleDeleteClick)}
370 {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
375 {this.canModOnSelf && (
377 <li className="list-inline-item">
380 onClick={linkEvent(this, this.handleModLock)}
382 {post.locked ? i18n.t('unlock') : i18n.t('lock')}
385 <li className="list-inline-item">
388 onClick={linkEvent(this, this.handleModSticky)}
390 {post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}
395 {/* Mods can ban from community, and appoint as mods to community */}
396 {(this.canMod || this.canAdmin) && (
397 <li className="list-inline-item">
401 onClick={linkEvent(this, this.handleModRemoveShow)}
408 onClick={linkEvent(this, this.handleModRemoveSubmit)}
418 <li className="list-inline-item">
419 {!post.banned_from_community ? (
424 this.handleModBanFromCommunityShow
434 this.handleModBanFromCommunitySubmit
442 {!post.banned_from_community && (
443 <li className="list-inline-item">
448 this.handleAddModToCommunity
452 ? i18n.t('remove_as_mod')
453 : i18n.t('appoint_as_mod')}
459 {/* Community creators and admins can transfer community to another mod */}
460 {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
461 <li className="list-inline-item">
462 {!this.state.showConfirmTransferCommunity ? (
467 this.handleShowConfirmTransferCommunity
470 {i18n.t('transfer_community')}
474 <span class="d-inline-block mr-1">
475 {i18n.t('are_you_sure')}
478 class="pointer d-inline-block mr-1"
481 this.handleTransferCommunity
487 class="pointer d-inline-block"
490 this.handleCancelShowConfirmTransferCommunity
499 {/* Admins can ban from all, and appoint other admins */}
503 <li className="list-inline-item">
507 onClick={linkEvent(this, this.handleModBanShow)}
509 {i18n.t('ban_from_site')}
514 onClick={linkEvent(this, this.handleModBanSubmit)}
516 {i18n.t('unban_from_site')}
522 <li className="list-inline-item">
525 onClick={linkEvent(this, this.handleAddAdmin)}
528 ? i18n.t('remove_as_admin')
529 : i18n.t('appoint_as_admin')}
535 {/* Site Creator can transfer to another admin */}
536 {this.amSiteCreator && this.isAdmin && (
537 <li className="list-inline-item">
538 {!this.state.showConfirmTransferSite ? (
543 this.handleShowConfirmTransferSite
546 {i18n.t('transfer_site')}
550 <span class="d-inline-block mr-1">
551 {i18n.t('are_you_sure')}
554 class="pointer d-inline-block mr-1"
555 onClick={linkEvent(this, this.handleTransferSite)}
560 class="pointer d-inline-block"
563 this.handleCancelShowConfirmTransferSite
574 {this.props.showBody && post.body && (
575 <li className="list-inline-item">
578 onClick={linkEvent(this, this.handleViewSource)}
580 {i18n.t('view_source')}
585 {this.state.showRemoveDialog && (
588 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
592 class="form-control mr-2"
593 placeholder={i18n.t('reason')}
594 value={this.state.removeReason}
595 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
597 <button type="submit" class="btn btn-secondary">
598 {i18n.t('remove_post')}
602 {this.state.showBanDialog && (
603 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
604 <div class="form-group row">
605 <label class="col-form-label" htmlFor="post-listing-reason">
610 id="post-listing-reason"
611 class="form-control mr-2"
612 placeholder={i18n.t('reason')}
613 value={this.state.banReason}
614 onInput={linkEvent(this, this.handleModBanReasonChange)}
617 {/* TODO hold off on expires until later */}
618 {/* <div class="form-group row"> */}
619 {/* <label class="col-form-label">Expires</label> */}
620 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
622 <div class="form-group row">
623 <button type="submit" class="btn btn-secondary">
624 {i18n.t('ban')} {post.creator_name}
629 {this.props.showBody && post.body && (
631 {this.state.viewSource ? (
632 <pre>{post.body}</pre>
636 dangerouslySetInnerHTML={mdToHtml(post.body)}
646 private get myPost(): boolean {
648 UserService.Instance.user &&
649 this.props.post.creator_id == UserService.Instance.user.id
653 get isMod(): boolean {
655 this.props.moderators &&
657 this.props.moderators.map(m => m.user_id),
658 this.props.post.creator_id
663 get isAdmin(): boolean {
667 this.props.admins.map(a => a.id),
668 this.props.post.creator_id
673 get canMod(): boolean {
674 if (this.props.admins && this.props.moderators) {
675 let adminsThenMods = this.props.admins
677 .concat(this.props.moderators.map(m => m.user_id));
680 UserService.Instance.user,
682 this.props.post.creator_id
689 get canModOnSelf(): boolean {
690 if (this.props.admins && this.props.moderators) {
691 let adminsThenMods = this.props.admins
693 .concat(this.props.moderators.map(m => m.user_id));
696 UserService.Instance.user,
698 this.props.post.creator_id,
706 get canAdmin(): boolean {
710 UserService.Instance.user,
711 this.props.admins.map(a => a.id),
712 this.props.post.creator_id
717 get amCommunityCreator(): boolean {
719 this.props.moderators &&
720 UserService.Instance.user &&
721 this.props.post.creator_id != UserService.Instance.user.id &&
722 UserService.Instance.user.id == this.props.moderators[0].user_id
726 get amSiteCreator(): boolean {
729 UserService.Instance.user &&
730 this.props.post.creator_id != UserService.Instance.user.id &&
731 UserService.Instance.user.id == this.props.admins[0].id
735 handlePostLike(i: PostListing) {
736 if (UserService.Instance.user) {
737 i.setState({ upvoteLoading: true });
740 let form: CreatePostLikeForm = {
741 post_id: i.props.post.id,
742 score: i.props.post.my_vote == 1 ? 0 : 1,
745 WebSocketService.Instance.likePost(form);
748 handlePostDisLike(i: PostListing) {
749 if (UserService.Instance.user) {
750 i.setState({ downvoteLoading: true });
753 let form: CreatePostLikeForm = {
754 post_id: i.props.post.id,
755 score: i.props.post.my_vote == -1 ? 0 : -1,
757 WebSocketService.Instance.likePost(form);
760 handleEditClick(i: PostListing) {
761 i.state.showEdit = true;
766 this.state.showEdit = false;
767 this.setState(this.state);
770 // The actual editing is done in the recieve for post
772 this.state.showEdit = false;
773 this.setState(this.state);
776 handleDeleteClick(i: PostListing) {
777 let deleteForm: PostFormI = {
778 body: i.props.post.body,
779 community_id: i.props.post.community_id,
780 name: i.props.post.name,
781 url: i.props.post.url,
782 edit_id: i.props.post.id,
783 creator_id: i.props.post.creator_id,
784 deleted: !i.props.post.deleted,
785 nsfw: i.props.post.nsfw,
788 WebSocketService.Instance.editPost(deleteForm);
791 handleSavePostClick(i: PostListing) {
792 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
793 let form: SavePostForm = {
794 post_id: i.props.post.id,
798 WebSocketService.Instance.savePost(form);
801 get crossPostParams(): string {
802 let params = `?title=${this.props.post.name}`;
803 if (this.props.post.url) {
804 params += `&url=${this.props.post.url}`;
806 if (this.props.post.body) {
807 params += `&body=${this.props.post.body}`;
812 handleModRemoveShow(i: PostListing) {
813 i.state.showRemoveDialog = true;
817 handleModRemoveReasonChange(i: PostListing, event: any) {
818 i.state.removeReason = event.target.value;
822 handleModRemoveSubmit(i: PostListing) {
823 event.preventDefault();
824 let form: PostFormI = {
825 name: i.props.post.name,
826 community_id: i.props.post.community_id,
827 edit_id: i.props.post.id,
828 creator_id: i.props.post.creator_id,
829 removed: !i.props.post.removed,
830 reason: i.state.removeReason,
831 nsfw: i.props.post.nsfw,
834 WebSocketService.Instance.editPost(form);
836 i.state.showRemoveDialog = false;
840 handleModLock(i: PostListing) {
841 let form: PostFormI = {
842 name: i.props.post.name,
843 community_id: i.props.post.community_id,
844 edit_id: i.props.post.id,
845 creator_id: i.props.post.creator_id,
846 nsfw: i.props.post.nsfw,
847 locked: !i.props.post.locked,
850 WebSocketService.Instance.editPost(form);
853 handleModSticky(i: PostListing) {
854 let form: PostFormI = {
855 name: i.props.post.name,
856 community_id: i.props.post.community_id,
857 edit_id: i.props.post.id,
858 creator_id: i.props.post.creator_id,
859 nsfw: i.props.post.nsfw,
860 stickied: !i.props.post.stickied,
863 WebSocketService.Instance.editPost(form);
866 handleModBanFromCommunityShow(i: PostListing) {
867 i.state.showBanDialog = true;
868 i.state.banType = BanType.Community;
872 handleModBanShow(i: PostListing) {
873 i.state.showBanDialog = true;
874 i.state.banType = BanType.Site;
878 handleModBanReasonChange(i: PostListing, event: any) {
879 i.state.banReason = event.target.value;
883 handleModBanExpiresChange(i: PostListing, event: any) {
884 i.state.banExpires = event.target.value;
888 handleModBanFromCommunitySubmit(i: PostListing) {
889 i.state.banType = BanType.Community;
891 i.handleModBanBothSubmit(i);
894 handleModBanSubmit(i: PostListing) {
895 i.state.banType = BanType.Site;
897 i.handleModBanBothSubmit(i);
900 handleModBanBothSubmit(i: PostListing) {
901 event.preventDefault();
903 if (i.state.banType == BanType.Community) {
904 let form: BanFromCommunityForm = {
905 user_id: i.props.post.creator_id,
906 community_id: i.props.post.community_id,
907 ban: !i.props.post.banned_from_community,
908 reason: i.state.banReason,
909 expires: getUnixTime(i.state.banExpires),
911 WebSocketService.Instance.banFromCommunity(form);
913 let form: BanUserForm = {
914 user_id: i.props.post.creator_id,
915 ban: !i.props.post.banned,
916 reason: i.state.banReason,
917 expires: getUnixTime(i.state.banExpires),
919 WebSocketService.Instance.banUser(form);
922 i.state.showBanDialog = false;
926 handleAddModToCommunity(i: PostListing) {
927 let form: AddModToCommunityForm = {
928 user_id: i.props.post.creator_id,
929 community_id: i.props.post.community_id,
932 WebSocketService.Instance.addModToCommunity(form);
936 handleAddAdmin(i: PostListing) {
937 let form: AddAdminForm = {
938 user_id: i.props.post.creator_id,
941 WebSocketService.Instance.addAdmin(form);
945 handleShowConfirmTransferCommunity(i: PostListing) {
946 i.state.showConfirmTransferCommunity = true;
950 handleCancelShowConfirmTransferCommunity(i: PostListing) {
951 i.state.showConfirmTransferCommunity = false;
955 handleTransferCommunity(i: PostListing) {
956 let form: TransferCommunityForm = {
957 community_id: i.props.post.community_id,
958 user_id: i.props.post.creator_id,
960 WebSocketService.Instance.transferCommunity(form);
961 i.state.showConfirmTransferCommunity = false;
965 handleShowConfirmTransferSite(i: PostListing) {
966 i.state.showConfirmTransferSite = true;
970 handleCancelShowConfirmTransferSite(i: PostListing) {
971 i.state.showConfirmTransferSite = false;
975 handleTransferSite(i: PostListing) {
976 let form: TransferSiteForm = {
977 user_id: i.props.post.creator_id,
979 WebSocketService.Instance.transferSite(form);
980 i.state.showConfirmTransferSite = false;
984 handleImageExpandClick(i: PostListing) {
985 i.state.imageExpanded = !i.state.imageExpanded;
989 handleViewSource(i: PostListing) {
990 i.state.viewSource = !i.state.viewSource;