]> 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 eccbe5f41198b5f5e1d0c402cd8e072c47e5c906..3ad8cc05a77742923a1a978b615673b14271d3a0 100644 (file)
@@ -1,3 +1,5 @@
+import { myAuthRequired, setIsoData } from "@utils/app";
+import { capitalizeFirstLetter } from "@utils/helpers";
 import { Component, linkEvent } from "inferno";
 import {
   CreateCustomEmoji,
@@ -5,25 +7,18 @@ import {
   EditCustomEmoji,
   GetSiteResponse,
 } from "lemmy-js-client";
-import { i18n } from "../../i18next";
-import { HttpService } from "../../services/HttpService";
-import {
-  customEmojisLookup,
-  myAuthRequired,
-  pictrsDeleteToast,
-  setIsoData,
-  toast,
-} 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;
-  loading: boolean;
 }
 
 interface EmojiFormState {
@@ -41,6 +36,7 @@ interface CustomEmojiViewForm {
   keywords: string;
   changed: boolean;
   page: number;
+  loading: boolean;
 }
 
 export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
@@ -57,6 +53,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
       keywords: x.keywords.map(x => x.keyword).join(" "),
       changed: false,
       page: 1 + Math.floor(index / this.itemsPerPage),
+      loading: false,
     })),
     page: 1,
   };
@@ -70,17 +67,17 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
     this.handleEmojiClick = this.handleEmojiClick.bind(this);
   }
   get documentTitle(): string {
-    return i18n.t("custom_emojis");
+    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
@@ -90,18 +87,27 @@ export class EmojiForm extends Component<EmojiFormProps, 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>
@@ -118,33 +124,40 @@ export class EmojiForm extends Component<EmojiFormProps, 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
@@ -212,26 +225,25 @@ export class EmojiForm extends Component<EmojiFormProps, 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(
                               { i: this, cv: cv },
                               this.handleEditEmojiClick
                             )}
-                            data-tippy-content={i18n.t("save")}
-                            aria-label={i18n.t("save")}
-                            disabled={
-                              this.props.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
@@ -240,14 +252,14 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
                             { i: this, index: index, cv: cv },
                             this.handleDeleteEmojiClick
                           )}
-                          data-tippy-content={i18n.t("delete")}
-                          aria-label={i18n.t("delete")}
-                          disabled={this.props.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>
@@ -261,7 +273,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
             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} />
@@ -280,12 +292,12 @@ export class EmojiForm extends Component<EmojiFormProps, 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) {
@@ -338,19 +350,36 @@ export class EmojiForm extends Component<EmojiFormProps, 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(
@@ -408,7 +437,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
       .split(" ")
       .filter(x => x.length > 0) as string[];
     const uniqueKeywords = Array.from(new Set(keywords));
-    if (d.cv.id != 0) {
+    if (d.cv.id !== 0) {
       d.i.props.onEdit({
         id: d.cv.id,
         category: d.cv.category,
@@ -431,24 +460,33 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
 
   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();
@@ -457,20 +495,27 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
       file = event;
     }
 
+    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);
-        } else {
-          toast(JSON.stringify(res), "danger");
-          const hash = res.data.files?.at(0)?.file;
-          const url = `${res.data.url}/${hash}`;
-          props.form.handleEmojiImageUrlChange(
-            { form: props.form, index: props.index, overrideValue: url },
+          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 (res.state === "failed") {
         console.error(res.msg);