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