]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/post/post-form.tsx
reset, merge issues
[lemmy-ui.git] / src / shared / components / post / post-form.tsx
index 18fcee774c4a31c647925493c21aa11abedcf64d..c21a6e2b8e7c189e654f232fa3b4662f8f6cdb26 100644 (file)
@@ -1,49 +1,42 @@
 import autosize from "autosize";
-import { Component, linkEvent } from "inferno";
-import { Prompt } from "inferno-router";
+import { Component, InfernoNode, linkEvent } from "inferno";
 import {
+  CommunityView,
   CreatePost,
   EditPost,
+  GetSiteMetadataResponse,
   Language,
-  PostResponse,
   PostView,
-  Search,
   SearchResponse,
-  UserOperation,
-  wsJsonToRes,
-  wsUserOp,
 } from "lemmy-js-client";
-import { Subscription } from "rxjs";
 import { i18n } from "../../i18next";
 import { PostFormParams } from "../../interfaces";
-import { UserService, WebSocketService } from "../../services";
+import { UserService } from "../../services";
+import { HttpService, RequestState } from "../../services/HttpService";
 import {
   Choice,
   archiveTodayUrl,
   capitalizeFirstLetter,
   communityToChoice,
-  debounce,
   fetchCommunities,
   getIdFromString,
-  getSiteMetadata,
   ghostArchiveUrl,
   isImage,
   myAuth,
-  pictrsDeleteToast,
+  myAuthRequired,
   relTags,
   setupTippy,
   toast,
   trendingFetchLimit,
-  uploadImage,
   validTitle,
   validURL,
   webArchiveUrl,
-  wsClient,
-  wsSubscribe,
 } from "../../utils";
+import { debounce } from "../../utils/helpers/debounce";
 import { Icon, Spinner } from "../common/icon";
 import { LanguageSelect } from "../common/language-select";
 import { MarkdownTextArea } from "../common/markdown-textarea";
+import NavigationPrompt from "../common/navigation-prompt";
 import { SearchableSelect } from "../common/searchable-select";
 import { PostListings } from "./post-listings";
 
@@ -51,16 +44,18 @@ const MAX_POST_TITLE_LENGTH = 200;
 
 interface PostFormProps {
   post_view?: PostView; // If a post is given, that means this is an edit
+  crossPosts?: PostView[];
   allLanguages: Language[];
   siteLanguages: number[];
   params?: PostFormParams;
-  onCancel?(): any;
-  onCreate?(post: PostView): any;
-  onEdit?(post: PostView): any;
+  onCancel?(): void;
+  onCreate?(form: CreatePost): void;
+  onEdit?(form: EditPost): void;
   enableNsfw?: boolean;
   enableDownvotes?: boolean;
   selectedCommunityChoice?: Choice;
   onSelectCommunity?: (choice: Choice) => void;
+  initialCommunities?: CommunityView[];
 }
 
 interface PostFormState {
@@ -73,25 +68,29 @@ interface PostFormState {
     community_id?: number;
     honeypot?: string;
   };
-  suggestedTitle?: string;
-  suggestedPosts?: PostView[];
-  crossPosts?: PostView[];
   loading: boolean;
+  suggestedPostsRes: RequestState<SearchResponse>;
+  metadataRes: RequestState<GetSiteMetadataResponse>;
   imageLoading: boolean;
+  imageDeleteUrl: string;
   communitySearchLoading: boolean;
   communitySearchOptions: Choice[];
   previewMode: boolean;
+  submitted: boolean;
 }
 
 export class PostForm extends Component<PostFormProps, PostFormState> {
-  private subscription?: Subscription;
   state: PostFormState = {
+    suggestedPostsRes: { state: "empty" },
+    metadataRes: { state: "empty" },
     form: {},
     loading: false,
     imageLoading: false,
+    imageDeleteUrl: "",
     communitySearchLoading: false,
     previewMode: false,
     communitySearchOptions: [],
+    submitted: false,
   };
 
   constructor(props: PostFormProps, context: any) {
@@ -102,39 +101,52 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     this.handleLanguageChange = this.handleLanguageChange.bind(this);
     this.handleCommunitySelect = this.handleCommunitySelect.bind(this);
 
-    this.parseMessage = this.parseMessage.bind(this);
-    this.subscription = wsSubscribe(this.parseMessage);
+    const { post_view, selectedCommunityChoice, params } = this.props;
 
     // Means its an edit
-    const pv = this.props.post_view;
-    if (pv) {
+    if (post_view) {
       this.state = {
         ...this.state,
         form: {
-          body: pv.post.body,
-          name: pv.post.name,
-          community_id: pv.community.id,
-          url: pv.post.url,
-          nsfw: pv.post.nsfw,
-          language_id: pv.post.language_id,
+          body: post_view.post.body,
+          name: post_view.post.name,
+          community_id: post_view.community.id,
+          url: post_view.post.url,
+          nsfw: post_view.post.nsfw,
+          language_id: post_view.post.language_id,
         },
       };
-    }
-
-    const selectedCommunityChoice = this.props.selectedCommunityChoice;
-
-    if (selectedCommunityChoice) {
+    } else if (selectedCommunityChoice) {
       this.state = {
         ...this.state,
         form: {
           ...this.state.form,
           community_id: getIdFromString(selectedCommunityChoice.value),
         },
-        communitySearchOptions: [selectedCommunityChoice],
+        communitySearchOptions: [selectedCommunityChoice]
+          .concat(
+            this.props.initialCommunities?.map(
+              ({ community: { id, title } }) => ({
+                label: title,
+                value: id.toString(),
+              })
+            ) ?? []
+          )
+          .filter(option => option.value !== selectedCommunityChoice.value),
+      };
+    } else {
+      this.state = {
+        ...this.state,
+        communitySearchOptions:
+          this.props.initialCommunities?.map(
+            ({ community: { id, title } }) => ({
+              label: title,
+              value: id.toString(),
+            })
+          ) ?? [],
       };
     }
 
-    const params = this.props.params;
     if (params) {
       this.state = {
         ...this.state,
@@ -155,373 +167,423 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     }
   }
 
-  componentDidUpdate() {
-    if (
-      !this.state.loading &&
-      (this.state.form.name || this.state.form.url || this.state.form.body)
-    ) {
-      window.onbeforeunload = () => true;
-    } else {
-      window.onbeforeunload = null;
+  componentWillReceiveProps(
+    nextProps: Readonly<{ children?: InfernoNode } & PostFormProps>
+  ): void {
+    if (this.props != nextProps) {
+      this.setState(
+        s => (
+          (s.form.community_id = getIdFromString(
+            nextProps.selectedCommunityChoice?.value
+          )),
+          s
+        )
+      );
     }
   }
 
-  componentWillUnmount() {
-    this.subscription?.unsubscribe();
-    /* this.choices && this.choices.destroy(); */
-    window.onbeforeunload = null;
-  }
-
-  static getDerivedStateFromProps(
-    { selectedCommunityChoice }: PostFormProps,
-    { form, ...restState }: PostFormState
-  ) {
-    return {
-      ...restState,
-      form: {
-        ...form,
-        community_id: getIdFromString(selectedCommunityChoice?.value),
-      },
-    };
-  }
-
   render() {
-    let firstLang = this.state.form.language_id;
-    let selectedLangs = firstLang ? Array.of(firstLang) : undefined;
+    const firstLang = this.state.form.language_id;
+    const selectedLangs = firstLang ? Array.of(firstLang) : undefined;
+
+    const url = this.state.form.url;
 
-    let url = this.state.form.url;
+    // TODO
+    // const promptCheck =
+    // !!this.state.form.name || !!this.state.form.url || !!this.state.form.body;
+    // <Prompt when={promptCheck} message={i18n.t("block_leaving")} />
     return (
-      <div>
-        <Prompt
+      <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
+        <NavigationPrompt
           when={
-            !this.state.loading &&
-            (this.state.form.name ||
+            !!(
+              this.state.form.name ||
               this.state.form.url ||
-              this.state.form.body)
+              this.state.form.body
+            ) && !this.state.submitted
           }
-          message={i18n.t("block_leaving")}
         />
-        <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
-          <div className="form-group row">
-            <label className="col-sm-2 col-form-label" htmlFor="post-url">
-              {i18n.t("url")}
-            </label>
-            <div className="col-sm-10">
+        <div className="form-group row">
+          <label className="col-sm-2 col-form-label" htmlFor="post-url">
+            {i18n.t("url")}
+          </label>
+          <div className="col-sm-10">
+            <input
+              type="url"
+              id="post-url"
+              className="form-control"
+              value={this.state.form.url}
+              onInput={linkEvent(this, this.handlePostUrlChange)}
+              onPaste={linkEvent(this, this.handleImageUploadPaste)}
+            />
+            {this.renderSuggestedTitleCopy()}
+            <form>
+              <label
+                htmlFor="file-upload"
+                className={`${
+                  UserService.Instance.myUserInfo && "pointer"
+                } d-inline-block float-right text-muted font-weight-bold`}
+                data-tippy-content={i18n.t("upload_image")}
+              >
+                <Icon icon="image" classes="icon-inline" />
+              </label>
               <input
-                type="url"
-                id="post-url"
-                className="form-control"
-                value={this.state.form.url}
-                onInput={linkEvent(this, this.handlePostUrlChange)}
-                onPaste={linkEvent(this, this.handleImageUploadPaste)}
+                id="file-upload"
+                type="file"
+                accept="image/*,video/*"
+                name="file"
+                className="d-none"
+                disabled={!UserService.Instance.myUserInfo}
+                onChange={linkEvent(this, this.handleImageUpload)}
               />
-              {this.state.suggestedTitle && (
-                <div
-                  className="mt-1 text-muted small font-weight-bold pointer"
-                  role="button"
-                  onClick={linkEvent(this, this.copySuggestedTitle)}
+            </form>
+            {url && validURL(url) && (
+              <div>
+                <a
+                  href={`${webArchiveUrl}/save/${encodeURIComponent(url)}`}
+                  className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
+                  rel={relTags}
                 >
-                  {i18n.t("copy_suggested_title", { title: "" })}{" "}
-                  {this.state.suggestedTitle}
-                </div>
-              )}
-              <form>
-                <label
-                  htmlFor="file-upload"
-                  className={`${
-                    UserService.Instance.myUserInfo && "pointer"
-                  } d-inline-block float-right text-muted font-weight-bold`}
-                  data-tippy-content={i18n.t("upload_image")}
+                  archive.org {i18n.t("archive_link")}
+                </a>
+                <a
+                  href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
+                    url
+                  )}`}
+                  className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
+                  rel={relTags}
                 >
-                  <Icon icon="image" classes="icon-inline" />
-                </label>
-                <input
-                  id="file-upload"
-                  type="file"
-                  accept="image/*,video/*"
-                  name="file"
-                  className="d-none"
-                  disabled={!UserService.Instance.myUserInfo}
-                  onChange={linkEvent(this, this.handleImageUpload)}
-                />
-              </form>
-              {url && validURL(url) && (
-                <div>
-                  <a
-                    href={`${webArchiveUrl}/save/${encodeURIComponent(url)}`}
-                    className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
-                    rel={relTags}
-                  >
-                    archive.org {i18n.t("archive_link")}
-                  </a>
-                  <a
-                    href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
-                      url
-                    )}`}
-                    className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
-                    rel={relTags}
-                  >
-                    ghostarchive.org {i18n.t("archive_link")}
-                  </a>
-                  <a
-                    href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
-                      url
-                    )}`}
-                    className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
-                    rel={relTags}
-                  >
-                    archive.today {i18n.t("archive_link")}
-                  </a>
+                  ghostarchive.org {i18n.t("archive_link")}
+                </a>
+                <a
+                  href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
+                    url
+                  )}`}
+                  className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
+                  rel={relTags}
+                >
+                  archive.today {i18n.t("archive_link")}
+                </a>
+              </div>
+            )}
+            {this.state.imageLoading && <Spinner />}
+            {url && isImage(url) && (
+              <img src={url} className="img-fluid" alt="" />
+            )}
+            {this.state.imageDeleteUrl && (
+              <button
+                className="btn btn-danger btn-sm mt-2"
+                onClick={linkEvent(this, this.handleImageDelete)}
+                aria-label={i18n.t("delete")}
+                data-tippy-content={i18n.t("delete")}
+              >
+                <Icon icon="x" classes="icon-inline mr-1" />
+                {capitalizeFirstLetter(i18n.t("delete"))}
+              </button>
+            )}
+            {this.props.crossPosts && this.props.crossPosts.length > 0 && (
+              <>
+                <div className="my-1 text-muted small font-weight-bold">
+                  {i18n.t("cross_posts")}
                 </div>
-              )}
-              {this.state.imageLoading && <Spinner />}
-              {url && isImage(url) && (
-                <img src={url} className="img-fluid" alt="" />
-              )}
-              {this.state.crossPosts && this.state.crossPosts.length > 0 && (
-                <>
-                  <div className="my-1 text-muted small font-weight-bold">
-                    {i18n.t("cross_posts")}
-                  </div>
-                  <PostListings
-                    showCommunity
-                    posts={this.state.crossPosts}
-                    enableDownvotes={this.props.enableDownvotes}
-                    enableNsfw={this.props.enableNsfw}
-                    allLanguages={this.props.allLanguages}
-                    siteLanguages={this.props.siteLanguages}
-                  />
-                </>
-              )}
-            </div>
+                <PostListings
+                  showCommunity
+                  posts={this.props.crossPosts}
+                  enableDownvotes={this.props.enableDownvotes}
+                  enableNsfw={this.props.enableNsfw}
+                  allLanguages={this.props.allLanguages}
+                  siteLanguages={this.props.siteLanguages}
+                  viewOnly
+                  // All of these are unused, since its view only
+                  onPostEdit={() => {}}
+                  onPostVote={() => {}}
+                  onPostReport={() => {}}
+                  onBlockPerson={() => {}}
+                  onLockPost={() => {}}
+                  onDeletePost={() => {}}
+                  onRemovePost={() => {}}
+                  onSavePost={() => {}}
+                  onFeaturePost={() => {}}
+                  onPurgePerson={() => {}}
+                  onPurgePost={() => {}}
+                  onBanPersonFromCommunity={() => {}}
+                  onBanPerson={() => {}}
+                  onAddModToCommunity={() => {}}
+                  onAddAdmin={() => {}}
+                  onTransferCommunity={() => {}}
+                />
+              </>
+            )}
+          </div>
+        </div>
+        <div className="form-group row">
+          <label className="col-sm-2 col-form-label" htmlFor="post-title">
+            {i18n.t("title")}
+          </label>
+          <div className="col-sm-10">
+            <textarea
+              value={this.state.form.name}
+              id="post-title"
+              onInput={linkEvent(this, this.handlePostNameChange)}
+              className={`form-control ${
+                !validTitle(this.state.form.name) && "is-invalid"
+              }`}
+              required
+              rows={1}
+              minLength={3}
+              maxLength={MAX_POST_TITLE_LENGTH}
+            />
+            {!validTitle(this.state.form.name) && (
+              <div className="invalid-feedback">
+                {i18n.t("invalid_post_title")}
+              </div>
+            )}
+            {this.renderSuggestedPosts()}
+          </div>
+        </div>
+
+        <div className="form-group row">
+          <label className="col-sm-2 col-form-label">{i18n.t("body")}</label>
+          <div className="col-sm-10">
+            <MarkdownTextArea
+              initialContent={this.state.form.body}
+              onContentChange={this.handlePostBodyChange}
+              allLanguages={this.props.allLanguages}
+              siteLanguages={this.props.siteLanguages}
+              hideNavigationWarnings
+            />
           </div>
+        </div>
+        {!this.props.post_view && (
           <div className="form-group row">
-            <label className="col-sm-2 col-form-label" htmlFor="post-title">
-              {i18n.t("title")}
+            <label className="col-sm-2 col-form-label" htmlFor="post-community">
+              {i18n.t("community")}
             </label>
             <div className="col-sm-10">
-              <textarea
-                value={this.state.form.name}
-                id="post-title"
-                onInput={linkEvent(this, this.handlePostNameChange)}
-                className={`form-control ${
-                  !validTitle(this.state.form.name) && "is-invalid"
-                }`}
-                required
-                rows={1}
-                minLength={3}
-                maxLength={MAX_POST_TITLE_LENGTH}
+              <SearchableSelect
+                id="post-community"
+                value={this.state.form.community_id}
+                options={[
+                  {
+                    label: i18n.t("select_a_community"),
+                    value: "",
+                    disabled: true,
+                  } as Choice,
+                ].concat(this.state.communitySearchOptions)}
+                loading={this.state.communitySearchLoading}
+                onChange={this.handleCommunitySelect}
+                onSearch={this.handleCommunitySearch}
               />
-              {!validTitle(this.state.form.name) && (
-                <div className="invalid-feedback">
-                  {i18n.t("invalid_post_title")}
-                </div>
-              )}
-              {this.state.suggestedPosts &&
-                this.state.suggestedPosts.length > 0 && (
-                  <>
-                    <div className="my-1 text-muted small font-weight-bold">
-                      {i18n.t("related_posts")}
-                    </div>
-                    <PostListings
-                      showCommunity
-                      posts={this.state.suggestedPosts}
-                      enableDownvotes={this.props.enableDownvotes}
-                      enableNsfw={this.props.enableNsfw}
-                      allLanguages={this.props.allLanguages}
-                      siteLanguages={this.props.siteLanguages}
-                    />
-                  </>
-                )}
             </div>
           </div>
-
+        )}
+        {this.props.enableNsfw && (
           <div className="form-group row">
-            <label className="col-sm-2 col-form-label">{i18n.t("body")}</label>
+            <legend className="col-form-label col-sm-2 pt-0">
+              {i18n.t("nsfw")}
+            </legend>
             <div className="col-sm-10">
-              <MarkdownTextArea
-                initialContent={this.state.form.body}
-                onContentChange={this.handlePostBodyChange}
-                allLanguages={this.props.allLanguages}
-                siteLanguages={this.props.siteLanguages}
-              />
-            </div>
-          </div>
-          {!this.props.post_view && (
-            <div className="form-group row">
-              <label
-                className="col-sm-2 col-form-label"
-                htmlFor="post-community"
-              >
-                {i18n.t("community")}
-              </label>
-              <div className="col-sm-10">
-                <SearchableSelect
-                  id="post-community"
-                  value={this.state.form.community_id}
-                  options={[
-                    {
-                      label: i18n.t("select_a_community"),
-                      value: "",
-                      disabled: true,
-                    } as Choice,
-                  ].concat(this.state.communitySearchOptions)}
-                  loading={this.state.communitySearchLoading}
-                  onChange={this.handleCommunitySelect}
-                  onSearch={this.handleCommunitySearch}
+              <div className="form-check">
+                <input
+                  className="form-check-input position-static"
+                  id="post-nsfw"
+                  type="checkbox"
+                  checked={this.state.form.nsfw}
+                  onChange={linkEvent(this, this.handlePostNsfwChange)}
                 />
               </div>
             </div>
-          )}
-          {this.props.enableNsfw && (
-            <div className="form-group row">
-              <legend className="col-form-label col-sm-2 pt-0">
-                {i18n.t("nsfw")}
-              </legend>
-              <div className="col-sm-10">
-                <div className="form-check">
-                  <input
-                    className="form-check-input position-static"
-                    id="post-nsfw"
-                    type="checkbox"
-                    checked={this.state.form.nsfw}
-                    onChange={linkEvent(this, this.handlePostNsfwChange)}
-                  />
-                </div>
-              </div>
-            </div>
-          )}
-          <LanguageSelect
-            allLanguages={this.props.allLanguages}
-            siteLanguages={this.props.siteLanguages}
-            selectedLanguageIds={selectedLangs}
-            multiple={false}
-            onChange={this.handleLanguageChange}
-          />
-          <input
-            tabIndex={-1}
-            autoComplete="false"
-            name="a_password"
-            type="text"
-            className="form-control honeypot"
-            id="register-honey"
-            value={this.state.form.honeypot}
-            onInput={linkEvent(this, this.handleHoneyPotChange)}
-          />
-          <div className="form-group row">
-            <div className="col-sm-10">
+          </div>
+        )}
+        <LanguageSelect
+          allLanguages={this.props.allLanguages}
+          siteLanguages={this.props.siteLanguages}
+          selectedLanguageIds={selectedLangs}
+          multiple={false}
+          onChange={this.handleLanguageChange}
+        />
+        <input
+          tabIndex={-1}
+          autoComplete="false"
+          name="a_password"
+          type="text"
+          className="form-control honeypot"
+          id="register-honey"
+          value={this.state.form.honeypot}
+          onInput={linkEvent(this, this.handleHoneyPotChange)}
+        />
+        <div className="form-group row">
+          <div className="col-sm-10">
+            <button
+              disabled={!this.state.form.community_id || this.state.loading}
+              type="submit"
+              className="btn btn-secondary mr-2"
+            >
+              {this.state.loading ? (
+                <Spinner />
+              ) : this.props.post_view ? (
+                capitalizeFirstLetter(i18n.t("save"))
+              ) : (
+                capitalizeFirstLetter(i18n.t("create"))
+              )}
+            </button>
+            {this.props.post_view && (
               <button
-                disabled={!this.state.form.community_id || this.state.loading}
-                type="submit"
-                className="btn btn-secondary mr-2"
+                type="button"
+                className="btn btn-secondary"
+                onClick={linkEvent(this, this.handleCancel)}
               >
-                {this.state.loading ? (
-                  <Spinner />
-                ) : this.props.post_view ? (
-                  capitalizeFirstLetter(i18n.t("save"))
-                ) : (
-                  capitalizeFirstLetter(i18n.t("create"))
-                )}
+                {i18n.t("cancel")}
               </button>
-              {this.props.post_view && (
-                <button
-                  type="button"
-                  className="btn btn-secondary"
-                  onClick={linkEvent(this, this.handleCancel)}
-                >
-                  {i18n.t("cancel")}
-                </button>
-              )}
-            </div>
+            )}
           </div>
-        </form>
-      </div>
+        </div>
+      </form>
     );
   }
 
-  handlePostSubmit(i: PostForm, event: any) {
-    event.preventDefault();
+  renderSuggestedTitleCopy() {
+    switch (this.state.metadataRes.state) {
+      case "loading":
+        return <Spinner />;
+      case "success": {
+        const suggestedTitle = this.state.metadataRes.data.metadata.title;
+
+        return (
+          suggestedTitle && (
+            <div
+              className="mt-1 text-muted small font-weight-bold pointer"
+              role="button"
+              onClick={linkEvent(
+                { i: this, suggestedTitle },
+                this.copySuggestedTitle
+              )}
+            >
+              {i18n.t("copy_suggested_title", { title: "" })} {suggestedTitle}
+            </div>
+          )
+        );
+      }
+    }
+  }
 
-    i.setState({ loading: true });
+  renderSuggestedPosts() {
+    switch (this.state.suggestedPostsRes.state) {
+      case "loading":
+        return <Spinner />;
+      case "success": {
+        const suggestedPosts = this.state.suggestedPostsRes.data.posts;
+
+        return (
+          suggestedPosts &&
+          suggestedPosts.length > 0 && (
+            <>
+              <div className="my-1 text-muted small font-weight-bold">
+                {i18n.t("related_posts")}
+              </div>
+              <PostListings
+                showCommunity
+                posts={suggestedPosts}
+                enableDownvotes={this.props.enableDownvotes}
+                enableNsfw={this.props.enableNsfw}
+                allLanguages={this.props.allLanguages}
+                siteLanguages={this.props.siteLanguages}
+                viewOnly
+                // All of these are unused, since its view only
+                onPostEdit={() => {}}
+                onPostVote={() => {}}
+                onPostReport={() => {}}
+                onBlockPerson={() => {}}
+                onLockPost={() => {}}
+                onDeletePost={() => {}}
+                onRemovePost={() => {}}
+                onSavePost={() => {}}
+                onFeaturePost={() => {}}
+                onPurgePerson={() => {}}
+                onPurgePost={() => {}}
+                onBanPersonFromCommunity={() => {}}
+                onBanPerson={() => {}}
+                onAddModToCommunity={() => {}}
+                onAddAdmin={() => {}}
+                onTransferCommunity={() => {}}
+              />
+            </>
+          )
+        );
+      }
+    }
+  }
 
+  handlePostSubmit(i: PostForm, event: any) {
+    event.preventDefault();
     // Coerce empty url string to undefined
-    if ((i.state.form.url ?? "blank") === "") {
+    if ((i.state.form.url ?? "") === "") {
       i.setState(s => ((s.form.url = undefined), s));
     }
+    i.setState({ loading: true, submitted: true });
+    const auth = myAuthRequired();
 
-    let pForm = i.state.form;
-    let pv = i.props.post_view;
-    let auth = myAuth();
-    if (auth) {
-      if (pv) {
-        let form: EditPost = {
-          name: pForm.name,
-          url: pForm.url,
-          body: pForm.body,
-          nsfw: pForm.nsfw,
-          post_id: pv.post.id,
-          language_id: pv.post.language_id,
-          auth,
-        };
-        WebSocketService.Instance.send(wsClient.editPost(form));
-      } else {
-        if (pForm.name && pForm.community_id) {
-          let form: CreatePost = {
-            name: pForm.name,
-            community_id: pForm.community_id,
-            url: pForm.url,
-            body: pForm.body,
-            nsfw: pForm.nsfw,
-            language_id: pForm.language_id,
-            honeypot: pForm.honeypot,
-            auth,
-          };
-          WebSocketService.Instance.send(wsClient.createPost(form));
-        }
-      }
+    const pForm = i.state.form;
+    const pv = i.props.post_view;
+
+    if (pv) {
+      i.props.onEdit?.({
+        name: pForm.name,
+        url: pForm.url,
+        body: pForm.body,
+        nsfw: pForm.nsfw,
+        post_id: pv.post.id,
+        language_id: pForm.language_id,
+        auth,
+      });
+    } else if (pForm.name && pForm.community_id) {
+      i.props.onCreate?.({
+        name: pForm.name,
+        community_id: pForm.community_id,
+        url: pForm.url,
+        body: pForm.body,
+        nsfw: pForm.nsfw,
+        language_id: pForm.language_id,
+        honeypot: pForm.honeypot,
+        auth,
+      });
     }
   }
 
-  copySuggestedTitle(i: PostForm) {
-    let sTitle = i.state.suggestedTitle;
+  copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) {
+    const sTitle = d.suggestedTitle;
     if (sTitle) {
-      i.setState(
+      d.i.setState(
         s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s)
       );
-      i.setState({ suggestedTitle: undefined });
+      d.i.setState({ suggestedPostsRes: { state: "empty" } });
       setTimeout(() => {
-        let textarea: any = document.getElementById("post-title");
+        const textarea: any = document.getElementById("post-title");
         autosize.update(textarea);
       }, 10);
     }
   }
 
   handlePostUrlChange(i: PostForm, event: any) {
-    i.setState(s => ((s.form.url = event.target.value), s));
+    const url = event.target.value;
+
+    i.setState({
+      form: {
+        url,
+      },
+      imageDeleteUrl: "",
+    });
+
     i.fetchPageTitle();
   }
 
-  fetchPageTitle() {
-    let url = this.state.form.url;
+  async fetchPageTitle() {
+    const url = this.state.form.url;
     if (url && validURL(url)) {
-      let form: Search = {
-        q: url,
-        type_: "Url",
-        sort: "TopAll",
-        listing_type: "All",
-        page: 1,
-        limit: trendingFetchLimit,
-        auth: myAuth(false),
-      };
-
-      WebSocketService.Instance.send(wsClient.search(form));
-
-      // Fetch the page title
-      getSiteMetadata(url).then(d => {
-        this.setState({ suggestedTitle: d.metadata.title });
+      this.setState({ metadataRes: { state: "loading" } });
+      this.setState({
+        metadataRes: await HttpService.client.getSiteMetadata({ url }),
       });
-    } else {
-      this.setState({ suggestedTitle: undefined, crossPosts: undefined });
     }
   }
 
@@ -530,23 +592,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     i.fetchSimilarPosts();
   }
 
-  fetchSimilarPosts() {
-    let q = this.state.form.name;
+  async fetchSimilarPosts() {
+    const q = this.state.form.name;
     if (q && q !== "") {
-      let form: Search = {
-        q,
-        type_: "Posts",
-        sort: "TopAll",
-        listing_type: "All",
-        community_id: this.state.form.community_id,
-        page: 1,
-        limit: trendingFetchLimit,
-        auth: myAuth(false),
-      };
-
-      WebSocketService.Instance.send(wsClient.search(form));
-    } else {
-      this.setState({ suggestedPosts: undefined });
+      this.setState({ suggestedPostsRes: { state: "loading" } });
+      this.setState({
+        suggestedPostsRes: await HttpService.client.search({
+          q,
+          type_: "Posts",
+          sort: "TopAll",
+          listing_type: "All",
+          community_id: this.state.form.community_id,
+          page: 1,
+          limit: trendingFetchLimit,
+          auth: myAuth(),
+        }),
+      });
     }
   }
 
@@ -580,7 +641,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   }
 
   handleImageUploadPaste(i: PostForm, event: any) {
-    let image = event.clipboardData.files[0];
+    const image = event.clipboardData.files[0];
     if (image) {
       i.handleImageUpload(i, image);
     }
@@ -597,24 +658,39 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
 
     i.setState({ imageLoading: true });
 
-    uploadImage(file)
-      .then(res => {
-        console.log("pictrs upload:");
-        console.log(res);
-        if (res.msg === "ok") {
-          i.state.form.url = res.url;
-          i.setState({ imageLoading: false });
-          pictrsDeleteToast(file.name, res.delete_url as string);
+    HttpService.client.uploadImage({ image: file }).then(res => {
+      console.log("pictrs upload:");
+      console.log(res);
+      if (res.state === "success") {
+        if (res.data.msg === "ok") {
+          i.state.form.url = res.data.url;
+          i.setState({
+            imageLoading: false,
+            imageDeleteUrl: res.data.delete_url as string,
+          });
         } else {
-          i.setState({ imageLoading: false });
           toast(JSON.stringify(res), "danger");
         }
-      })
-      .catch(error => {
+      } else if (res.state === "failed") {
+        console.error(res.msg);
+        toast(res.msg, "danger");
         i.setState({ imageLoading: false });
-        console.error(error);
-        toast(error, "danger");
-      });
+      }
+    });
+  }
+
+  handleImageDelete(i: PostForm) {
+    const { imageDeleteUrl } = i.state;
+
+    fetch(imageDeleteUrl);
+
+    i.setState({
+      imageDeleteUrl: "",
+      imageLoading: false,
+      form: {
+        url: "",
+      },
+    });
   }
 
   handleCommunitySearch = debounce(async (text: string) => {
@@ -628,9 +704,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     }
 
     if (text.length > 0) {
-      newOptions.push(
-        ...(await fetchCommunities(text)).communities.map(communityToChoice)
-      );
+      newOptions.push(...(await fetchCommunities(text)).map(communityToChoice));
 
       this.setState({
         communitySearchOptions: newOptions,
@@ -647,35 +721,4 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       this.props.onSelectCommunity(choice);
     }
   }
-
-  parseMessage(msg: any) {
-    let mui = UserService.Instance.myUserInfo;
-    let op = wsUserOp(msg);
-    console.log(msg);
-    if (msg.error) {
-      // Errors handled by top level pages
-      // toast(i18n.t(msg.error), "danger");
-      this.setState({ loading: false });
-      return;
-    } else if (op == UserOperation.CreatePost) {
-      let data = wsJsonToRes<PostResponse>(msg);
-      if (data.post_view.creator.id == mui?.local_user_view.person.id) {
-        this.props.onCreate?.(data.post_view);
-      }
-    } else if (op == UserOperation.EditPost) {
-      let data = wsJsonToRes<PostResponse>(msg);
-      if (data.post_view.creator.id == mui?.local_user_view.person.id) {
-        this.setState({ loading: false });
-        this.props.onEdit?.(data.post_view);
-      }
-    } else if (op == UserOperation.Search) {
-      let data = wsJsonToRes<SearchResponse>(msg);
-
-      if (data.type_ == "Posts") {
-        this.setState({ suggestedPosts: data.posts });
-      } else if (data.type_ == "Url") {
-        this.setState({ crossPosts: data.posts });
-      }
-    }
-  }
 }