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