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