]> 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 044964da724f03ee25e6b67c9226c656d8922377..3ad8cc05a77742923a1a978b615673b14271d3a0 100644 (file)
@@ -1,40 +1,29 @@
+import { myAuthRequired, setIsoData } from "@utils/app";
+import { capitalizeFirstLetter } from "@utils/helpers";
 import { Component, linkEvent } from "inferno";
 import {
   CreateCustomEmoji,
-  CustomEmojiResponse,
   DeleteCustomEmoji,
-  DeleteCustomEmojiResponse,
   EditCustomEmoji,
   GetSiteResponse,
-  UserOperation,
-  wsJsonToRes,
-  wsUserOp,
 } from "lemmy-js-client";
-import { Subscription } from "rxjs";
-import { i18n } from "../../i18next";
-import { WebSocketService } from "../../services";
-import {
-  customEmojisLookup,
-  isBrowser,
-  myAuth,
-  pictrsDeleteToast,
-  removeFromEmojiDataModel,
-  setIsoData,
-  toast,
-  updateEmojiDataModel,
-  uploadImage,
-  wsClient,
-  wsSubscribe,
-} from "../../utils";
+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 { 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;
 }
 
@@ -47,14 +36,13 @@ interface CustomEmojiViewForm {
   keywords: string;
   changed: boolean;
   page: number;
+  loading: boolean;
 }
 
-export class EmojiForm extends Component<any, EmojiFormState> {
+export class EmojiForm extends Component<EmojiFormProps, 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,
@@ -65,6 +53,7 @@ export class EmojiForm extends Component<any, EmojiFormState> {
       keywords: x.keywords.map(x => x.keyword).join(" "),
       changed: false,
       page: 1 + Math.floor(index / this.itemsPerPage),
+      loading: false,
     })),
     page: 1,
   };
@@ -75,28 +64,20 @@ export class EmojiForm extends Component<any, EmojiFormState> {
     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");
-  }
-
-  componentWillUnmount() {
-    if (isBrowser()) {
-      this.subscription?.unsubscribe();
-    }
+    return I18NextService.i18n.t("custom_emojis");
   }
 
   render() {
     return (
-      <div className="col-12">
+      <div className="home-emojis-form col-12">
         <HtmlTags
           title={this.documentTitle}
           path={this.context.router.route.match.url}
         />
-        <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
+        <h5 className="col-12">{I18NextService.i18n.t("custom_emojis")}</h5>
         {customEmojisLookup.size > 0 && (
           <div>
             <EmojiMart
@@ -106,18 +87,27 @@ export class EmojiForm extends Component<any, EmojiFormState> {
           </div>
         )}
         <div className="table-responsive">
-          <table id="emojis_table" className="table table-sm table-hover">
+          <table
+            id="emojis_table"
+            className="table table-sm table-hover align-middle"
+          >
             <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>{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">
-                  {i18n.t("column_imageurl")}
+                  {I18NextService.i18n.t("column_imageurl")}
+                </th>
+                <th className="text-right">
+                  {I18NextService.i18n.t("column_alttext")}
                 </th>
-                <th className="text-right">{i18n.t("column_alttext")}</th>
                 <th className="text-right d-lg-table-cell">
-                  {i18n.t("column_keywords")}
+                  {I18NextService.i18n.t("column_keywords")}
                 </th>
                 <th style="width:121px"></th>
               </tr>
@@ -134,33 +124,40 @@ export class EmojiForm extends Component<any, EmojiFormState> {
                 .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 && (
+                        <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
+                            )}
                           />
-                        )}
-                        {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
-                        )}
-                      />
+                        </label>
+                      )}
                     </td>
                     <td className="text-right">
                       <input
@@ -228,42 +225,41 @@ export class EmojiForm extends Component<any, EmojiFormState> {
                         <span title={this.getEditTooltip(cv)}>
                           <button
                             className={
-                              (cv.changed ? "text-success " : "text-muted ") +
-                              "btn btn-link btn-animate"
+                              (this.canEdit(cv)
+                                ? "text-success "
+                                : "text-muted ") + "btn btn-link btn-animate"
                             }
                             onClick={linkEvent(
-                              { form: this, cv: cv },
+                              { i: 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
-                            }
+                            data-tippy-content={I18NextService.i18n.t("save")}
+                            aria-label={I18NextService.i18n.t("save")}
+                            disabled={!this.canEdit(cv)}
                           >
-                            {/* <Icon
-                                                            icon="edit"
-                                                            classes={`icon-inline`}
-                                                        /> */}
-                            Save
+                            {cv.loading ? (
+                              <Spinner />
+                            ) : (
+                              capitalizeFirstLetter(
+                                I18NextService.i18n.t("save")
+                              )
+                            )}
                           </button>
                         </span>
                         <button
                           className="btn btn-link btn-animate text-muted"
                           onClick={linkEvent(
-                            { form: this, index: index, cv: cv },
+                            { i: 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")}
+                          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`}
+                            classes="icon-inline text-danger"
                           />
                         </button>
                       </div>
@@ -274,10 +270,10 @@ export class EmojiForm extends Component<any, EmojiFormState> {
           </table>
           <br />
           <button
-            className="btn btn-sm btn-secondary mr-2"
+            className="btn btn-sm btn-secondary me-2"
             onClick={linkEvent(this, this.handleAddEmojiClick)}
           >
-            {i18n.t("add_custom_emoji")}
+            {I18NextService.i18n.t("add_custom_emoji")}
           </button>
 
           <Paginator page={this.state.page} onChange={this.handlePageChange} />
@@ -296,12 +292,12 @@ export class EmojiForm extends Component<any, EmojiFormState> {
       this.state.customEmojis.filter(
         x => x.shortcode == cv.shortcode && x.id != cv.id
       ).length == 0;
-    return noEmptyFields && noDuplicateShortCodes;
+    return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
   }
 
   getEditTooltip(cv: CustomEmojiViewForm) {
-    if (this.canEdit(cv)) return i18n.t("save");
-    else return i18n.t("custom_emoji_save_validation");
+    if (this.canEdit(cv)) return I18NextService.i18n.t("save");
+    else return I18NextService.i18n.t("custom_emoji_save_validation");
   }
 
   handlePageChange(page: number) {
@@ -354,19 +350,36 @@ export class EmojiForm extends Component<any, EmojiFormState> {
   }
 
   handleEmojiImageUrlChange(
-    props: { form: EmojiForm; index: number; overrideValue: string | null },
+    {
+      form,
+      index,
+      overrideValue,
+    }: { form: EmojiForm; index: number; overrideValue: string | null },
     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],
-      image_url: props.overrideValue ?? event.target.value,
-      changed: true,
-    };
-    custom_emojis[Number(pagedIndex)] = item;
-    props.form.setState({ customEmojis: custom_emojis });
+    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(
@@ -401,74 +414,79 @@ export class EmojiForm extends Component<any, EmojiFormState> {
     props.form.setState({ customEmojis: custom_emojis });
   }
 
-  handleDeleteEmojiClick(props: {
-    form: EmojiForm;
+  handleDeleteEmojiClick(d: {
+    i: EmojiForm;
     index: number;
     cv: CustomEmojiViewForm;
   }) {
-    const 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));
+    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 = [...props.form.state.customEmojis];
+      const custom_emojis = [...d.i.state.customEmojis];
       custom_emojis.splice(Number(pagedIndex), 1);
-      props.form.setState({ customEmojis: custom_emojis });
+      d.i.setState({ customEmojis: custom_emojis });
     }
   }
 
-  handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
-    const keywords = props.cv.keywords
+  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 (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,
+    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: myAuth() ?? "",
-      };
-      WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
+        auth: myAuthRequired(),
+      });
     } else {
-      const createForm: CreateCustomEmoji = {
-        category: props.cv.category,
-        shortcode: props.cv.shortcode,
-        image_url: props.cv.image_url,
-        alt_text: props.cv.alt_text,
+      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: myAuth() ?? "",
-      };
-      WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
+        auth: myAuthRequired(),
+      });
     }
   }
 
   handleAddEmojiClick(form: EmojiForm, event: any) {
     event.preventDefault();
-    const custom_emojis = [...form.state.customEmojis];
-    const page =
-      1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
-    const 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 });
+    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,
+      };
+
+      return {
+        ...prevState,
+        customEmojis: [...prevState.customEmojis, item],
+        page,
+      };
+    });
   }
 
-  handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
+  handleImageUpload(
+    { form, index }: { form: EmojiForm; index: number },
+    event: any
+  ) {
     let file: any;
     if (event.target) {
       event.preventDefault();
@@ -477,26 +495,33 @@ export class EmojiForm extends Component<any, EmojiFormState> {
       file = event;
     }
 
-    uploadImage(file)
-      .then(res => {
-        console.log("pictrs upload:");
-        console.log(res);
-        if (res.msg === "ok") {
-          pictrsDeleteToast(file.name, res.delete_url as string);
-        } else {
-          toast(JSON.stringify(res), "danger");
-          const hash = res.files?.at(0)?.file;
-          const url = `${res.url}/${hash}`;
-          props.form.handleEmojiImageUrlChange(
-            { form: props.form, index: props.index, overrideValue: url },
+    form.setState(prevState => ({
+      ...prevState,
+      customEmojis: prevState.customEmojis.map((cv, i) =>
+        i === index ? { ...cv, loading: true } : cv
+      ),
+    }));
+
+    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");
         }
-      })
-      .catch(error => {
-        console.error(error);
-        toast(error, "danger");
-      });
+      } else if (res.state === "failed") {
+        console.error(res.msg);
+        toast(res.msg, "danger");
+      }
+    });
   }
 
   configurePicker(): any {
@@ -506,51 +531,4 @@ export class EmojiForm extends Component<any, EmojiFormState> {
       dynamicWidth: true,
     };
   }
-
-  parseMessage(msg: any) {
-    const 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) {
-      const data = wsJsonToRes<CustomEmojiResponse>(msg);
-      const custom_emoji_view = data.custom_emoji;
-      updateEmojiDataModel(custom_emoji_view);
-      const currentEmojis = this.state.customEmojis;
-      const 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) {
-      const data = wsJsonToRes<CustomEmojiResponse>(msg);
-      const custom_emoji_view = data.custom_emoji;
-      updateEmojiDataModel(data.custom_emoji);
-      const currentEmojis = this.state.customEmojis;
-      const 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 });
-    } else if (op == UserOperation.DeleteCustomEmoji) {
-      const data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
-      if (data.success) {
-        removeFromEmojiDataModel(data.id);
-        const 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 });
-    }
-  }
 }