]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/post/post-listing.tsx
Use http client (#1081)
[lemmy-ui.git] / src / shared / components / post / post-listing.tsx
index 7eb289c81d1704399e24002dba78d64997e1118d..f1f06c5869f123641f84edb82c449da788170702 100644 (file)
@@ -11,6 +11,7 @@ import {
   CreatePostLike,
   CreatePostReport,
   DeletePost,
+  EditPost,
   FeaturePost,
   Language,
   LockPost,
@@ -24,8 +25,8 @@ import {
 } from "lemmy-js-client";
 import { getExternalHost, getHttpBase } from "../../env";
 import { i18n } from "../../i18next";
-import { BanType, PostFormParams, PurgeType } from "../../interfaces";
-import { UserService, WebSocketService } from "../../services";
+import { BanType, PostFormParams, PurgeType, VoteType } from "../../interfaces";
+import { UserService } from "../../services";
 import {
   amAdmin,
   amCommunityCreator,
@@ -43,13 +44,13 @@ import {
   mdNoImages,
   mdToHtml,
   mdToHtmlInline,
-  myAuth,
+  myAuthRequired,
+  newVote,
   numToSI,
   relTags,
   setupTippy,
   share,
   showScores,
-  wsClient,
 } from "../../utils";
 import { Icon, PurgeWarning, Spinner } from "../common/icon";
 import { MomentTime } from "../common/moment-time";
@@ -81,15 +82,25 @@ interface PostListingState {
   showBody: boolean;
   showReportDialog: boolean;
   reportReason?: string;
-  my_vote?: number;
-  score: number;
-  upvotes: number;
-  downvotes: number;
+  upvoteLoading: boolean;
+  downvoteLoading: boolean;
+  reportLoading: boolean;
+  blockLoading: boolean;
+  lockLoading: boolean;
+  deleteLoading: boolean;
+  removeLoading: boolean;
+  saveLoading: boolean;
+  featureCommunityLoading: boolean;
+  featureLocalLoading: boolean;
+  banLoading: boolean;
+  addModLoading: boolean;
+  addAdminLoading: boolean;
+  transferLoading: boolean;
 }
 
 interface PostListingProps {
   post_view: PostView;
-  duplicates?: PostView[];
+  crossPosts?: PostView[];
   moderators?: CommunityModeratorView[];
   admins?: PersonView[];
   allLanguages: Language[];
@@ -100,6 +111,22 @@ interface PostListingProps {
   enableDownvotes?: boolean;
   enableNsfw?: boolean;
   viewOnly?: boolean;
+  onPostEdit(form: EditPost): void;
+  onPostVote(form: CreatePostLike): void;
+  onPostReport(form: CreatePostReport): void;
+  onBlockPerson(form: BlockPerson): void;
+  onLockPost(form: LockPost): void;
+  onDeletePost(form: DeletePost): void;
+  onRemovePost(form: RemovePost): void;
+  onSavePost(form: SavePost): void;
+  onFeaturePost(form: FeaturePost): void;
+  onPurgePerson(form: PurgePerson): void;
+  onPurgePost(form: PurgePost): void;
+  onBanPersonFromCommunity(form: BanFromCommunity): void;
+  onBanPerson(form: BanPerson): void;
+  onAddModToCommunity(form: AddModToCommunity): void;
+  onAddAdmin(form: AddAdmin): void;
+  onTransferCommunity(form: TransferCommunity): void;
 }
 
 export class PostListing extends Component<PostListingProps, PostListingState> {
@@ -108,7 +135,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     showRemoveDialog: false,
     showPurgeDialog: false,
     purgeType: PurgeType.Person,
-    purgeLoading: false,
     showBanDialog: false,
     banType: BanType.Community,
     removeData: false,
@@ -120,35 +146,59 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     showMoreMobile: false,
     showBody: false,
     showReportDialog: false,
-    my_vote: this.props.post_view.my_vote,
-    score: this.props.post_view.counts.score,
-    upvotes: this.props.post_view.counts.upvotes,
-    downvotes: this.props.post_view.counts.downvotes,
+    upvoteLoading: false,
+    downvoteLoading: false,
+    purgeLoading: false,
+    reportLoading: false,
+    blockLoading: false,
+    lockLoading: false,
+    deleteLoading: false,
+    removeLoading: false,
+    saveLoading: false,
+    featureCommunityLoading: false,
+    featureLocalLoading: false,
+    banLoading: false,
+    addModLoading: false,
+    addAdminLoading: false,
+    transferLoading: false,
   };
 
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.handlePostLike = this.handlePostLike.bind(this);
-    this.handlePostDisLike = this.handlePostDisLike.bind(this);
     this.handleEditPost = this.handleEditPost.bind(this);
     this.handleEditCancel = this.handleEditCancel.bind(this);
   }
 
   componentWillReceiveProps(nextProps: PostListingProps) {
-    this.setState({
-      my_vote: nextProps.post_view.my_vote,
-      upvotes: nextProps.post_view.counts.upvotes,
-      downvotes: nextProps.post_view.counts.downvotes,
-      score: nextProps.post_view.counts.score,
-    });
-    if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
-      this.setState({ imageExpanded: false });
+    if (this.props !== nextProps) {
+      this.setState({
+        upvoteLoading: false,
+        downvoteLoading: false,
+        purgeLoading: false,
+        reportLoading: false,
+        blockLoading: false,
+        lockLoading: false,
+        deleteLoading: false,
+        removeLoading: false,
+        saveLoading: false,
+        featureCommunityLoading: false,
+        featureLocalLoading: false,
+        banLoading: false,
+        addModLoading: false,
+        addAdminLoading: false,
+        transferLoading: false,
+        imageExpanded: false,
+      });
     }
   }
 
+  get postView(): PostView {
+    return this.props.post_view;
+  }
+
   render() {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
 
     return (
       <div className="post-listing">
@@ -156,30 +206,29 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           <>
             {this.listing()}
             {this.state.imageExpanded && !this.props.hideImage && this.img}
-            {post.url && this.showBody && post.embed_title && (
+            {post.url && this.state.showBody && post.embed_title && (
               <MetadataCard post={post} />
             )}
             {this.showBody && this.body()}
           </>
         ) : (
-          <div className="col-12">
-            <PostForm
-              post_view={this.props.post_view}
-              onEdit={this.handleEditPost}
-              onCancel={this.handleEditCancel}
-              enableNsfw={this.props.enableNsfw}
-              enableDownvotes={this.props.enableDownvotes}
-              allLanguages={this.props.allLanguages}
-              siteLanguages={this.props.siteLanguages}
-            />
-          </div>
+          <PostForm
+            post_view={this.postView}
+            crossPosts={this.props.crossPosts}
+            onEdit={this.handleEditPost}
+            onCancel={this.handleEditCancel}
+            enableNsfw={this.props.enableNsfw}
+            enableDownvotes={this.props.enableDownvotes}
+            allLanguages={this.props.allLanguages}
+            siteLanguages={this.props.siteLanguages}
+          />
         )}
       </div>
     );
   }
 
   body() {
-    const body = this.props.post_view.post.body;
+    const body = this.postView.post.body;
     return body ? (
       <div className="col-12 card my-2 p-2">
         {this.state.viewSource ? (
@@ -194,12 +243,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get img() {
-    const src = this.imageSrc;
-    return src ? (
+    return this.imageSrc ? (
       <>
         <div className="offset-sm-3 my-2 d-none d-sm-block">
-          <a href={src} className="d-inline-block">
-            <PictrsImage src={src} />
+          <a href={this.imageSrc} className="d-inline-block">
+            <PictrsImage src={this.imageSrc} />
           </a>
         </div>
         <div className="my-2 d-block d-sm-none">
@@ -207,7 +255,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             className="d-inline-block"
             onClick={linkEvent(this, this.handleImageExpandClick)}
           >
-            <PictrsImage src={src} />
+            <PictrsImage src={this.imageSrc} />
           </a>
         </div>
       </>
@@ -217,7 +265,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   imgThumb(src: string) {
-    const post_view = this.props.post_view;
+    const post_view = this.postView;
     return (
       <PictrsImage
         src={src}
@@ -229,7 +277,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get imageSrc(): string | undefined {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
     const url = post.url;
     const thumbnail = post.thumbnail_url;
 
@@ -249,7 +297,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   thumbnail() {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
     const url = post.url;
     const thumbnail = post.thumbnail_url;
 
@@ -318,7 +366,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   createdLine() {
-    const post_view = this.props.post_view;
+    const post_view = this.postView;
     const url = post_view.post.url;
     const body = post_view.post.body;
     return (
@@ -401,21 +449,25 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       <div className={`vote-bar col-1 pr-0 small text-center`}>
         <button
           className={`btn-animate btn btn-link p-0 ${
-            this.state.my_vote === 1 ? "text-info" : "text-muted"
+            this.postView.my_vote == 1 ? "text-info" : "text-muted"
           }`}
-          onClick={this.handlePostLike}
+          onClick={linkEvent(this, this.handleUpvote)}
           data-tippy-content={i18n.t("upvote")}
           aria-label={i18n.t("upvote")}
-          aria-pressed={this.state.my_vote === 1}
+          aria-pressed={this.postView.my_vote === 1}
         >
-          <Icon icon="arrow-up1" classes="upvote" />
+          {this.state.upvoteLoading ? (
+            <Spinner />
+          ) : (
+            <Icon icon="arrow-up1" classes="upvote" />
+          )}
         </button>
         {showScores() ? (
           <div
             className={`unselectable pointer font-weight-bold text-muted px-1`}
             data-tippy-content={this.pointsTippy}
           >
-            {numToSI(this.state.score)}
+            {numToSI(this.postView.counts.score)}
           </div>
         ) : (
           <div className="p-1"></div>
@@ -423,14 +475,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         {this.props.enableDownvotes && (
           <button
             className={`btn-animate btn btn-link p-0 ${
-              this.state.my_vote === -1 ? "text-danger" : "text-muted"
+              this.postView.my_vote == -1 ? "text-danger" : "text-muted"
             }`}
-            onClick={this.handlePostDisLike}
+            onClick={linkEvent(this, this.handleDownvote)}
             data-tippy-content={i18n.t("downvote")}
             aria-label={i18n.t("downvote")}
-            aria-pressed={this.state.my_vote === -1}
+            aria-pressed={this.postView.my_vote === -1}
           >
-            <Icon icon="arrow-down1" classes="downvote" />
+            {this.state.downvoteLoading ? (
+              <Spinner />
+            ) : (
+              <Icon icon="arrow-down1" classes="downvote" />
+            )}
           </button>
         )}
       </div>
@@ -438,7 +494,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get postLink() {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
     return (
       <Link
         className={`d-inline-block ${
@@ -458,7 +514,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   postTitleLine() {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
     const url = post.url;
 
     return (
@@ -550,7 +606,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   duplicatesLine() {
-    const dupes = this.props.duplicates;
+    const dupes = this.props.crossPosts;
     return dupes && dupes.length > 0 ? (
       <ul className="list-inline mb-1 small text-muted">
         <>
@@ -572,7 +628,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   commentsLine(mobile = false) {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
 
     return (
       <div className="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
@@ -606,7 +662,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   postActions(mobile = false) {
     // Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
     // Possible enhancement: Make each button a component.
-    const post_view = this.props.post_view;
+    const post_view = this.postView;
     return (
       <>
         {this.saveButton}
@@ -647,7 +703,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get commentsButton() {
-    const post_view = this.props.post_view;
+    const post_view = this.postView;
     return (
       <Link
         className="btn btn-link text-muted py-0 pl-0 text-muted"
@@ -674,7 +730,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get unreadCount(): number | undefined {
-    const pv = this.props.post_view;
+    const pv = this.postView;
     return pv.unread_comments == pv.counts.comments || pv.unread_comments == 0
       ? undefined
       : pv.unread_comments;
@@ -690,37 +746,51 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         <div>
           <button
             className={`btn-animate btn py-0 px-1 ${
-              this.state.my_vote === 1 ? "text-info" : "text-muted"
+              this.postView.my_vote === 1 ? "text-info" : "text-muted"
             }`}
             {...tippy}
-            onClick={this.handlePostLike}
+            onClick={linkEvent(this, this.handleUpvote)}
             aria-label={i18n.t("upvote")}
-            aria-pressed={this.state.my_vote === 1}
+            aria-pressed={this.postView.my_vote === 1}
           >
-            <Icon icon="arrow-up1" classes="icon-inline small" />
-            {showScores() && (
-              <span className="ml-2">{numToSI(this.state.upvotes)}</span>
+            {this.state.upvoteLoading ? (
+              <Spinner />
+            ) : (
+              <>
+                <Icon icon="arrow-up1" classes="icon-inline small" />
+                {showScores() && (
+                  <span className="ml-2">
+                    {numToSI(this.postView.counts.upvotes)}
+                  </span>
+                )}
+              </>
             )}
           </button>
           {this.props.enableDownvotes && (
             <button
               className={`ml-2 btn-animate btn py-0 px-1 ${
-                this.state.my_vote === -1 ? "text-danger" : "text-muted"
+                this.postView.my_vote === -1 ? "text-danger" : "text-muted"
               }`}
-              onClick={this.handlePostDisLike}
+              onClick={linkEvent(this, this.handleDownvote)}
               {...tippy}
               aria-label={i18n.t("downvote")}
-              aria-pressed={this.state.my_vote === -1}
+              aria-pressed={this.postView.my_vote === -1}
             >
-              <Icon icon="arrow-down1" classes="icon-inline small" />
-              {showScores() && (
-                <span
-                  className={classNames("ml-2", {
-                    invisible: this.state.downvotes === 0,
-                  })}
-                >
-                  {numToSI(this.state.downvotes)}
-                </span>
+              {this.state.downvoteLoading ? (
+                <Spinner />
+              ) : (
+                <>
+                  <Icon icon="arrow-down1" classes="icon-inline small" />
+                  {showScores() && (
+                    <span
+                      className={classNames("ml-2", {
+                        invisible: this.postView.counts.downvotes === 0,
+                      })}
+                    >
+                      {numToSI(this.postView.counts.downvotes)}
+                    </span>
+                  )}
+                </>
               )}
             </button>
           )}
@@ -730,7 +800,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get saveButton() {
-    const saved = this.props.post_view.saved;
+    const saved = this.postView.saved;
     const label = saved ? i18n.t("unsave") : i18n.t("save");
     return (
       <button
@@ -739,11 +809,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         data-tippy-content={label}
         aria-label={label}
       >
-        <Icon
-          icon="star"
-          classes={classNames({ "text-warning": saved })}
-          inline
-        />
+        {this.state.saveLoading ? (
+          <Spinner />
+        ) : (
+          <Icon
+            icon="star"
+            classes={classNames({ "text-warning": saved })}
+            inline
+          />
+        )}
       </button>
     );
   }
@@ -784,11 +858,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return (
       <button
         className="btn btn-link btn-animate text-muted py-0"
-        onClick={linkEvent(this, this.handleBlockUserClick)}
+        onClick={linkEvent(this, this.handleBlockPersonClick)}
         data-tippy-content={i18n.t("block_user")}
         aria-label={i18n.t("block_user")}
       >
-        <Icon icon="slash" inline />
+        {this.state.blockLoading ? <Spinner /> : <Icon icon="slash" inline />}
       </button>
     );
   }
@@ -807,7 +881,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get deleteButton() {
-    const deleted = this.props.post_view.post.deleted;
+    const deleted = this.postView.post.deleted;
     const label = !deleted ? i18n.t("delete") : i18n.t("restore");
     return (
       <button
@@ -816,11 +890,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         data-tippy-content={label}
         aria-label={label}
       >
-        <Icon
-          icon="trash"
-          classes={classNames({ "text-danger": deleted })}
-          inline
-        />
+        {this.state.deleteLoading ? (
+          <Spinner />
+        ) : (
+          <Icon
+            icon="trash"
+            classes={classNames({ "text-danger": deleted })}
+            inline
+          />
+        )}
       </button>
     );
   }
@@ -856,7 +934,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get lockButton() {
-    const locked = this.props.post_view.post.locked;
+    const locked = this.postView.post.locked;
     const label = locked ? i18n.t("unlock") : i18n.t("lock");
     return (
       <button
@@ -865,22 +943,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         data-tippy-content={label}
         aria-label={label}
       >
-        <Icon
-          icon="lock"
-          classes={classNames({ "text-danger": locked })}
-          inline
-        />
+        {this.state.lockLoading ? (
+          <Spinner />
+        ) : (
+          <Icon
+            icon="lock"
+            classes={classNames({ "text-danger": locked })}
+            inline
+          />
+        )}
       </button>
     );
   }
 
   get featureButton() {
-    const featuredCommunity = this.props.post_view.post.featured_community;
+    const featuredCommunity = this.postView.post.featured_community;
     const labelCommunity = featuredCommunity
       ? i18n.t("unfeature_from_community")
       : i18n.t("feature_in_community");
 
-    const featuredLocal = this.props.post_view.post.featured_local;
+    const featuredLocal = this.postView.post.featured_local;
     const labelLocal = featuredLocal
       ? i18n.t("unfeature_from_local")
       : i18n.t("feature_in_local");
@@ -892,12 +974,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           data-tippy-content={labelCommunity}
           aria-label={labelCommunity}
         >
-          <Icon
-            icon="pin"
-            classes={classNames({ "text-success": featuredCommunity })}
-            inline
-          />{" "}
-          Community
+          {this.state.featureCommunityLoading ? (
+            <Spinner />
+          ) : (
+            <span>
+              <Icon
+                icon="pin"
+                classes={classNames({ "text-success": featuredCommunity })}
+                inline
+              />
+              {i18n.t("community")}
+            </span>
+          )}
         </button>
         {amAdmin() && (
           <button
@@ -906,12 +994,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             data-tippy-content={labelLocal}
             aria-label={labelLocal}
           >
-            <Icon
-              icon="pin"
-              classes={classNames({ "text-success": featuredLocal })}
-              inline
-            />{" "}
-            Local
+            {this.state.featureLocalLoading ? (
+              <Spinner />
+            ) : (
+              <span>
+                <Icon
+                  icon="pin"
+                  classes={classNames({ "text-success": featuredLocal })}
+                  inline
+                />
+                {i18n.t("local")}
+              </span>
+            )}
           </button>
         )}
       </span>
@@ -919,7 +1013,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   get modRemoveButton() {
-    const removed = this.props.post_view.post.removed;
+    const removed = this.postView.post.removed;
     return (
       <button
         className="btn btn-link btn-animate text-muted py-0"
@@ -929,7 +1023,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         )}
       >
         {/* TODO: Find an icon for this. */}
-        {!removed ? i18n.t("remove") : i18n.t("restore")}
+        {this.state.removeLoading ? (
+          <Spinner />
+        ) : !removed ? (
+          i18n.t("remove")
+        ) : (
+          i18n.t("restore")
+        )}
       </button>
     );
   }
@@ -939,7 +1039,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
    */
   userActionsLine() {
     // TODO: make nicer
-    const post_view = this.props.post_view;
+    const post_view = this.postView;
     return (
       this.state.showAdvanced && (
         <>
@@ -966,7 +1066,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                     )}
                     aria-label={i18n.t("unban")}
                   >
-                    {i18n.t("unban")}
+                    {this.state.banLoading ? <Spinner /> : i18n.t("unban")}
                   </button>
                 ))}
               {!post_view.creator_banned_from_community && (
@@ -979,9 +1079,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                       : i18n.t("appoint_as_mod")
                   }
                 >
-                  {this.creatorIsMod_
-                    ? i18n.t("remove_as_mod")
-                    : i18n.t("appoint_as_mod")}
+                  {this.state.addModLoading ? (
+                    <Spinner />
+                  ) : this.creatorIsMod_ ? (
+                    i18n.t("remove_as_mod")
+                  ) : (
+                    i18n.t("appoint_as_mod")
+                  )}
                 </button>
               )}
             </>
@@ -1014,7 +1118,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                   aria-label={i18n.t("yes")}
                   onClick={linkEvent(this, this.handleTransferCommunity)}
                 >
-                  {i18n.t("yes")}
+                  {this.state.transferLoading ? <Spinner /> : i18n.t("yes")}
                 </button>
                 <button
                   className="btn btn-link btn-animate text-muted py-0 d-inline-block"
@@ -1047,7 +1151,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                       onClick={linkEvent(this, this.handleModBanSubmit)}
                       aria-label={i18n.t("unban_from_site")}
                     >
-                      {i18n.t("unban_from_site")}
+                      {this.state.banLoading ? (
+                        <Spinner />
+                      ) : (
+                        i18n.t("unban_from_site")
+                      )}
                     </button>
                   )}
                   <button
@@ -1076,9 +1184,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                       : i18n.t("appoint_as_admin")
                   }
                 >
-                  {this.creatorIsAdmin_
-                    ? i18n.t("remove_as_admin")
-                    : i18n.t("appoint_as_admin")}
+                  {this.state.addAdminLoading ? (
+                    <Spinner />
+                  ) : this.creatorIsAdmin_ ? (
+                    i18n.t("remove_as_admin")
+                  ) : (
+                    i18n.t("appoint_as_admin")
+                  )}
                 </button>
               )}
             </>
@@ -1089,7 +1201,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   removeAndBanDialogs() {
-    const post = this.props.post_view;
+    const post = this.postView;
     const purgeTypeText =
       this.state.purgeType == PurgeType.Post
         ? i18n.t("purge_post")
@@ -1117,7 +1229,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               className="btn btn-secondary"
               aria-label={i18n.t("remove_post")}
             >
-              {i18n.t("remove_post")}
+              {this.state.removeLoading ? <Spinner /> : i18n.t("remove_post")}
             </button>
           </form>
         )}
@@ -1179,7 +1291,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 className="btn btn-secondary"
                 aria-label={i18n.t("ban")}
               >
-                {i18n.t("ban")} {post.creator.name}
+                {this.state.banLoading ? (
+                  <Spinner />
+                ) : (
+                  <span>
+                    {i18n.t("ban")} {post.creator.name}
+                  </span>
+                )}
               </button>
             </div>
           </form>
@@ -1206,7 +1324,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               className="btn btn-secondary"
               aria-label={i18n.t("create_report")}
             >
-              {i18n.t("create_report")}
+              {this.state.reportLoading ? <Spinner /> : i18n.t("create_report")}
             </button>
           </form>
         )}
@@ -1235,7 +1353,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 className="btn btn-secondary"
                 aria-label={purgeTypeText}
               >
-                {purgeTypeText}
+                {this.state.purgeLoading ? <Spinner /> : { purgeTypeText }}
               </button>
             )}
           </form>
@@ -1245,7 +1363,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   mobileThumbnail() {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
     return post.thumbnail_url || (post.url && isImage(post.url)) ? (
       <div className="row">
         <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
@@ -1262,7 +1380,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   showMobilePreview() {
-    const body = this.props.post_view.post.body;
+    const body = this.postView.post.body;
     return !this.showBody && body ? (
       <div className="md-div mb-1 preview-lines">{body}</div>
     ) : (
@@ -1320,97 +1438,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   private get myPost(): boolean {
     return (
-      this.props.post_view.creator.id ==
+      this.postView.creator.id ==
       UserService.Instance.myUserInfo?.local_user_view.person.id
     );
   }
-
-  handlePostLike(event: any) {
-    event.preventDefault();
-    if (!UserService.Instance.myUserInfo) {
-      this.context.router.history.push(`/login`);
-    }
-
-    const myVote = this.state.my_vote;
-    const newVote = myVote == 1 ? 0 : 1;
-
-    if (myVote == 1) {
-      this.setState({
-        score: this.state.score - 1,
-        upvotes: this.state.upvotes - 1,
-      });
-    } else if (myVote == -1) {
-      this.setState({
-        score: this.state.score + 2,
-        upvotes: this.state.upvotes + 1,
-        downvotes: this.state.downvotes - 1,
-      });
-    } else {
-      this.setState({
-        score: this.state.score + 1,
-        upvotes: this.state.upvotes + 1,
-      });
-    }
-
-    this.setState({ my_vote: newVote });
-
-    const auth = myAuth();
-    if (auth) {
-      const form: CreatePostLike = {
-        post_id: this.props.post_view.post.id,
-        score: newVote,
-        auth,
-      };
-
-      WebSocketService.Instance.send(wsClient.likePost(form));
-      this.setState(this.state);
-    }
-    setupTippy();
-  }
-
-  handlePostDisLike(event: any) {
-    event.preventDefault();
-    if (!UserService.Instance.myUserInfo) {
-      this.context.router.history.push(`/login`);
-    }
-
-    const myVote = this.state.my_vote;
-    const newVote = myVote == -1 ? 0 : -1;
-
-    if (myVote == 1) {
-      this.setState({
-        score: this.state.score - 2,
-        upvotes: this.state.upvotes - 1,
-        downvotes: this.state.downvotes + 1,
-      });
-    } else if (myVote == -1) {
-      this.setState({
-        score: this.state.score + 1,
-        downvotes: this.state.downvotes - 1,
-      });
-    } else {
-      this.setState({
-        score: this.state.score - 1,
-        downvotes: this.state.downvotes + 1,
-      });
-    }
-
-    this.setState({ my_vote: newVote });
-
-    const auth = myAuth();
-    if (auth) {
-      const form: CreatePostLike = {
-        post_id: this.props.post_view.post.id,
-        score: newVote,
-        auth,
-      };
-
-      WebSocketService.Instance.send(wsClient.likePost(form));
-      this.setState(this.state);
-    }
-    setupTippy();
-  }
-
   handleEditClick(i: PostListing) {
     i.setState({ showEdit: true });
   }
@@ -1420,8 +1451,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   // The actual editing is done in the receive for post
-  handleEditPost() {
+  handleEditPost(form: EditPost) {
     this.setState({ showEdit: false });
+    this.props.onPostEdit(form);
   }
 
   handleShare(i: PostListing) {
@@ -1443,61 +1475,44 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   handleReportSubmit(i: PostListing, event: any) {
     event.preventDefault();
-    const auth = myAuth();
-    const reason = i.state.reportReason;
-    if (auth && reason) {
-      const form: CreatePostReport = {
-        post_id: i.props.post_view.post.id,
-        reason,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.createPostReport(form));
-
-      i.setState({ showReportDialog: false });
-    }
+    i.setState({ reportLoading: true });
+    i.props.onPostReport({
+      post_id: i.postView.post.id,
+      reason: i.state.reportReason ?? "",
+      auth: myAuthRequired(),
+    });
   }
 
-  handleBlockUserClick(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const blockUserForm: BlockPerson = {
-        person_id: i.props.post_view.creator.id,
-        block: true,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
-    }
+  handleBlockPersonClick(i: PostListing) {
+    i.setState({ blockLoading: true });
+    i.props.onBlockPerson({
+      person_id: i.postView.creator.id,
+      block: true,
+      auth: myAuthRequired(),
+    });
   }
 
   handleDeleteClick(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const deleteForm: DeletePost = {
-        post_id: i.props.post_view.post.id,
-        deleted: !i.props.post_view.post.deleted,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
-    }
+    i.setState({ deleteLoading: true });
+    i.props.onDeletePost({
+      post_id: i.postView.post.id,
+      deleted: !i.postView.post.deleted,
+      auth: myAuthRequired(),
+    });
   }
 
   handleSavePostClick(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const saved =
-        i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
-      const form: SavePost = {
-        post_id: i.props.post_view.post.id,
-        save: saved,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.savePost(form));
-    }
+    i.setState({ saveLoading: true });
+    i.props.onSavePost({
+      post_id: i.postView.post.id,
+      save: !i.postView.saved,
+      auth: myAuthRequired(),
+    });
   }
 
   get crossPostParams(): PostFormParams {
     const queryParams: PostFormParams = {};
-    const { name, url } = this.props.post_view.post;
+    const { name, url } = this.postView.post;
 
     queryParams.name = name;
 
@@ -1514,7 +1529,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   crossPostBody(): string | undefined {
-    const post = this.props.post_view.post;
+    const post = this.postView.post;
     const body = post.body;
 
     return body
@@ -1546,56 +1561,41 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   handleModRemoveSubmit(i: PostListing, event: any) {
     event.preventDefault();
-
-    const auth = myAuth();
-    if (auth) {
-      const form: RemovePost = {
-        post_id: i.props.post_view.post.id,
-        removed: !i.props.post_view.post.removed,
-        reason: i.state.removeReason,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.removePost(form));
-      i.setState({ showRemoveDialog: false });
-    }
+    i.setState({ removeLoading: true });
+    i.props.onRemovePost({
+      post_id: i.postView.post.id,
+      removed: !i.postView.post.removed,
+      auth: myAuthRequired(),
+    });
   }
 
   handleModLock(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const form: LockPost = {
-        post_id: i.props.post_view.post.id,
-        locked: !i.props.post_view.post.locked,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.lockPost(form));
-    }
+    i.setState({ lockLoading: true });
+    i.props.onLockPost({
+      post_id: i.postView.post.id,
+      locked: !i.postView.post.locked,
+      auth: myAuthRequired(),
+    });
   }
 
   handleModFeaturePostLocal(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const form: FeaturePost = {
-        post_id: i.props.post_view.post.id,
-        feature_type: "Local",
-        featured: !i.props.post_view.post.featured_local,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.featurePost(form));
-    }
+    i.setState({ featureLocalLoading: true });
+    i.props.onFeaturePost({
+      post_id: i.postView.post.id,
+      featured: !i.postView.post.featured_local,
+      feature_type: "Local",
+      auth: myAuthRequired(),
+    });
   }
 
   handleModFeaturePostCommunity(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const form: FeaturePost = {
-        post_id: i.props.post_view.post.id,
-        feature_type: "Community",
-        featured: !i.props.post_view.post.featured_community,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.featurePost(form));
-    }
+    i.setState({ featureCommunityLoading: true });
+    i.props.onFeaturePost({
+      post_id: i.postView.post.id,
+      featured: !i.postView.post.featured_community,
+      feature_type: "Community",
+      auth: myAuthRequired(),
+    });
   }
 
   handleModBanFromCommunityShow(i: PostListing) {
@@ -1636,26 +1636,19 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   handlePurgeSubmit(i: PostListing, event: any) {
     event.preventDefault();
-
-    const auth = myAuth();
-    if (auth) {
-      if (i.state.purgeType == PurgeType.Person) {
-        const form: PurgePerson = {
-          person_id: i.props.post_view.creator.id,
-          reason: i.state.purgeReason,
-          auth,
-        };
-        WebSocketService.Instance.send(wsClient.purgePerson(form));
-      } else if (i.state.purgeType == PurgeType.Post) {
-        const form: PurgePost = {
-          post_id: i.props.post_view.post.id,
-          reason: i.state.purgeReason,
-          auth,
-        };
-        WebSocketService.Instance.send(wsClient.purgePost(form));
-      }
-
-      i.setState({ purgeLoading: true });
+    i.setState({ purgeLoading: true });
+    if (i.state.purgeType == PurgeType.Person) {
+      i.props.onPurgePerson({
+        person_id: i.postView.creator.id,
+        reason: i.state.purgeReason,
+        auth: myAuthRequired(),
+      });
+    } else if (i.state.purgeType == PurgeType.Post) {
+      i.props.onPurgePost({
+        post_id: i.postView.post.id,
+        reason: i.state.purgeReason,
+        auth: myAuthRequired(),
+      });
     }
   }
 
@@ -1667,88 +1660,70 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     i.setState({ banExpireDays: event.target.value });
   }
 
-  handleModBanFromCommunitySubmit(i: PostListing) {
+  handleModBanFromCommunitySubmit(i: PostListing, event: any) {
     i.setState({ banType: BanType.Community });
-    i.handleModBanBothSubmit(i);
+    i.handleModBanBothSubmit(i, event);
   }
 
-  handleModBanSubmit(i: PostListing) {
+  handleModBanSubmit(i: PostListing, event: any) {
     i.setState({ banType: BanType.Site });
-    i.handleModBanBothSubmit(i);
-  }
-
-  handleModBanBothSubmit(i: PostListing, event?: any) {
-    if (event) event.preventDefault();
-    const auth = myAuth();
-    if (auth) {
-      const ban = !i.props.post_view.creator_banned_from_community;
-      const person_id = i.props.post_view.creator.id;
-      const remove_data = i.state.removeData;
-      const reason = i.state.banReason;
-      const expires = futureDaysToUnixTime(i.state.banExpireDays);
-
-      if (i.state.banType == BanType.Community) {
-        // If its an unban, restore all their data
-        if (ban == false) {
-          i.setState({ removeData: false });
-        }
-
-        const form: BanFromCommunity = {
-          person_id,
-          community_id: i.props.post_view.community.id,
-          ban,
-          remove_data,
-          reason,
-          expires,
-          auth,
-        };
-        WebSocketService.Instance.send(wsClient.banFromCommunity(form));
-      } else {
-        // If its an unban, restore all their data
-        const ban = !i.props.post_view.creator.banned;
-        if (ban == false) {
-          i.setState({ removeData: false });
-        }
-        const form: BanPerson = {
-          person_id,
-          ban,
-          remove_data,
-          reason,
-          expires,
-          auth,
-        };
-        WebSocketService.Instance.send(wsClient.banPerson(form));
-      }
+    i.handleModBanBothSubmit(i, event);
+  }
+
+  handleModBanBothSubmit(i: PostListing, event: any) {
+    event.preventDefault();
+    i.setState({ banLoading: true });
 
-      i.setState({ showBanDialog: false });
+    const ban = !i.props.post_view.creator_banned_from_community;
+    // If its an unban, restore all their data
+    if (ban == false) {
+      i.setState({ removeData: false });
+    }
+    const person_id = i.props.post_view.creator.id;
+    const remove_data = i.state.removeData;
+    const reason = i.state.banReason;
+    const expires = futureDaysToUnixTime(i.state.banExpireDays);
+
+    if (i.state.banType == BanType.Community) {
+      const community_id = i.postView.community.id;
+      i.props.onBanPersonFromCommunity({
+        community_id,
+        person_id,
+        ban,
+        remove_data,
+        reason,
+        expires,
+        auth: myAuthRequired(),
+      });
+    } else {
+      i.props.onBanPerson({
+        person_id,
+        ban,
+        remove_data,
+        reason,
+        expires,
+        auth: myAuthRequired(),
+      });
     }
   }
 
   handleAddModToCommunity(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const form: AddModToCommunity = {
-        person_id: i.props.post_view.creator.id,
-        community_id: i.props.post_view.community.id,
-        added: !i.creatorIsMod_,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.addModToCommunity(form));
-      i.setState(i.state);
-    }
+    i.setState({ addModLoading: true });
+    i.props.onAddModToCommunity({
+      community_id: i.postView.community.id,
+      person_id: i.postView.creator.id,
+      added: !i.creatorIsMod_,
+      auth: myAuthRequired(),
+    });
   }
 
   handleAddAdmin(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const form: AddAdmin = {
-        person_id: i.props.post_view.creator.id,
-        added: !i.creatorIsAdmin_,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.addAdmin(form));
-      i.setState(i.state);
-    }
+    i.setState({ addAdminLoading: true });
+    i.props.onAddAdmin({
+      person_id: i.postView.creator.id,
+      added: !i.creatorIsAdmin_,
+      auth: myAuthRequired(),
+    });
   }
 
   handleShowConfirmTransferCommunity(i: PostListing) {
@@ -1760,16 +1735,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleTransferCommunity(i: PostListing) {
-    const auth = myAuth();
-    if (auth) {
-      const form: TransferCommunity = {
-        community_id: i.props.post_view.community.id,
-        person_id: i.props.post_view.creator.id,
-        auth,
-      };
-      WebSocketService.Instance.send(wsClient.transferCommunity(form));
-      i.setState({ showConfirmTransferCommunity: false });
-    }
+    i.setState({ transferLoading: true });
+    i.props.onTransferCommunity({
+      community_id: i.postView.community.id,
+      person_id: i.postView.creator.id,
+      auth: myAuthRequired(),
+    });
   }
 
   handleShowConfirmTransferSite(i: PostListing) {
@@ -1808,20 +1779,38 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     setupTippy();
   }
 
+  handleUpvote(i: PostListing) {
+    i.setState({ upvoteLoading: true });
+    i.props.onPostVote({
+      post_id: i.postView.post.id,
+      score: newVote(VoteType.Upvote, i.props.post_view.my_vote),
+      auth: myAuthRequired(),
+    });
+  }
+
+  handleDownvote(i: PostListing) {
+    i.setState({ downvoteLoading: true });
+    i.props.onPostVote({
+      post_id: i.postView.post.id,
+      score: newVote(VoteType.Downvote, i.props.post_view.my_vote),
+      auth: myAuthRequired(),
+    });
+  }
+
   get pointsTippy(): string {
     const points = i18n.t("number_of_points", {
-      count: Number(this.state.score),
-      formattedCount: Number(this.state.score),
+      count: Number(this.postView.counts.score),
+      formattedCount: Number(this.postView.counts.score),
     });
 
     const upvotes = i18n.t("number_of_upvotes", {
-      count: Number(this.state.upvotes),
-      formattedCount: Number(this.state.upvotes),
+      count: Number(this.postView.counts.upvotes),
+      formattedCount: Number(this.postView.counts.upvotes),
     });
 
     const downvotes = i18n.t("number_of_downvotes", {
-      count: Number(this.state.downvotes),
-      formattedCount: Number(this.state.downvotes),
+      count: Number(this.postView.counts.downvotes),
+      formattedCount: Number(this.postView.counts.downvotes),
     });
 
     return `${points} • ${upvotes} • ${downvotes}`;
@@ -1829,7 +1818,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   get canModOnSelf_(): boolean {
     return canMod(
-      this.props.post_view.creator.id,
+      this.postView.creator.id,
       this.props.moderators,
       this.props.admins,
       undefined,
@@ -1839,21 +1828,21 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   get canMod_(): boolean {
     return canMod(
-      this.props.post_view.creator.id,
+      this.postView.creator.id,
       this.props.moderators,
       this.props.admins
     );
   }
 
   get canAdmin_(): boolean {
-    return canAdmin(this.props.post_view.creator.id, this.props.admins);
+    return canAdmin(this.postView.creator.id, this.props.admins);
   }
 
   get creatorIsMod_(): boolean {
-    return isMod(this.props.post_view.creator.id, this.props.moderators);
+    return isMod(this.postView.creator.id, this.props.moderators);
   }
 
   get creatorIsAdmin_(): boolean {
-    return isAdmin(this.props.post_view.creator.id, this.props.admins);
+    return isAdmin(this.postView.creator.id, this.props.admins);
   }
 }