]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/emojis-form.tsx
fix submodule error
[lemmy-ui.git] / src / shared / components / home / emojis-form.tsx
1 import { myAuthRequired, setIsoData } from "@utils/app";
2 import { capitalizeFirstLetter } from "@utils/helpers";
3 import { Component, linkEvent } from "inferno";
4 import {
5   CreateCustomEmoji,
6   DeleteCustomEmoji,
7   EditCustomEmoji,
8   GetSiteResponse,
9 } from "lemmy-js-client";
10 import { customEmojisLookup } from "../../markdown";
11 import { HttpService, I18NextService } from "../../services";
12 import { pictrsDeleteToast, toast } from "../../toast";
13 import { EmojiMart } from "../common/emoji-mart";
14 import { Icon, Spinner } from "../common/icon";
15 import { Paginator } from "../common/paginator";
16
17 interface EmojiFormProps {
18   onEdit(form: EditCustomEmoji): void;
19   onCreate(form: CreateCustomEmoji): void;
20   onDelete(form: DeleteCustomEmoji): void;
21 }
22
23 interface EmojiFormState {
24   siteRes: GetSiteResponse;
25   customEmojis: CustomEmojiViewForm[];
26   page: number;
27 }
28
29 interface CustomEmojiViewForm {
30   id: number;
31   category: string;
32   shortcode: string;
33   image_url: string;
34   alt_text: string;
35   keywords: string;
36   changed: boolean;
37   page: number;
38   loading: boolean;
39 }
40
41 export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
42   private isoData = setIsoData(this.context);
43   private itemsPerPage = 15;
44   private emptyState: EmojiFormState = {
45     siteRes: this.isoData.site_res,
46     customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
47       id: x.custom_emoji.id,
48       category: x.custom_emoji.category,
49       shortcode: x.custom_emoji.shortcode,
50       image_url: x.custom_emoji.image_url,
51       alt_text: x.custom_emoji.alt_text,
52       keywords: x.keywords.map(x => x.keyword).join(" "),
53       changed: false,
54       page: 1 + Math.floor(index / this.itemsPerPage),
55       loading: false,
56     })),
57     page: 1,
58   };
59   state: EmojiFormState;
60   private scrollRef: any = {};
61   constructor(props: any, context: any) {
62     super(props, context);
63     this.state = this.emptyState;
64
65     this.handlePageChange = this.handlePageChange.bind(this);
66     this.handleEmojiClick = this.handleEmojiClick.bind(this);
67   }
68   render() {
69     return (
70       <div className="home-emojis-form col-12">
71         <h1 className="h4 mb-4">{I18NextService.i18n.t("custom_emojis")}</h1>
72         {customEmojisLookup.size > 0 && (
73           <div>
74             <EmojiMart
75               onEmojiClick={this.handleEmojiClick}
76               pickerOptions={this.configurePicker()}
77             ></EmojiMart>
78           </div>
79         )}
80         <div className="table-responsive">
81           <table
82             id="emojis_table"
83             className="table table-sm table-hover align-middle"
84           >
85             <thead className="pointer">
86               <tr>
87                 <th>{I18NextService.i18n.t("column_emoji")}</th>
88                 <th className="text-right">
89                   {I18NextService.i18n.t("column_shortcode")}
90                 </th>
91                 <th className="text-right">
92                   {I18NextService.i18n.t("column_category")}
93                 </th>
94                 <th className="text-right d-lg-table-cell d-none">
95                   {I18NextService.i18n.t("column_imageurl")}
96                 </th>
97                 <th className="text-right">
98                   {I18NextService.i18n.t("column_alttext")}
99                 </th>
100                 <th className="text-right d-lg-table-cell">
101                   {I18NextService.i18n.t("column_keywords")}
102                 </th>
103                 <th style="width:121px"></th>
104               </tr>
105             </thead>
106             <tbody>
107               {this.state.customEmojis
108                 .slice(
109                   Number((this.state.page - 1) * this.itemsPerPage),
110                   Number(
111                     (this.state.page - 1) * this.itemsPerPage +
112                       this.itemsPerPage,
113                   ),
114                 )
115                 .map((cv, index) => (
116                   <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
117                     <td style="text-align:center;">
118                       {cv.image_url.length > 0 && (
119                         <img
120                           className="icon-emoji-admin"
121                           src={cv.image_url}
122                           alt={cv.alt_text}
123                         />
124                       )}
125                       {cv.image_url.length === 0 && (
126                         <label
127                           // TODO: Fix this linting violation
128                           // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
129                           tabIndex={0}
130                           className="btn btn-sm btn-secondary pointer"
131                           htmlFor={`file-uploader-${index}`}
132                           data-tippy-content={I18NextService.i18n.t(
133                             "upload_image",
134                           )}
135                         >
136                           {capitalizeFirstLetter(
137                             I18NextService.i18n.t("upload"),
138                           )}
139                           <input
140                             name={`file-uploader-${index}`}
141                             id={`file-uploader-${index}`}
142                             type="file"
143                             accept="image/*"
144                             className="d-none"
145                             onChange={linkEvent(
146                               { form: this, index: index },
147                               this.handleImageUpload,
148                             )}
149                           />
150                         </label>
151                       )}
152                     </td>
153                     <td className="text-right">
154                       <input
155                         type="text"
156                         placeholder="ShortCode"
157                         className="form-control"
158                         disabled={cv.id > 0}
159                         value={cv.shortcode}
160                         onInput={linkEvent(
161                           { form: this, index: index },
162                           this.handleEmojiShortCodeChange,
163                         )}
164                       />
165                     </td>
166                     <td className="text-right">
167                       <input
168                         type="text"
169                         placeholder="Category"
170                         className="form-control"
171                         value={cv.category}
172                         onInput={linkEvent(
173                           { form: this, index: index },
174                           this.handleEmojiCategoryChange,
175                         )}
176                       />
177                     </td>
178                     <td className="text-right d-lg-table-cell d-none">
179                       <input
180                         type="text"
181                         placeholder="Url"
182                         className="form-control"
183                         value={cv.image_url}
184                         onInput={linkEvent(
185                           { form: this, index: index, overrideValue: null },
186                           this.handleEmojiImageUrlChange,
187                         )}
188                       />
189                     </td>
190                     <td className="text-right">
191                       <input
192                         type="text"
193                         placeholder="Alt Text"
194                         className="form-control"
195                         value={cv.alt_text}
196                         onInput={linkEvent(
197                           { form: this, index: index },
198                           this.handleEmojiAltTextChange,
199                         )}
200                       />
201                     </td>
202                     <td className="text-right d-lg-table-cell">
203                       <input
204                         type="text"
205                         placeholder="Keywords"
206                         className="form-control"
207                         value={cv.keywords}
208                         onInput={linkEvent(
209                           { form: this, index: index },
210                           this.handleEmojiKeywordChange,
211                         )}
212                       />
213                     </td>
214                     <td>
215                       <div>
216                         <span title={this.getEditTooltip(cv)}>
217                           <button
218                             className={
219                               (this.canEdit(cv)
220                                 ? "text-success "
221                                 : "text-muted ") + "btn btn-link btn-animate"
222                             }
223                             onClick={linkEvent(
224                               { i: this, cv: cv },
225                               this.handleEditEmojiClick,
226                             )}
227                             data-tippy-content={I18NextService.i18n.t("save")}
228                             aria-label={I18NextService.i18n.t("save")}
229                             disabled={!this.canEdit(cv)}
230                           >
231                             {cv.loading ? (
232                               <Spinner />
233                             ) : (
234                               capitalizeFirstLetter(
235                                 I18NextService.i18n.t("save"),
236                               )
237                             )}
238                           </button>
239                         </span>
240                         <button
241                           className="btn btn-link btn-animate text-muted"
242                           onClick={linkEvent(
243                             { i: this, index: index, cv: cv },
244                             this.handleDeleteEmojiClick,
245                           )}
246                           data-tippy-content={I18NextService.i18n.t("delete")}
247                           aria-label={I18NextService.i18n.t("delete")}
248                           disabled={cv.loading}
249                           title={I18NextService.i18n.t("delete")}
250                         >
251                           <Icon
252                             icon="trash"
253                             classes="icon-inline text-danger"
254                           />
255                         </button>
256                       </div>
257                     </td>
258                   </tr>
259                 ))}
260             </tbody>
261           </table>
262           <br />
263           <button
264             className="btn btn-sm btn-secondary me-2"
265             onClick={linkEvent(this, this.handleAddEmojiClick)}
266           >
267             {I18NextService.i18n.t("add_custom_emoji")}
268           </button>
269
270           <Paginator page={this.state.page} onChange={this.handlePageChange} />
271         </div>
272       </div>
273     );
274   }
275
276   canEdit(cv: CustomEmojiViewForm) {
277     const noEmptyFields =
278       cv.alt_text.length > 0 &&
279       cv.category.length > 0 &&
280       cv.image_url.length > 0 &&
281       cv.shortcode.length > 0;
282     const noDuplicateShortCodes =
283       this.state.customEmojis.filter(
284         x => x.shortcode === cv.shortcode && x.id !== cv.id,
285       ).length === 0;
286     return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
287   }
288
289   getEditTooltip(cv: CustomEmojiViewForm) {
290     if (this.canEdit(cv)) return I18NextService.i18n.t("save");
291     else return I18NextService.i18n.t("custom_emoji_save_validation");
292   }
293
294   handlePageChange(page: number) {
295     this.setState({ page: page });
296   }
297
298   handleEmojiClick(e: any) {
299     const view = customEmojisLookup.get(e.id);
300     if (view) {
301       const page = this.state.customEmojis.find(
302         x => x.id === view.custom_emoji.id,
303       )?.page;
304       if (page) {
305         this.setState({ page: page });
306         this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
307       }
308     }
309   }
310
311   handleEmojiCategoryChange(
312     props: { form: EmojiForm; index: number },
313     event: any,
314   ) {
315     const custom_emojis = [...props.form.state.customEmojis];
316     const pagedIndex =
317       (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
318     const item = {
319       ...props.form.state.customEmojis[pagedIndex],
320       category: event.target.value,
321       changed: true,
322     };
323     custom_emojis[Number(pagedIndex)] = item;
324     props.form.setState({ customEmojis: custom_emojis });
325   }
326
327   handleEmojiShortCodeChange(
328     props: { form: EmojiForm; index: number },
329     event: any,
330   ) {
331     const custom_emojis = [...props.form.state.customEmojis];
332     const pagedIndex =
333       (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
334     const item = {
335       ...props.form.state.customEmojis[pagedIndex],
336       shortcode: event.target.value,
337       changed: true,
338     };
339     custom_emojis[Number(pagedIndex)] = item;
340     props.form.setState({ customEmojis: custom_emojis });
341   }
342
343   handleEmojiImageUrlChange(
344     {
345       form,
346       index,
347       overrideValue,
348     }: { form: EmojiForm; index: number; overrideValue: string | null },
349     event: any,
350   ) {
351     form.setState(prevState => {
352       const custom_emojis = [...form.state.customEmojis];
353       const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
354       const item = {
355         ...form.state.customEmojis[pagedIndex],
356         image_url: overrideValue ?? event.target.value,
357         changed: true,
358       };
359       custom_emojis[Number(pagedIndex)] = item;
360       return {
361         ...prevState,
362         customEmojis: prevState.customEmojis.map((ce, i) =>
363           i === pagedIndex
364             ? {
365                 ...ce,
366                 image_url: overrideValue ?? event.target.value,
367                 changed: true,
368                 loading: false,
369               }
370             : ce,
371         ),
372       };
373     });
374   }
375
376   handleEmojiAltTextChange(
377     props: { form: EmojiForm; index: number },
378     event: any,
379   ) {
380     const custom_emojis = [...props.form.state.customEmojis];
381     const pagedIndex =
382       (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
383     const item = {
384       ...props.form.state.customEmojis[pagedIndex],
385       alt_text: event.target.value,
386       changed: true,
387     };
388     custom_emojis[Number(pagedIndex)] = item;
389     props.form.setState({ customEmojis: custom_emojis });
390   }
391
392   handleEmojiKeywordChange(
393     props: { form: EmojiForm; index: number },
394     event: any,
395   ) {
396     const custom_emojis = [...props.form.state.customEmojis];
397     const pagedIndex =
398       (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
399     const item = {
400       ...props.form.state.customEmojis[pagedIndex],
401       keywords: event.target.value,
402       changed: true,
403     };
404     custom_emojis[Number(pagedIndex)] = item;
405     props.form.setState({ customEmojis: custom_emojis });
406   }
407
408   handleDeleteEmojiClick(d: {
409     i: EmojiForm;
410     index: number;
411     cv: CustomEmojiViewForm;
412   }) {
413     const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
414     if (d.cv.id !== 0) {
415       d.i.props.onDelete({
416         id: d.cv.id,
417         auth: myAuthRequired(),
418       });
419     } else {
420       const custom_emojis = [...d.i.state.customEmojis];
421       custom_emojis.splice(Number(pagedIndex), 1);
422       d.i.setState({ customEmojis: custom_emojis });
423     }
424   }
425
426   handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
427     const keywords = d.cv.keywords
428       .split(" ")
429       .filter(x => x.length > 0) as string[];
430     const uniqueKeywords = Array.from(new Set(keywords));
431     if (d.cv.id !== 0) {
432       d.i.props.onEdit({
433         id: d.cv.id,
434         category: d.cv.category,
435         image_url: d.cv.image_url,
436         alt_text: d.cv.alt_text,
437         keywords: uniqueKeywords,
438         auth: myAuthRequired(),
439       });
440     } else {
441       d.i.props.onCreate({
442         category: d.cv.category,
443         shortcode: d.cv.shortcode,
444         image_url: d.cv.image_url,
445         alt_text: d.cv.alt_text,
446         keywords: uniqueKeywords,
447         auth: myAuthRequired(),
448       });
449     }
450   }
451
452   handleAddEmojiClick(form: EmojiForm, event: any) {
453     event.preventDefault();
454     form.setState(prevState => {
455       const page =
456         1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
457       const item: CustomEmojiViewForm = {
458         id: 0,
459         shortcode: "",
460         alt_text: "",
461         category: "",
462         image_url: "",
463         keywords: "",
464         changed: false,
465         page: page,
466         loading: false,
467       };
468
469       return {
470         ...prevState,
471         customEmojis: [...prevState.customEmojis, item],
472         page,
473       };
474     });
475   }
476
477   handleImageUpload(
478     { form, index }: { form: EmojiForm; index: number },
479     event: any,
480   ) {
481     let file: any;
482     if (event.target) {
483       event.preventDefault();
484       file = event.target.files[0];
485     } else {
486       file = event;
487     }
488
489     form.setState(prevState => ({
490       ...prevState,
491       customEmojis: prevState.customEmojis.map((cv, i) =>
492         i === index ? { ...cv, loading: true } : cv,
493       ),
494     }));
495
496     HttpService.client.uploadImage({ image: file }).then(res => {
497       console.log("pictrs upload:");
498       console.log(res);
499       if (res.state === "success") {
500         if (res.data.msg === "ok") {
501           pictrsDeleteToast(file.name, res.data.delete_url as string);
502           form.handleEmojiImageUrlChange(
503             { form: form, index: index, overrideValue: res.data.url as string },
504             event,
505           );
506         } else if (res.data.msg === "too_large") {
507           toast(I18NextService.i18n.t("upload_too_large"), "danger");
508         } else {
509           toast(JSON.stringify(res), "danger");
510         }
511       } else if (res.state === "failed") {
512         console.error(res.msg);
513         toast(res.msg, "danger");
514       }
515     });
516   }
517
518   configurePicker(): any {
519     return {
520       data: { categories: [], emojis: [], aliases: [] },
521       maxFrequentRows: 0,
522       dynamicWidth: true,
523     };
524   }
525 }