1 import { myAuthRequired, setIsoData } from "@utils/app";
2 import { capitalizeFirstLetter } from "@utils/helpers";
3 import { Component, linkEvent } from "inferno";
9 } from "lemmy-js-client";
10 import { customEmojisLookup } from "../../markdown";
11 import { HttpService, I18NextService } from "../../services";
12 import { pictrsDeleteToast, toast } from "../../toast";
13 import { EmojiMart } from "../common/emoji-mart";
14 import { HtmlTags } from "../common/html-tags";
15 import { Icon, Spinner } 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;
24 interface EmojiFormState {
25 siteRes: GetSiteResponse;
26 customEmojis: CustomEmojiViewForm[];
30 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),
60 state: EmojiFormState;
61 private scrollRef: any = {};
62 constructor(props: any, context: any) {
63 super(props, context);
64 this.state = this.emptyState;
66 this.handlePageChange = this.handlePageChange.bind(this);
67 this.handleEmojiClick = this.handleEmojiClick.bind(this);
69 get documentTitle(): string {
70 return I18NextService.i18n.t("custom_emojis");
75 <div className="home-emojis-form col-12">
77 title={this.documentTitle}
78 path={this.context.router.route.match.url}
80 <h1 className="h4 mb-4">{I18NextService.i18n.t("custom_emojis")}</h1>
81 {customEmojisLookup.size > 0 && (
84 onEmojiClick={this.handleEmojiClick}
85 pickerOptions={this.configurePicker()}
89 <div className="table-responsive">
92 className="table table-sm table-hover align-middle"
94 <thead className="pointer">
96 <th>{I18NextService.i18n.t("column_emoji")}</th>
97 <th className="text-right">
98 {I18NextService.i18n.t("column_shortcode")}
100 <th className="text-right">
101 {I18NextService.i18n.t("column_category")}
103 <th className="text-right d-lg-table-cell d-none">
104 {I18NextService.i18n.t("column_imageurl")}
106 <th className="text-right">
107 {I18NextService.i18n.t("column_alttext")}
109 <th className="text-right d-lg-table-cell">
110 {I18NextService.i18n.t("column_keywords")}
112 <th style="width:121px"></th>
116 {this.state.customEmojis
118 Number((this.state.page - 1) * this.itemsPerPage),
120 (this.state.page - 1) * this.itemsPerPage +
124 .map((cv, index) => (
125 <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
126 <td style="text-align:center;">
127 {cv.image_url.length > 0 && (
129 className="icon-emoji-admin"
134 {cv.image_url.length === 0 && (
136 // TODO: Fix this linting violation
137 // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
139 className="btn btn-sm btn-secondary pointer"
140 htmlFor={`file-uploader-${index}`}
141 data-tippy-content={I18NextService.i18n.t(
145 {capitalizeFirstLetter(
146 I18NextService.i18n.t("upload")
149 name={`file-uploader-${index}`}
150 id={`file-uploader-${index}`}
155 { form: this, index: index },
156 this.handleImageUpload
162 <td className="text-right">
165 placeholder="ShortCode"
166 className="form-control"
170 { form: this, index: index },
171 this.handleEmojiShortCodeChange
175 <td className="text-right">
178 placeholder="Category"
179 className="form-control"
182 { form: this, index: index },
183 this.handleEmojiCategoryChange
187 <td className="text-right d-lg-table-cell d-none">
191 className="form-control"
194 { form: this, index: index, overrideValue: null },
195 this.handleEmojiImageUrlChange
199 <td className="text-right">
202 placeholder="Alt Text"
203 className="form-control"
206 { form: this, index: index },
207 this.handleEmojiAltTextChange
211 <td className="text-right d-lg-table-cell">
214 placeholder="Keywords"
215 className="form-control"
218 { form: this, index: index },
219 this.handleEmojiKeywordChange
225 <span title={this.getEditTooltip(cv)}>
230 : "text-muted ") + "btn btn-link btn-animate"
234 this.handleEditEmojiClick
236 data-tippy-content={I18NextService.i18n.t("save")}
237 aria-label={I18NextService.i18n.t("save")}
238 disabled={!this.canEdit(cv)}
243 capitalizeFirstLetter(
244 I18NextService.i18n.t("save")
250 className="btn btn-link btn-animate text-muted"
252 { i: this, index: index, cv: cv },
253 this.handleDeleteEmojiClick
255 data-tippy-content={I18NextService.i18n.t("delete")}
256 aria-label={I18NextService.i18n.t("delete")}
257 disabled={cv.loading}
258 title={I18NextService.i18n.t("delete")}
262 classes="icon-inline text-danger"
273 className="btn btn-sm btn-secondary me-2"
274 onClick={linkEvent(this, this.handleAddEmojiClick)}
276 {I18NextService.i18n.t("add_custom_emoji")}
279 <Paginator page={this.state.page} onChange={this.handlePageChange} />
285 canEdit(cv: CustomEmojiViewForm) {
286 const noEmptyFields =
287 cv.alt_text.length > 0 &&
288 cv.category.length > 0 &&
289 cv.image_url.length > 0 &&
290 cv.shortcode.length > 0;
291 const noDuplicateShortCodes =
292 this.state.customEmojis.filter(
293 x => x.shortcode == cv.shortcode && x.id != cv.id
295 return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
298 getEditTooltip(cv: CustomEmojiViewForm) {
299 if (this.canEdit(cv)) return I18NextService.i18n.t("save");
300 else return I18NextService.i18n.t("custom_emoji_save_validation");
303 handlePageChange(page: number) {
304 this.setState({ page: page });
307 handleEmojiClick(e: any) {
308 const view = customEmojisLookup.get(e.id);
310 const page = this.state.customEmojis.find(
311 x => x.id == view.custom_emoji.id
314 this.setState({ page: page });
315 this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
320 handleEmojiCategoryChange(
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 category: event.target.value,
332 custom_emojis[Number(pagedIndex)] = item;
333 props.form.setState({ customEmojis: custom_emojis });
336 handleEmojiShortCodeChange(
337 props: { form: EmojiForm; index: number },
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 shortcode: event.target.value,
348 custom_emojis[Number(pagedIndex)] = item;
349 props.form.setState({ customEmojis: custom_emojis });
352 handleEmojiImageUrlChange(
357 }: { form: EmojiForm; index: number; overrideValue: string | null },
360 form.setState(prevState => {
361 const custom_emojis = [...form.state.customEmojis];
362 const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
364 ...form.state.customEmojis[pagedIndex],
365 image_url: overrideValue ?? event.target.value,
368 custom_emojis[Number(pagedIndex)] = item;
371 customEmojis: prevState.customEmojis.map((ce, i) =>
375 image_url: overrideValue ?? event.target.value,
385 handleEmojiAltTextChange(
386 props: { form: EmojiForm; index: number },
389 const custom_emojis = [...props.form.state.customEmojis];
391 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
393 ...props.form.state.customEmojis[pagedIndex],
394 alt_text: event.target.value,
397 custom_emojis[Number(pagedIndex)] = item;
398 props.form.setState({ customEmojis: custom_emojis });
401 handleEmojiKeywordChange(
402 props: { form: EmojiForm; index: number },
405 const custom_emojis = [...props.form.state.customEmojis];
407 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
409 ...props.form.state.customEmojis[pagedIndex],
410 keywords: event.target.value,
413 custom_emojis[Number(pagedIndex)] = item;
414 props.form.setState({ customEmojis: custom_emojis });
417 handleDeleteEmojiClick(d: {
420 cv: CustomEmojiViewForm;
422 const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
426 auth: myAuthRequired(),
429 const custom_emojis = [...d.i.state.customEmojis];
430 custom_emojis.splice(Number(pagedIndex), 1);
431 d.i.setState({ customEmojis: custom_emojis });
435 handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
436 const keywords = d.cv.keywords
438 .filter(x => x.length > 0) as string[];
439 const uniqueKeywords = Array.from(new Set(keywords));
443 category: d.cv.category,
444 image_url: d.cv.image_url,
445 alt_text: d.cv.alt_text,
446 keywords: uniqueKeywords,
447 auth: myAuthRequired(),
451 category: d.cv.category,
452 shortcode: d.cv.shortcode,
453 image_url: d.cv.image_url,
454 alt_text: d.cv.alt_text,
455 keywords: uniqueKeywords,
456 auth: myAuthRequired(),
461 handleAddEmojiClick(form: EmojiForm, event: any) {
462 event.preventDefault();
463 form.setState(prevState => {
465 1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
466 const item: CustomEmojiViewForm = {
480 customEmojis: [...prevState.customEmojis, item],
487 { form, index }: { form: EmojiForm; index: number },
492 event.preventDefault();
493 file = event.target.files[0];
498 form.setState(prevState => ({
500 customEmojis: prevState.customEmojis.map((cv, i) =>
501 i === index ? { ...cv, loading: true } : cv
505 HttpService.client.uploadImage({ image: file }).then(res => {
506 console.log("pictrs upload:");
508 if (res.state === "success") {
509 if (res.data.msg === "ok") {
510 pictrsDeleteToast(file.name, res.data.delete_url as string);
511 form.handleEmojiImageUrlChange(
512 { form: form, index: index, overrideValue: res.data.url as string },
515 } else if (res.data.msg === "too_large") {
516 toast(I18NextService.i18n.t("upload_too_large"), "danger");
518 toast(JSON.stringify(res), "danger");
520 } else if (res.state === "failed") {
521 console.error(res.msg);
522 toast(res.msg, "danger");
527 configurePicker(): any {
529 data: { categories: [], emojis: [], aliases: [] },