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 img-expanded" 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">
336 {i18n.t('cross_posted_to')}
338 {this.props.post.duplicates.map(post => (
339 <li className="list-inline-item mr-2">
340 <Link to={`/post/${post.id}`}>{post.community_name}</Link>
346 <ul class="list-inline mb-1 text-muted small font-weight-bold">
347 {UserService.Instance.user && (
349 {this.props.showBody && (
351 <li className="list-inline-item mr-2">
354 onClick={linkEvent(this, this.handleSavePostClick)}
356 {post.saved ? i18n.t('unsave') : i18n.t('save')}
359 <li className="list-inline-item mr-2">
361 className="text-muted"
362 to={`/create_post${this.crossPostParams}`}
364 {i18n.t('cross_post')}
369 {this.myPost && this.props.showBody && (
371 <li className="list-inline-item">
374 onClick={linkEvent(this, this.handleEditClick)}
379 <li className="list-inline-item mr-2">
382 onClick={linkEvent(this, this.handleDeleteClick)}
384 {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
389 {this.canModOnSelf && (
391 <li className="list-inline-item">
394 onClick={linkEvent(this, this.handleModLock)}
396 {post.locked ? i18n.t('unlock') : i18n.t('lock')}
399 <li className="list-inline-item">
402 onClick={linkEvent(this, this.handleModSticky)}
404 {post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}
409 {/* Mods can ban from community, and appoint as mods to community */}
410 {(this.canMod || this.canAdmin) && (
411 <li className="list-inline-item">
415 onClick={linkEvent(this, this.handleModRemoveShow)}
422 onClick={linkEvent(this, this.handleModRemoveSubmit)}
432 <li className="list-inline-item">
433 {!post.banned_from_community ? (
438 this.handleModBanFromCommunityShow
448 this.handleModBanFromCommunitySubmit
456 {!post.banned_from_community && (
457 <li className="list-inline-item">
462 this.handleAddModToCommunity
466 ? i18n.t('remove_as_mod')
467 : i18n.t('appoint_as_mod')}
473 {/* Community creators and admins can transfer community to another mod */}
474 {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
475 <li className="list-inline-item">
476 {!this.state.showConfirmTransferCommunity ? (
481 this.handleShowConfirmTransferCommunity
484 {i18n.t('transfer_community')}
488 <span class="d-inline-block mr-1">
489 {i18n.t('are_you_sure')}
492 class="pointer d-inline-block mr-1"
495 this.handleTransferCommunity
501 class="pointer d-inline-block"
504 this.handleCancelShowConfirmTransferCommunity
513 {/* Admins can ban from all, and appoint other admins */}
517 <li className="list-inline-item">
521 onClick={linkEvent(this, this.handleModBanShow)}
523 {i18n.t('ban_from_site')}
528 onClick={linkEvent(this, this.handleModBanSubmit)}
530 {i18n.t('unban_from_site')}
536 <li className="list-inline-item">
539 onClick={linkEvent(this, this.handleAddAdmin)}
542 ? i18n.t('remove_as_admin')
543 : i18n.t('appoint_as_admin')}
549 {/* Site Creator can transfer to another admin */}
550 {this.amSiteCreator && this.isAdmin && (
551 <li className="list-inline-item">
552 {!this.state.showConfirmTransferSite ? (
557 this.handleShowConfirmTransferSite
560 {i18n.t('transfer_site')}
564 <span class="d-inline-block mr-1">
565 {i18n.t('are_you_sure')}
568 class="pointer d-inline-block mr-1"
569 onClick={linkEvent(this, this.handleTransferSite)}
574 class="pointer d-inline-block"
577 this.handleCancelShowConfirmTransferSite
588 {this.props.showBody && post.body && (
589 <li className="list-inline-item">
592 onClick={linkEvent(this, this.handleViewSource)}
594 {i18n.t('view_source')}
599 {this.state.showRemoveDialog && (
602 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
606 class="form-control mr-2"
607 placeholder={i18n.t('reason')}
608 value={this.state.removeReason}
609 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
611 <button type="submit" class="btn btn-secondary">
612 {i18n.t('remove_post')}
616 {this.state.showBanDialog && (
617 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
618 <div class="form-group row">
619 <label class="col-form-label" htmlFor="post-listing-reason">
624 id="post-listing-reason"
625 class="form-control mr-2"
626 placeholder={i18n.t('reason')}
627 value={this.state.banReason}
628 onInput={linkEvent(this, this.handleModBanReasonChange)}
631 {/* TODO hold off on expires until later */}
632 {/* <div class="form-group row"> */}
633 {/* <label class="col-form-label">Expires</label> */}
634 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
636 <div class="form-group row">
637 <button type="submit" class="btn btn-secondary">
638 {i18n.t('ban')} {post.creator_name}
643 {this.props.showBody && post.body && (
645 {this.state.viewSource ? (
646 <pre>{post.body}</pre>
650 dangerouslySetInnerHTML={mdToHtml(post.body)}
660 private get myPost(): boolean {
662 UserService.Instance.user &&
663 this.props.post.creator_id == UserService.Instance.user.id
667 get isMod(): boolean {
669 this.props.moderators &&
671 this.props.moderators.map(m => m.user_id),
672 this.props.post.creator_id
677 get isAdmin(): boolean {
681 this.props.admins.map(a => a.id),
682 this.props.post.creator_id
687 get canMod(): boolean {
688 if (this.props.admins && this.props.moderators) {
689 let adminsThenMods = this.props.admins
691 .concat(this.props.moderators.map(m => m.user_id));
694 UserService.Instance.user,
696 this.props.post.creator_id
703 get canModOnSelf(): boolean {
704 if (this.props.admins && this.props.moderators) {
705 let adminsThenMods = this.props.admins
707 .concat(this.props.moderators.map(m => m.user_id));
710 UserService.Instance.user,
712 this.props.post.creator_id,
720 get canAdmin(): boolean {
724 UserService.Instance.user,
725 this.props.admins.map(a => a.id),
726 this.props.post.creator_id
731 get amCommunityCreator(): boolean {
733 this.props.moderators &&
734 UserService.Instance.user &&
735 this.props.post.creator_id != UserService.Instance.user.id &&
736 UserService.Instance.user.id == this.props.moderators[0].user_id
740 get amSiteCreator(): boolean {
743 UserService.Instance.user &&
744 this.props.post.creator_id != UserService.Instance.user.id &&
745 UserService.Instance.user.id == this.props.admins[0].id
749 handlePostLike(i: PostListing) {
750 if (UserService.Instance.user) {
751 i.setState({ upvoteLoading: true });
754 let form: CreatePostLikeForm = {
755 post_id: i.props.post.id,
756 score: i.props.post.my_vote == 1 ? 0 : 1,
759 WebSocketService.Instance.likePost(form);
762 handlePostDisLike(i: PostListing) {
763 if (UserService.Instance.user) {
764 i.setState({ downvoteLoading: true });
767 let form: CreatePostLikeForm = {
768 post_id: i.props.post.id,
769 score: i.props.post.my_vote == -1 ? 0 : -1,
771 WebSocketService.Instance.likePost(form);
774 handleEditClick(i: PostListing) {
775 i.state.showEdit = true;
780 this.state.showEdit = false;
781 this.setState(this.state);
784 // The actual editing is done in the recieve for post
786 this.state.showEdit = false;
787 this.setState(this.state);
790 handleDeleteClick(i: PostListing) {
791 let deleteForm: PostFormI = {
792 body: i.props.post.body,
793 community_id: i.props.post.community_id,
794 name: i.props.post.name,
795 url: i.props.post.url,
796 edit_id: i.props.post.id,
797 creator_id: i.props.post.creator_id,
798 deleted: !i.props.post.deleted,
799 nsfw: i.props.post.nsfw,
802 WebSocketService.Instance.editPost(deleteForm);
805 handleSavePostClick(i: PostListing) {
806 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
807 let form: SavePostForm = {
808 post_id: i.props.post.id,
812 WebSocketService.Instance.savePost(form);
815 get crossPostParams(): string {
816 let params = `?title=${this.props.post.name}`;
817 if (this.props.post.url) {
818 params += `&url=${this.props.post.url}`;
820 if (this.props.post.body) {
821 params += `&body=${this.props.post.body}`;
826 handleModRemoveShow(i: PostListing) {
827 i.state.showRemoveDialog = true;
831 handleModRemoveReasonChange(i: PostListing, event: any) {
832 i.state.removeReason = event.target.value;
836 handleModRemoveSubmit(i: PostListing) {
837 event.preventDefault();
838 let form: PostFormI = {
839 name: i.props.post.name,
840 community_id: i.props.post.community_id,
841 edit_id: i.props.post.id,
842 creator_id: i.props.post.creator_id,
843 removed: !i.props.post.removed,
844 reason: i.state.removeReason,
845 nsfw: i.props.post.nsfw,
848 WebSocketService.Instance.editPost(form);
850 i.state.showRemoveDialog = false;
854 handleModLock(i: PostListing) {
855 let form: PostFormI = {
856 name: i.props.post.name,
857 community_id: i.props.post.community_id,
858 edit_id: i.props.post.id,
859 creator_id: i.props.post.creator_id,
860 nsfw: i.props.post.nsfw,
861 locked: !i.props.post.locked,
864 WebSocketService.Instance.editPost(form);
867 handleModSticky(i: PostListing) {
868 let form: PostFormI = {
869 name: i.props.post.name,
870 community_id: i.props.post.community_id,
871 edit_id: i.props.post.id,
872 creator_id: i.props.post.creator_id,
873 nsfw: i.props.post.nsfw,
874 stickied: !i.props.post.stickied,
877 WebSocketService.Instance.editPost(form);
880 handleModBanFromCommunityShow(i: PostListing) {
881 i.state.showBanDialog = true;
882 i.state.banType = BanType.Community;
886 handleModBanShow(i: PostListing) {
887 i.state.showBanDialog = true;
888 i.state.banType = BanType.Site;
892 handleModBanReasonChange(i: PostListing, event: any) {
893 i.state.banReason = event.target.value;
897 handleModBanExpiresChange(i: PostListing, event: any) {
898 i.state.banExpires = event.target.value;
902 handleModBanFromCommunitySubmit(i: PostListing) {
903 i.state.banType = BanType.Community;
905 i.handleModBanBothSubmit(i);
908 handleModBanSubmit(i: PostListing) {
909 i.state.banType = BanType.Site;
911 i.handleModBanBothSubmit(i);
914 handleModBanBothSubmit(i: PostListing) {
915 event.preventDefault();
917 if (i.state.banType == BanType.Community) {
918 let form: BanFromCommunityForm = {
919 user_id: i.props.post.creator_id,
920 community_id: i.props.post.community_id,
921 ban: !i.props.post.banned_from_community,
922 reason: i.state.banReason,
923 expires: getUnixTime(i.state.banExpires),
925 WebSocketService.Instance.banFromCommunity(form);
927 let form: BanUserForm = {
928 user_id: i.props.post.creator_id,
929 ban: !i.props.post.banned,
930 reason: i.state.banReason,
931 expires: getUnixTime(i.state.banExpires),
933 WebSocketService.Instance.banUser(form);
936 i.state.showBanDialog = false;
940 handleAddModToCommunity(i: PostListing) {
941 let form: AddModToCommunityForm = {
942 user_id: i.props.post.creator_id,
943 community_id: i.props.post.community_id,
946 WebSocketService.Instance.addModToCommunity(form);
950 handleAddAdmin(i: PostListing) {
951 let form: AddAdminForm = {
952 user_id: i.props.post.creator_id,
955 WebSocketService.Instance.addAdmin(form);
959 handleShowConfirmTransferCommunity(i: PostListing) {
960 i.state.showConfirmTransferCommunity = true;
964 handleCancelShowConfirmTransferCommunity(i: PostListing) {
965 i.state.showConfirmTransferCommunity = false;
969 handleTransferCommunity(i: PostListing) {
970 let form: TransferCommunityForm = {
971 community_id: i.props.post.community_id,
972 user_id: i.props.post.creator_id,
974 WebSocketService.Instance.transferCommunity(form);
975 i.state.showConfirmTransferCommunity = false;
979 handleShowConfirmTransferSite(i: PostListing) {
980 i.state.showConfirmTransferSite = true;
984 handleCancelShowConfirmTransferSite(i: PostListing) {
985 i.state.showConfirmTransferSite = false;
989 handleTransferSite(i: PostListing) {
990 let form: TransferSiteForm = {
991 user_id: i.props.post.creator_id,
993 WebSocketService.Instance.transferSite(form);
994 i.state.showConfirmTransferSite = false;
998 handleImageExpandClick(i: PostListing) {
999 i.state.imageExpanded = !i.state.imageExpanded;
1000 i.setState(i.state);
1003 handleViewSource(i: PostListing) {
1004 i.state.viewSource = !i.state.viewSource;
1005 i.setState(i.state);