]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/home/emojis-form.tsx
Merge branch 'main' into fix/fix-badges-spacing-componentize
[lemmy-ui.git] / src / shared / components / home / emojis-form.tsx
index 73fceac573250c97905884bf6c31948485826b71..3ad8cc05a77742923a1a978b615673b14271d3a0 100644 (file)
+import { myAuthRequired, setIsoData } from "@utils/app";
+import { capitalizeFirstLetter } from "@utils/helpers";
 import { Component, linkEvent } from "inferno";
-import { GetSiteResponse, UserOperation, wsJsonToRes, wsUserOp } from "lemmy-js-client";
-import { CreateCustomEmoji, CustomEmojiResponse, DeleteCustomEmoji, DeleteCustomEmojiResponse, EditCustomEmoji } from "lemmy-js-client/dist/interfaces/api/custom_emoji";
-import { WebSocketService } from "../../services";
-import { i18n } from "../../i18next";
-import { customEmojisLookup, isBrowser, myAuth, removeFromEmojiDataModel, setIsoData, toast, updateEmojiDataModel, wsClient, wsSubscribe } from "../../utils";
+import {
+  CreateCustomEmoji,
+  DeleteCustomEmoji,
+  EditCustomEmoji,
+  GetSiteResponse,
+} from "lemmy-js-client";
+import { customEmojisLookup } from "../../markdown";
+import { HttpService, I18NextService } from "../../services";
+import { pictrsDeleteToast, toast } from "../../toast";
 import { EmojiMart } from "../common/emoji-mart";
 import { HtmlTags } from "../common/html-tags";
-import { Icon } from "../common/icon";
-import { Subscription } from "rxjs";
-import { pictrsUri } from "../../env";
+import { Icon, Spinner } from "../common/icon";
 import { Paginator } from "../common/paginator";
 
+interface EmojiFormProps {
+  onEdit(form: EditCustomEmoji): void;
+  onCreate(form: CreateCustomEmoji): void;
+  onDelete(form: DeleteCustomEmoji): void;
+}
+
 interface EmojiFormState {
-    siteRes: GetSiteResponse;
-    customEmojis: CustomEmojiViewForm[];
-    loading: boolean;
-    page: number;
+  siteRes: GetSiteResponse;
+  customEmojis: CustomEmojiViewForm[];
+  page: number;
 }
 
 interface CustomEmojiViewForm {
-    id: number;
-    category: string;
-    shortcode: string;
-    image_url: string;
-    alt_text: string;
-    keywords: string;
-    changed: boolean;
-    page: number;
+  id: number;
+  category: string;
+  shortcode: string;
+  image_url: string;
+  alt_text: string;
+  keywords: string;
+  changed: boolean;
+  page: number;
+  loading: boolean;
 }
 
-export class EmojiForm extends Component<any, EmojiFormState> {
-    private isoData = setIsoData(this.context);
-    private subscription: Subscription | undefined;
-    private itemsPerPage = 15;
-    private emptyState: EmojiFormState = {
-        loading: false,
-        siteRes: this.isoData.site_res,
-        customEmojis: this.isoData.site_res.custom_emojis.map((x, index) =>
-        ({
-            id: x.custom_emoji.id,
-            category: x.custom_emoji.category,
-            shortcode: x.custom_emoji.shortcode,
-            image_url: x.custom_emoji.image_url,
-            alt_text: x.custom_emoji.alt_text,
-            keywords: x.keywords.map(x => x.keyword).join(" "),
-            changed: false,
-            page: 1 + (Math.floor(index / this.itemsPerPage))
-        })),
-        page: 1
-    };
-    state: EmojiFormState;
-    private scrollRef: any = {};
-    constructor(props: any, context: any) {
-        super(props, context);
-        this.state = this.emptyState;
+export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
+  private isoData = setIsoData(this.context);
+  private itemsPerPage = 15;
+  private emptyState: EmojiFormState = {
+    siteRes: this.isoData.site_res,
+    customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
+      id: x.custom_emoji.id,
+      category: x.custom_emoji.category,
+      shortcode: x.custom_emoji.shortcode,
+      image_url: x.custom_emoji.image_url,
+      alt_text: x.custom_emoji.alt_text,
+      keywords: x.keywords.map(x => x.keyword).join(" "),
+      changed: false,
+      page: 1 + Math.floor(index / this.itemsPerPage),
+      loading: false,
+    })),
+    page: 1,
+  };
+  state: EmojiFormState;
+  private scrollRef: any = {};
+  constructor(props: any, context: any) {
+    super(props, context);
+    this.state = this.emptyState;
 
-        this.handlePageChange = this.handlePageChange.bind(this);
-        this.parseMessage = this.parseMessage.bind(this);
-        this.handleEmojiClick = this.handleEmojiClick.bind(this);
-        this.subscription = wsSubscribe(this.parseMessage);
-    }
-    get documentTitle(): string {
-        return i18n.t("custom_emojis");
-    }
+    this.handlePageChange = this.handlePageChange.bind(this);
+    this.handleEmojiClick = this.handleEmojiClick.bind(this);
+  }
+  get documentTitle(): string {
+    return I18NextService.i18n.t("custom_emojis");
+  }
 
-    componentWillUnmount() {
-        if (isBrowser()) {
-            this.subscription?.unsubscribe();
-        }
-    }
+  render() {
+    return (
+      <div className="home-emojis-form col-12">
+        <HtmlTags
+          title={this.documentTitle}
+          path={this.context.router.route.match.url}
+        />
+        <h5 className="col-12">{I18NextService.i18n.t("custom_emojis")}</h5>
+        {customEmojisLookup.size > 0 && (
+          <div>
+            <EmojiMart
+              onEmojiClick={this.handleEmojiClick}
+              pickerOptions={this.configurePicker()}
+            ></EmojiMart>
+          </div>
+        )}
+        <div className="table-responsive">
+          <table
+            id="emojis_table"
+            className="table table-sm table-hover align-middle"
+          >
+            <thead className="pointer">
+              <tr>
+                <th>{I18NextService.i18n.t("column_emoji")}</th>
+                <th className="text-right">
+                  {I18NextService.i18n.t("column_shortcode")}
+                </th>
+                <th className="text-right">
+                  {I18NextService.i18n.t("column_category")}
+                </th>
+                <th className="text-right d-lg-table-cell d-none">
+                  {I18NextService.i18n.t("column_imageurl")}
+                </th>
+                <th className="text-right">
+                  {I18NextService.i18n.t("column_alttext")}
+                </th>
+                <th className="text-right d-lg-table-cell">
+                  {I18NextService.i18n.t("column_keywords")}
+                </th>
+                <th style="width:121px"></th>
+              </tr>
+            </thead>
+            <tbody>
+              {this.state.customEmojis
+                .slice(
+                  Number((this.state.page - 1) * this.itemsPerPage),
+                  Number(
+                    (this.state.page - 1) * this.itemsPerPage +
+                      this.itemsPerPage
+                  )
+                )
+                .map((cv, index) => (
+                  <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
+                    <td style="text-align:center;">
+                      {cv.image_url.length > 0 && (
+                        <img
+                          className="icon-emoji-admin"
+                          src={cv.image_url}
+                          alt={cv.alt_text}
+                        />
+                      )}
+                      {cv.image_url.length === 0 && (
+                        <label
+                          // TODO: Fix this linting violation
+                          // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
+                          tabIndex={0}
+                          className="btn btn-sm btn-secondary pointer"
+                          htmlFor={`file-uploader-${index}`}
+                          data-tippy-content={I18NextService.i18n.t(
+                            "upload_image"
+                          )}
+                        >
+                          {capitalizeFirstLetter(
+                            I18NextService.i18n.t("upload")
+                          )}
+                          <input
+                            name={`file-uploader-${index}`}
+                            id={`file-uploader-${index}`}
+                            type="file"
+                            accept="image/*"
+                            className="d-none"
+                            onChange={linkEvent(
+                              { form: this, index: index },
+                              this.handleImageUpload
+                            )}
+                          />
+                        </label>
+                      )}
+                    </td>
+                    <td className="text-right">
+                      <input
+                        type="text"
+                        placeholder="ShortCode"
+                        className="form-control"
+                        disabled={cv.id > 0}
+                        value={cv.shortcode}
+                        onInput={linkEvent(
+                          { form: this, index: index },
+                          this.handleEmojiShortCodeChange
+                        )}
+                      />
+                    </td>
+                    <td className="text-right">
+                      <input
+                        type="text"
+                        placeholder="Category"
+                        className="form-control"
+                        value={cv.category}
+                        onInput={linkEvent(
+                          { form: this, index: index },
+                          this.handleEmojiCategoryChange
+                        )}
+                      />
+                    </td>
+                    <td className="text-right d-lg-table-cell d-none">
+                      <input
+                        type="text"
+                        placeholder="Url"
+                        className="form-control"
+                        value={cv.image_url}
+                        onInput={linkEvent(
+                          { form: this, index: index, overrideValue: null },
+                          this.handleEmojiImageUrlChange
+                        )}
+                      />
+                    </td>
+                    <td className="text-right">
+                      <input
+                        type="text"
+                        placeholder="Alt Text"
+                        className="form-control"
+                        value={cv.alt_text}
+                        onInput={linkEvent(
+                          { form: this, index: index },
+                          this.handleEmojiAltTextChange
+                        )}
+                      />
+                    </td>
+                    <td className="text-right d-lg-table-cell">
+                      <input
+                        type="text"
+                        placeholder="Keywords"
+                        className="form-control"
+                        value={cv.keywords}
+                        onInput={linkEvent(
+                          { form: this, index: index },
+                          this.handleEmojiKeywordChange
+                        )}
+                      />
+                    </td>
+                    <td>
+                      <div>
+                        <span title={this.getEditTooltip(cv)}>
+                          <button
+                            className={
+                              (this.canEdit(cv)
+                                ? "text-success "
+                                : "text-muted ") + "btn btn-link btn-animate"
+                            }
+                            onClick={linkEvent(
+                              { i: this, cv: cv },
+                              this.handleEditEmojiClick
+                            )}
+                            data-tippy-content={I18NextService.i18n.t("save")}
+                            aria-label={I18NextService.i18n.t("save")}
+                            disabled={!this.canEdit(cv)}
+                          >
+                            {cv.loading ? (
+                              <Spinner />
+                            ) : (
+                              capitalizeFirstLetter(
+                                I18NextService.i18n.t("save")
+                              )
+                            )}
+                          </button>
+                        </span>
+                        <button
+                          className="btn btn-link btn-animate text-muted"
+                          onClick={linkEvent(
+                            { i: this, index: index, cv: cv },
+                            this.handleDeleteEmojiClick
+                          )}
+                          data-tippy-content={I18NextService.i18n.t("delete")}
+                          aria-label={I18NextService.i18n.t("delete")}
+                          disabled={cv.loading}
+                          title={I18NextService.i18n.t("delete")}
+                        >
+                          <Icon
+                            icon="trash"
+                            classes="icon-inline text-danger"
+                          />
+                        </button>
+                      </div>
+                    </td>
+                  </tr>
+                ))}
+            </tbody>
+          </table>
+          <br />
+          <button
+            className="btn btn-sm btn-secondary me-2"
+            onClick={linkEvent(this, this.handleAddEmojiClick)}
+          >
+            {I18NextService.i18n.t("add_custom_emoji")}
+          </button>
 
-    render() {
-        return (
-            <div className="col-12">
-                <HtmlTags
-                    title={this.documentTitle}
-                    path={this.context.router.route.match.url}
-                />
-                <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
-                {customEmojisLookup.size > 0 && <div>
-                    <EmojiMart onEmojiClick={this.handleEmojiClick} pickerOptions={this.configurePicker()}></EmojiMart>
-                </div>}
-                <div className="table-responsive">
-                    <table
-                        id="emojis_table"
-                        className="table table-sm table-hover"
-                    >
-                        <thead className="pointer">
-                            <tr>
-                                <th>{i18n.t("column_emoji")}</th>
-                                <th className="text-right">{i18n.t("column_shortcode")}</th>
-                                <th className="text-right">{i18n.t("column_category")}</th>
-                                <th className="text-right">{i18n.t("column_imageurl")}</th>
-                                <th className="text-right">{i18n.t("column_alttext")}</th>
-                                <th className="text-right d-none d-lg-table-cell">{i18n.t("column_keywords")}</th>
-                                <th style="width:121px"></th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {this.state.customEmojis.slice((this.state.page - 1) * this.itemsPerPage, ((this.state.page - 1) * this.itemsPerPage) + this.itemsPerPage)
-                                .map((cv, index) => (
-                                    <tr key={index} ref={e => this.scrollRef[cv.shortcode] = e}>
-                                        <td style="text-align:center;">
-                                            <label
-                                                htmlFor={index.toString()}
-                                                className="pointer text-muted small font-weight-bold"
-                                            >
-                                                {cv.image_url.length > 0 && <img className="icon-emoji-admin" src={cv.image_url} />}
-                                                {cv.image_url.length == 0 && <span className="btn btn-sm btn-secondary">Upload</span>}
-                                            </label>
-                                            <input
-                                                name={index.toString()}
-                                                id={index.toString()}
-                                                type="file"
-                                                accept="image/*"
-                                                className="d-none"
-                                                onChange={linkEvent({form: this, index: index},this.handleImageUpload)}
-                                            />
-                                        </td>
-                                        <td className="text-right">
-                                            <input
-                                                type="text"
-                                                placeholder="ShortCode"
-                                                className="form-control"
-                                                disabled={cv.id > 0}
-                                                value={cv.shortcode}
-                                                onInput={linkEvent({form: this, index: index},this.handleEmojiShortCodeChange)}
-                                            />
-                                        </td>
-                                        <td className="text-right">
-                                            <input
-                                                type="text"
-                                                placeholder="Category"
-                                                className="form-control"
-                                                value={cv.category}
-                                                onInput={linkEvent({form: this, index: index},this.handleEmojiCategoryChange)}
-                                            />
-                                        </td>
-                                        <td className="text-right">
-                                            <input
-                                                type="text"
-                                                placeholder="Url"
-                                                className="form-control"
-                                                value={cv.image_url}
-                                                onInput={linkEvent({form: this, index: index, overrideValue: null},this.handleEmojiImageUrlChange)}
-                                            />
-                                        </td>
-                                        <td className="text-right">
-                                            <input
-                                                type="text"
-                                                placeholder="Alt Text"
-                                                className="form-control"
-                                                value={cv.alt_text}
-                                                onInput={linkEvent({form: this, index: index},this.handleEmojiAltTextChange)}
-                                            />
-                                        </td>
-                                        <td className="text-right d-none d-lg-table-cell">
-                                            <input
-                                                type="text"
-                                                placeholder="Keywords"
-                                                className="form-control"
-                                                value={cv.keywords}
-                                                onInput={linkEvent({form: this, index: index},this.handleEmojiKeywordChange)}
-                                            />
-                                        </td>
-                                        <td>
-                                            <div>
-                                                <span
-                                                    title={this.getEditTooltip(cv)}>
-                                                    <button
-                                                        className={(cv.changed ? "text-success " : "text-muted ") + "btn btn-link btn-animate"}
-                                                        onClick={linkEvent({form: this, cv: cv},this.handleEditEmojiClick)}
-                                                        data-tippy-content={i18n.t("save")}
-                                                        aria-label={i18n.t("save")}
-                                                        disabled={this.state.loading || !this.canEdit(cv) || !cv.changed}
-                                                    >
-                                                        {/* <Icon
-                                                            icon="edit"
-                                                            classes={`icon-inline`}
-                                                        /> */}
-                                                        Save
-                                                    </button>
-                                                </span>
-                                                <button
-                                                    className="btn btn-link btn-animate text-muted"
-                                                    onClick={linkEvent({form: this, index: index, cv:cv},this.handleDeleteEmojiClick)}
-                                                    data-tippy-content={i18n.t("delete")}
-                                                    aria-label={i18n.t("delete")}
-                                                    disabled={this.state.loading}
-                                                    title={i18n.t("delete")}
-                                                >
-                                                    <Icon
-                                                        icon="trash"
-                                                        classes={`icon-inline text-danger`}
-                                                    />
-                                                </button>
-                                            </div>
-                                        </td>
-                                    </tr>
-                                ))}
-                        </tbody>
-                    </table>
-                    <br />
-                    <button
-                        className="btn btn-sm btn-secondary mr-2"
-                        onClick={linkEvent(this,this.handleAddEmojiClick)}
-                    >
-                        {i18n.t("add_custom_emoji")}
-                    </button>
+          <Paginator page={this.state.page} onChange={this.handlePageChange} />
+        </div>
+      </div>
+    );
+  }
 
-                    <Paginator
-                        page={this.state.page}
-                        onChange={this.handlePageChange}
-                    />
-                </div>
-            </div>
-        );
-    }
+  canEdit(cv: CustomEmojiViewForm) {
+    const noEmptyFields =
+      cv.alt_text.length > 0 &&
+      cv.category.length > 0 &&
+      cv.image_url.length > 0 &&
+      cv.shortcode.length > 0;
+    const noDuplicateShortCodes =
+      this.state.customEmojis.filter(
+        x => x.shortcode == cv.shortcode && x.id != cv.id
+      ).length == 0;
+    return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
+  }
 
-    canEdit(cv: CustomEmojiViewForm) {
-        const noEmptyFields = (cv.alt_text.length > 0 && cv.category.length > 0 && cv.image_url.length > 0 && cv.shortcode.length > 0);
-        const noDuplicateShortCodes = this.state.customEmojis.filter(x => x.shortcode == cv.shortcode && x.id != cv.id).length == 0;
-        return noEmptyFields && noDuplicateShortCodes;
-    }
+  getEditTooltip(cv: CustomEmojiViewForm) {
+    if (this.canEdit(cv)) return I18NextService.i18n.t("save");
+    else return I18NextService.i18n.t("custom_emoji_save_validation");
+  }
 
-    getEditTooltip(cv: CustomEmojiViewForm) {
-        if (this.canEdit(cv))
-            return i18n.t("save");
-        else
-            return i18n.t("custom_emoji_save_validation");
-    }
+  handlePageChange(page: number) {
+    this.setState({ page: page });
+  }
 
-    handlePageChange(page: number) {
+  handleEmojiClick(e: any) {
+    const view = customEmojisLookup.get(e.id);
+    if (view) {
+      const page = this.state.customEmojis.find(
+        x => x.id == view.custom_emoji.id
+      )?.page;
+      if (page) {
         this.setState({ page: page });
+        this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
+      }
     }
+  }
 
-    handleEmojiClick(e: any) {
-        const view = customEmojisLookup.get(e.id);
-        if (view) {
-            const page = this.state.customEmojis.find(x => x.id == view.custom_emoji.id)?.page;
-            if (page) {
-                this.setState({ page: page });
-                this.scrollRef[view.custom_emoji.shortcode].scrollIntoView()
-            }
-        }
-    }
-
-    handleEmojiCategoryChange(props: { form: EmojiForm, index: number }, event: any) {
-        let custom_emojis = [...props.form.state.customEmojis];
-        let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
-        let item = {
-            ... props.form.state.customEmojis[pagedIndex],
-            category: event.target.value,
-            changed: true,
-        }
-        custom_emojis[pagedIndex] = item;
-        props.form.setState({ customEmojis: custom_emojis });
-    }
+  handleEmojiCategoryChange(
+    props: { form: EmojiForm; index: number },
+    event: any
+  ) {
+    const custom_emojis = [...props.form.state.customEmojis];
+    const pagedIndex =
+      (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
+    const item = {
+      ...props.form.state.customEmojis[pagedIndex],
+      category: event.target.value,
+      changed: true,
+    };
+    custom_emojis[Number(pagedIndex)] = item;
+    props.form.setState({ customEmojis: custom_emojis });
+  }
 
-    handleEmojiShortCodeChange(props: { form: EmojiForm, index: number }, event: any) {
-        let custom_emojis = [...props.form.state.customEmojis];
-        let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
-        let item = {
-            ... props.form.state.customEmojis[pagedIndex],
-            shortcode: event.target.value,
-            changed: true,
-        }
-        custom_emojis[pagedIndex] = item;
-        props.form.setState({ customEmojis: custom_emojis });
-    }
+  handleEmojiShortCodeChange(
+    props: { form: EmojiForm; index: number },
+    event: any
+  ) {
+    const custom_emojis = [...props.form.state.customEmojis];
+    const pagedIndex =
+      (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
+    const item = {
+      ...props.form.state.customEmojis[pagedIndex],
+      shortcode: event.target.value,
+      changed: true,
+    };
+    custom_emojis[Number(pagedIndex)] = item;
+    props.form.setState({ customEmojis: custom_emojis });
+  }
 
-    handleEmojiImageUrlChange(props: { form: EmojiForm, index: number, overrideValue: string | null }, event: any) {
-        let custom_emojis = [...props.form.state.customEmojis];
-        let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
-        let item = {
-            ... props.form.state.customEmojis[pagedIndex],
-            image_url: props.overrideValue ?? event.target.value,
-            changed: true,
-        }
-        custom_emojis[pagedIndex] = item;
-        props.form.setState({ customEmojis: custom_emojis });
-    }
+  handleEmojiImageUrlChange(
+    {
+      form,
+      index,
+      overrideValue,
+    }: { form: EmojiForm; index: number; overrideValue: string | null },
+    event: any
+  ) {
+    form.setState(prevState => {
+      const custom_emojis = [...form.state.customEmojis];
+      const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
+      const item = {
+        ...form.state.customEmojis[pagedIndex],
+        image_url: overrideValue ?? event.target.value,
+        changed: true,
+      };
+      custom_emojis[Number(pagedIndex)] = item;
+      return {
+        ...prevState,
+        customEmojis: prevState.customEmojis.map((ce, i) =>
+          i === pagedIndex
+            ? {
+                ...ce,
+                image_url: overrideValue ?? event.target.value,
+                changed: true,
+                loading: false,
+              }
+            : ce
+        ),
+      };
+    });
+  }
 
-    handleEmojiAltTextChange(props: { form: EmojiForm, index: number }, event: any) {
-        let custom_emojis = [...props.form.state.customEmojis];
-        let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
-        let item = {
-            ... props.form.state.customEmojis[pagedIndex],
-            alt_text: event.target.value,
-            changed: true,
-        }
-        custom_emojis[pagedIndex] = item;
-        props.form.setState({ customEmojis: custom_emojis });
-    }
+  handleEmojiAltTextChange(
+    props: { form: EmojiForm; index: number },
+    event: any
+  ) {
+    const custom_emojis = [...props.form.state.customEmojis];
+    const pagedIndex =
+      (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
+    const item = {
+      ...props.form.state.customEmojis[pagedIndex],
+      alt_text: event.target.value,
+      changed: true,
+    };
+    custom_emojis[Number(pagedIndex)] = item;
+    props.form.setState({ customEmojis: custom_emojis });
+  }
 
-    handleEmojiKeywordChange(props: { form: EmojiForm, index: number }, event: any) {
-        let custom_emojis = [...props.form.state.customEmojis];
-        let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
-        let item = {
-            ... props.form.state.customEmojis[pagedIndex],
-            keywords: event.target.value,
-            changed: true,
-        }
-        custom_emojis[pagedIndex] = item;
-        props.form.setState({ customEmojis: custom_emojis });
-    }
+  handleEmojiKeywordChange(
+    props: { form: EmojiForm; index: number },
+    event: any
+  ) {
+    const custom_emojis = [...props.form.state.customEmojis];
+    const pagedIndex =
+      (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
+    const item = {
+      ...props.form.state.customEmojis[pagedIndex],
+      keywords: event.target.value,
+      changed: true,
+    };
+    custom_emojis[Number(pagedIndex)] = item;
+    props.form.setState({ customEmojis: custom_emojis });
+  }
 
-    handleDeleteEmojiClick(props: { form: EmojiForm, index: number, cv: CustomEmojiViewForm }) {
-        let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
-        if (props.cv.id != 0) {
-            const deleteForm: DeleteCustomEmoji = {
-                id: props.cv.id,
-                auth: myAuth() ?? ""
-            };
-            WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm));
-        }
-        else {
-            let custom_emojis = [...props.form.state.customEmojis];
-            custom_emojis.splice(pagedIndex, 1);
-            props.form.setState({ customEmojis: custom_emojis });
-        }
+  handleDeleteEmojiClick(d: {
+    i: EmojiForm;
+    index: number;
+    cv: CustomEmojiViewForm;
+  }) {
+    const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
+    if (d.cv.id != 0) {
+      d.i.props.onDelete({
+        id: d.cv.id,
+        auth: myAuthRequired(),
+      });
+    } else {
+      const custom_emojis = [...d.i.state.customEmojis];
+      custom_emojis.splice(Number(pagedIndex), 1);
+      d.i.setState({ customEmojis: custom_emojis });
     }
+  }
 
-    handleEditEmojiClick(props: { form: EmojiForm, cv: CustomEmojiViewForm }) {
-        const keywords = props.cv.keywords.split(" ").filter(x => x.length > 0) as string[];
-        const uniqueKeywords = Array.from(new Set(keywords));
-        if (props.cv.id != 0) {
-            const editForm: EditCustomEmoji = {
-                id: props.cv.id,
-                category: props.cv.category,
-                image_url: props.cv.image_url,
-                alt_text: props.cv.alt_text,
-                keywords: uniqueKeywords,
-                auth: myAuth() ?? ""
-            };
-            WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
-        }
-        else {
-            const createForm: CreateCustomEmoji = {
-                category: props.cv.category,
-                shortcode: props.cv.shortcode,
-                image_url: props.cv.image_url,
-                alt_text: props.cv.alt_text,
-                keywords: uniqueKeywords,
-                auth: myAuth() ?? ""
-            };
-            WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
-        }
+  handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
+    const keywords = d.cv.keywords
+      .split(" ")
+      .filter(x => x.length > 0) as string[];
+    const uniqueKeywords = Array.from(new Set(keywords));
+    if (d.cv.id !== 0) {
+      d.i.props.onEdit({
+        id: d.cv.id,
+        category: d.cv.category,
+        image_url: d.cv.image_url,
+        alt_text: d.cv.alt_text,
+        keywords: uniqueKeywords,
+        auth: myAuthRequired(),
+      });
+    } else {
+      d.i.props.onCreate({
+        category: d.cv.category,
+        shortcode: d.cv.shortcode,
+        image_url: d.cv.image_url,
+        alt_text: d.cv.alt_text,
+        keywords: uniqueKeywords,
+        auth: myAuthRequired(),
+      });
     }
+  }
 
-    handleAddEmojiClick(form: EmojiForm, event: any) {
-        event.preventDefault();
-        let custom_emojis = [...form.state.customEmojis];
-        const page = 1 + (Math.floor((form.state.customEmojis.length) / form.itemsPerPage))
-        let item: CustomEmojiViewForm = {
-            id: 0,
-            shortcode: "",
-            alt_text: "",
-            category: "",
-            image_url: "",
-            keywords: "",
-            changed: true,
-            page: page
-        }
-        custom_emojis.push(item);
-        form.setState({ customEmojis: custom_emojis, page: page });
-    }
+  handleAddEmojiClick(form: EmojiForm, event: any) {
+    event.preventDefault();
+    form.setState(prevState => {
+      const page =
+        1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
+      const item: CustomEmojiViewForm = {
+        id: 0,
+        shortcode: "",
+        alt_text: "",
+        category: "",
+        image_url: "",
+        keywords: "",
+        changed: false,
+        page: page,
+        loading: false,
+      };
 
-    async handleImageUpload(props: { form: EmojiForm, index: number }, event: any) {
-        event.preventDefault();
-        let file = event.target.files[0];
-        const formData = new FormData();
-        formData.append("images[]", file);
+      return {
+        ...prevState,
+        customEmojis: [...prevState.customEmojis, item],
+        page,
+      };
+    });
+  }
 
-        props.form.setState({ loading: true });
-        let res: any = await fetch(pictrsUri, {
-            method: "POST",
-            body: formData,
-        });
-        let data = await res.json();
-        props.form.setState({ loading: false });
-        if (data.msg != "ok") {
-            toast(JSON.stringify(data), "danger");
-        }
-        else {
-            let hash = data.files[0].file;
-            let url = `${pictrsUri}/${hash}`;
-            props.form.handleEmojiImageUrlChange({form: props.form, index: props.index, overrideValue: url}, event)
-        }
+  handleImageUpload(
+    { form, index }: { form: EmojiForm; index: number },
+    event: any
+  ) {
+    let file: any;
+    if (event.target) {
+      event.preventDefault();
+      file = event.target.files[0];
+    } else {
+      file = event;
     }
 
-    configurePicker(): any {
-        return {
-            data: { categories: [], emojis: [], aliases: [] },
-            maxFrequentRows: 0,
-            dynamicWidth: true,
-        };
-    }
+    form.setState(prevState => ({
+      ...prevState,
+      customEmojis: prevState.customEmojis.map((cv, i) =>
+        i === index ? { ...cv, loading: true } : cv
+      ),
+    }));
 
-    parseMessage(msg: any) {
-        let op = wsUserOp(msg);
-        console.log(msg);
-        if (msg.error) {
-            toast(i18n.t(msg.error), "danger");
-            this.context.router.history.push("/");
-            this.setState({ loading: false });
-            return;
-        }
-        else if (op == UserOperation.CreateCustomEmoji) {
-            let data = wsJsonToRes<CustomEmojiResponse>(msg);
-            const custom_emoji_view = data.custom_emoji;
-            updateEmojiDataModel(custom_emoji_view)
-            let currentEmojis = this.state.customEmojis;
-            let newEmojiIndex = currentEmojis.findIndex(x => x.shortcode == custom_emoji_view.custom_emoji.shortcode)
-            currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id;
-            currentEmojis[newEmojiIndex].changed = false;
-            this.setState({ customEmojis: currentEmojis });
-            toast(i18n.t("saved_emoji"));
-            this.setState({ loading: false });
-        }
-        else if (op == UserOperation.EditCustomEmoji) {
-            let data = wsJsonToRes<CustomEmojiResponse>(msg);
-            const custom_emoji_view = data.custom_emoji;
-            updateEmojiDataModel(data.custom_emoji)
-            let currentEmojis = this.state.customEmojis;
-            let newEmojiIndex = currentEmojis.findIndex(x => x.shortcode == custom_emoji_view.custom_emoji.shortcode)
-            currentEmojis[newEmojiIndex].changed = false;
-            this.setState({ customEmojis: currentEmojis });
-            toast(i18n.t("saved_emoji"));
-            this.setState({ loading: false });
+    HttpService.client.uploadImage({ image: file }).then(res => {
+      console.log("pictrs upload:");
+      console.log(res);
+      if (res.state === "success") {
+        if (res.data.msg === "ok") {
+          pictrsDeleteToast(file.name, res.data.delete_url as string);
+          form.handleEmojiImageUrlChange(
+            { form: form, index: index, overrideValue: res.data.url as string },
+            event
+          );
+        } else if (res.data.msg === "too_large") {
+          toast(I18NextService.i18n.t("upload_too_large"), "danger");
+        } else {
+          toast(JSON.stringify(res), "danger");
         }
-        else if (op == UserOperation.DeleteCustomEmoji) {
-            let data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
-            if (data.success) {
-                removeFromEmojiDataModel(data.id);
-                let custom_emojis = [...this.state.customEmojis.filter(x => x.id != data.id)];
-                this.setState({ customEmojis: custom_emojis });
-                toast(i18n.t("deleted_emoji"));
-            }
-            this.setState({ loading: false });
-        }
-    }
-}
+      } else if (res.state === "failed") {
+        console.error(res.msg);
+        toast(res.msg, "danger");
+      }
+    });
+  }
 
+  configurePicker(): any {
+    return {
+      data: { categories: [], emojis: [], aliases: [] },
+      maxFrequentRows: 0,
+      dynamicWidth: true,
+    };
+  }
+}