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