1 import { myAuthRequired, setIsoData } from "@utils/app";
2 import { Component, linkEvent } from "inferno";
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";
18 interface EmojiFormProps {
19 onEdit(form: EditCustomEmoji): void;
20 onCreate(form: CreateCustomEmoji): void;
21 onDelete(form: DeleteCustomEmoji): void;
25 interface EmojiFormState {
26 siteRes: GetSiteResponse;
27 customEmojis: CustomEmojiViewForm[];
31 interface CustomEmojiViewForm {
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(" "),
55 page: 1 + Math.floor(index / this.itemsPerPage),
59 state: EmojiFormState;
60 private scrollRef: any = {};
61 constructor(props: any, context: any) {
62 super(props, context);
63 this.state = this.emptyState;
65 this.handlePageChange = this.handlePageChange.bind(this);
66 this.handleEmojiClick = this.handleEmojiClick.bind(this);
68 get documentTitle(): string {
69 return i18n.t("custom_emojis");
74 <div className="home-emojis-form col-12">
76 title={this.documentTitle}
77 path={this.context.router.route.match.url}
79 <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
80 {customEmojisLookup.size > 0 && (
83 onEmojiClick={this.handleEmojiClick}
84 pickerOptions={this.configurePicker()}
88 <div className="table-responsive">
89 <table id="emojis_table" className="table table-sm table-hover">
90 <thead className="pointer">
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")}
98 <th className="text-right">{i18n.t("column_alttext")}</th>
99 <th className="text-right d-lg-table-cell">
100 {i18n.t("column_keywords")}
102 <th style="width:121px"></th>
106 {this.state.customEmojis
108 Number((this.state.page - 1) * this.itemsPerPage),
110 (this.state.page - 1) * this.itemsPerPage +
114 .map((cv, index) => (
115 <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
116 <td style="text-align:center;">
118 htmlFor={index.toString()}
119 className="pointer text-muted small font-weight-bold"
121 {cv.image_url.length > 0 && (
123 className="icon-emoji-admin"
127 {cv.image_url.length == 0 && (
128 <span className="btn btn-sm btn-secondary">
134 name={index.toString()}
135 id={index.toString()}
140 { form: this, index: index },
141 this.handleImageUpload
145 <td className="text-right">
148 placeholder="ShortCode"
149 className="form-control"
153 { form: this, index: index },
154 this.handleEmojiShortCodeChange
158 <td className="text-right">
161 placeholder="Category"
162 className="form-control"
165 { form: this, index: index },
166 this.handleEmojiCategoryChange
170 <td className="text-right d-lg-table-cell d-none">
174 className="form-control"
177 { form: this, index: index, overrideValue: null },
178 this.handleEmojiImageUrlChange
182 <td className="text-right">
185 placeholder="Alt Text"
186 className="form-control"
189 { form: this, index: index },
190 this.handleEmojiAltTextChange
194 <td className="text-right d-lg-table-cell">
197 placeholder="Keywords"
198 className="form-control"
201 { form: this, index: index },
202 this.handleEmojiKeywordChange
208 <span title={this.getEditTooltip(cv)}>
211 (cv.changed ? "text-success " : "text-muted ") +
212 "btn btn-link btn-animate"
216 this.handleEditEmojiClick
218 data-tippy-content={i18n.t("save")}
219 aria-label={i18n.t("save")}
221 this.props.loading ||
228 classes={`icon-inline`}
234 className="btn btn-link btn-animate text-muted"
236 { i: this, index: index, cv: cv },
237 this.handleDeleteEmojiClick
239 data-tippy-content={i18n.t("delete")}
240 aria-label={i18n.t("delete")}
241 disabled={this.props.loading}
242 title={i18n.t("delete")}
246 classes={`icon-inline text-danger`}
257 className="btn btn-sm btn-secondary me-2"
258 onClick={linkEvent(this, this.handleAddEmojiClick)}
260 {i18n.t("add_custom_emoji")}
263 <Paginator page={this.state.page} onChange={this.handlePageChange} />
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
279 return noEmptyFields && noDuplicateShortCodes;
282 getEditTooltip(cv: CustomEmojiViewForm) {
283 if (this.canEdit(cv)) return i18n.t("save");
284 else return i18n.t("custom_emoji_save_validation");
287 handlePageChange(page: number) {
288 this.setState({ page: page });
291 handleEmojiClick(e: any) {
292 const view = customEmojisLookup.get(e.id);
294 const page = this.state.customEmojis.find(
295 x => x.id == view.custom_emoji.id
298 this.setState({ page: page });
299 this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
304 handleEmojiCategoryChange(
305 props: { form: EmojiForm; index: number },
308 const custom_emojis = [...props.form.state.customEmojis];
310 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
312 ...props.form.state.customEmojis[pagedIndex],
313 category: event.target.value,
316 custom_emojis[Number(pagedIndex)] = item;
317 props.form.setState({ customEmojis: custom_emojis });
320 handleEmojiShortCodeChange(
321 props: { form: EmojiForm; index: number },
324 const custom_emojis = [...props.form.state.customEmojis];
326 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
328 ...props.form.state.customEmojis[pagedIndex],
329 shortcode: event.target.value,
332 custom_emojis[Number(pagedIndex)] = item;
333 props.form.setState({ customEmojis: custom_emojis });
336 handleEmojiImageUrlChange(
337 props: { form: EmojiForm; index: number; overrideValue: string | null },
340 const custom_emojis = [...props.form.state.customEmojis];
342 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
344 ...props.form.state.customEmojis[pagedIndex],
345 image_url: props.overrideValue ?? event.target.value,
348 custom_emojis[Number(pagedIndex)] = item;
349 props.form.setState({ customEmojis: custom_emojis });
352 handleEmojiAltTextChange(
353 props: { form: EmojiForm; index: number },
356 const custom_emojis = [...props.form.state.customEmojis];
358 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
360 ...props.form.state.customEmojis[pagedIndex],
361 alt_text: event.target.value,
364 custom_emojis[Number(pagedIndex)] = item;
365 props.form.setState({ customEmojis: custom_emojis });
368 handleEmojiKeywordChange(
369 props: { form: EmojiForm; index: number },
372 const custom_emojis = [...props.form.state.customEmojis];
374 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
376 ...props.form.state.customEmojis[pagedIndex],
377 keywords: event.target.value,
380 custom_emojis[Number(pagedIndex)] = item;
381 props.form.setState({ customEmojis: custom_emojis });
384 handleDeleteEmojiClick(d: {
387 cv: CustomEmojiViewForm;
389 const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
393 auth: myAuthRequired(),
396 const custom_emojis = [...d.i.state.customEmojis];
397 custom_emojis.splice(Number(pagedIndex), 1);
398 d.i.setState({ customEmojis: custom_emojis });
402 handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
403 const keywords = d.cv.keywords
405 .filter(x => x.length > 0) as string[];
406 const uniqueKeywords = Array.from(new Set(keywords));
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(),
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(),
428 handleAddEmojiClick(form: EmojiForm, event: any) {
429 event.preventDefault();
430 const custom_emojis = [...form.state.customEmojis];
432 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
433 const item: CustomEmojiViewForm = {
443 custom_emojis.push(item);
444 form.setState({ customEmojis: custom_emojis, page: page });
447 handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
450 event.preventDefault();
451 file = event.target.files[0];
456 HttpService.client.uploadImage({ image: file }).then(res => {
457 console.log("pictrs upload:");
459 if (res.state === "success") {
460 if (res.data.msg === "ok") {
461 pictrsDeleteToast(file.name, res.data.delete_url as string);
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 },
471 } else if (res.state === "failed") {
472 console.error(res.msg);
473 toast(res.msg, "danger");
478 configurePicker(): any {
480 data: { categories: [], emojis: [], aliases: [] },