1 import { Component, linkEvent } from "inferno";
7 } from "lemmy-js-client";
8 import { i18n } from "../../i18next";
9 import { HttpService } from "../../services/HttpService";
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";
22 interface EmojiFormProps {
23 onEdit(form: EditCustomEmoji): void;
24 onCreate(form: CreateCustomEmoji): void;
25 onDelete(form: DeleteCustomEmoji): void;
29 interface EmojiFormState {
30 siteRes: GetSiteResponse;
31 customEmojis: CustomEmojiViewForm[];
35 interface CustomEmojiViewForm {
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(" "),
59 page: 1 + Math.floor(index / this.itemsPerPage),
63 state: EmojiFormState;
64 private scrollRef: any = {};
65 constructor(props: any, context: any) {
66 super(props, context);
67 this.state = this.emptyState;
69 this.handlePageChange = this.handlePageChange.bind(this);
70 this.handleEmojiClick = this.handleEmojiClick.bind(this);
72 get documentTitle(): string {
73 return i18n.t("custom_emojis");
78 <div className="col-12">
80 title={this.documentTitle}
81 path={this.context.router.route.match.url}
83 <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
84 {customEmojisLookup.size > 0 && (
87 onEmojiClick={this.handleEmojiClick}
88 pickerOptions={this.configurePicker()}
92 <div className="table-responsive">
93 <table id="emojis_table" className="table table-sm table-hover">
94 <thead className="pointer">
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")}
102 <th className="text-right">{i18n.t("column_alttext")}</th>
103 <th className="text-right d-lg-table-cell">
104 {i18n.t("column_keywords")}
106 <th style="width:121px"></th>
110 {this.state.customEmojis
112 Number((this.state.page - 1) * this.itemsPerPage),
114 (this.state.page - 1) * this.itemsPerPage +
118 .map((cv, index) => (
119 <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
120 <td style="text-align:center;">
122 htmlFor={index.toString()}
123 className="pointer text-muted small font-weight-bold"
125 {cv.image_url.length > 0 && (
127 className="icon-emoji-admin"
131 {cv.image_url.length == 0 && (
132 <span className="btn btn-sm btn-secondary">
138 name={index.toString()}
139 id={index.toString()}
144 { form: this, index: index },
145 this.handleImageUpload
149 <td className="text-right">
152 placeholder="ShortCode"
153 className="form-control"
157 { form: this, index: index },
158 this.handleEmojiShortCodeChange
162 <td className="text-right">
165 placeholder="Category"
166 className="form-control"
169 { form: this, index: index },
170 this.handleEmojiCategoryChange
174 <td className="text-right d-lg-table-cell d-none">
178 className="form-control"
181 { form: this, index: index, overrideValue: null },
182 this.handleEmojiImageUrlChange
186 <td className="text-right">
189 placeholder="Alt Text"
190 className="form-control"
193 { form: this, index: index },
194 this.handleEmojiAltTextChange
198 <td className="text-right d-lg-table-cell">
201 placeholder="Keywords"
202 className="form-control"
205 { form: this, index: index },
206 this.handleEmojiKeywordChange
212 <span title={this.getEditTooltip(cv)}>
215 (cv.changed ? "text-success " : "text-muted ") +
216 "btn btn-link btn-animate"
220 this.handleEditEmojiClick
222 data-tippy-content={i18n.t("save")}
223 aria-label={i18n.t("save")}
225 this.props.loading ||
232 classes={`icon-inline`}
238 className="btn btn-link btn-animate text-muted"
240 { i: this, index: index, cv: cv },
241 this.handleDeleteEmojiClick
243 data-tippy-content={i18n.t("delete")}
244 aria-label={i18n.t("delete")}
245 disabled={this.props.loading}
246 title={i18n.t("delete")}
250 classes={`icon-inline text-danger`}
261 className="btn btn-sm btn-secondary me-2"
262 onClick={linkEvent(this, this.handleAddEmojiClick)}
264 {i18n.t("add_custom_emoji")}
267 <Paginator page={this.state.page} onChange={this.handlePageChange} />
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
283 return noEmptyFields && noDuplicateShortCodes;
286 getEditTooltip(cv: CustomEmojiViewForm) {
287 if (this.canEdit(cv)) return i18n.t("save");
288 else return i18n.t("custom_emoji_save_validation");
291 handlePageChange(page: number) {
292 this.setState({ page: page });
295 handleEmojiClick(e: any) {
296 const view = customEmojisLookup.get(e.id);
298 const page = this.state.customEmojis.find(
299 x => x.id == view.custom_emoji.id
302 this.setState({ page: page });
303 this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
308 handleEmojiCategoryChange(
309 props: { form: EmojiForm; index: number },
312 const custom_emojis = [...props.form.state.customEmojis];
314 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
316 ...props.form.state.customEmojis[pagedIndex],
317 category: event.target.value,
320 custom_emojis[Number(pagedIndex)] = item;
321 props.form.setState({ customEmojis: custom_emojis });
324 handleEmojiShortCodeChange(
325 props: { form: EmojiForm; index: number },
328 const custom_emojis = [...props.form.state.customEmojis];
330 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
332 ...props.form.state.customEmojis[pagedIndex],
333 shortcode: event.target.value,
336 custom_emojis[Number(pagedIndex)] = item;
337 props.form.setState({ customEmojis: custom_emojis });
340 handleEmojiImageUrlChange(
341 props: { form: EmojiForm; index: number; overrideValue: string | null },
344 const custom_emojis = [...props.form.state.customEmojis];
346 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
348 ...props.form.state.customEmojis[pagedIndex],
349 image_url: props.overrideValue ?? event.target.value,
352 custom_emojis[Number(pagedIndex)] = item;
353 props.form.setState({ customEmojis: custom_emojis });
356 handleEmojiAltTextChange(
357 props: { form: EmojiForm; index: number },
360 const custom_emojis = [...props.form.state.customEmojis];
362 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
364 ...props.form.state.customEmojis[pagedIndex],
365 alt_text: event.target.value,
368 custom_emojis[Number(pagedIndex)] = item;
369 props.form.setState({ customEmojis: custom_emojis });
372 handleEmojiKeywordChange(
373 props: { form: EmojiForm; index: number },
376 const custom_emojis = [...props.form.state.customEmojis];
378 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
380 ...props.form.state.customEmojis[pagedIndex],
381 keywords: event.target.value,
384 custom_emojis[Number(pagedIndex)] = item;
385 props.form.setState({ customEmojis: custom_emojis });
388 handleDeleteEmojiClick(d: {
391 cv: CustomEmojiViewForm;
393 const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
397 auth: myAuthRequired(),
400 const custom_emojis = [...d.i.state.customEmojis];
401 custom_emojis.splice(Number(pagedIndex), 1);
402 d.i.setState({ customEmojis: custom_emojis });
406 handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
407 const keywords = d.cv.keywords
409 .filter(x => x.length > 0) as string[];
410 const uniqueKeywords = Array.from(new Set(keywords));
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(),
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(),
432 handleAddEmojiClick(form: EmojiForm, event: any) {
433 event.preventDefault();
434 const custom_emojis = [...form.state.customEmojis];
436 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
437 const item: CustomEmojiViewForm = {
447 custom_emojis.push(item);
448 form.setState({ customEmojis: custom_emojis, page: page });
451 handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
454 event.preventDefault();
455 file = event.target.files[0];
460 HttpService.client.uploadImage({ image: file }).then(res => {
461 console.log("pictrs upload:");
463 if (res.state === "success") {
464 if (res.data.msg === "ok") {
465 pictrsDeleteToast(file.name, res.data.delete_url as string);
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 },
475 } else if (res.state === "failed") {
476 console.error(res.msg);
477 toast(res.msg, "danger");
482 configurePicker(): any {
484 data: { categories: [], emojis: [], aliases: [] },