1 import { myAuthRequired, setIsoData } from "@utils/app";
2 import { Component, linkEvent } from "inferno";
8 } from "lemmy-js-client";
9 import { customEmojisLookup } from "../../markdown";
10 import { HttpService, I18NextService } from "../../services";
11 import { pictrsDeleteToast, toast } from "../../toast";
12 import { EmojiMart } from "../common/emoji-mart";
13 import { HtmlTags } from "../common/html-tags";
14 import { Icon } from "../common/icon";
15 import { Paginator } from "../common/paginator";
17 interface EmojiFormProps {
18 onEdit(form: EditCustomEmoji): void;
19 onCreate(form: CreateCustomEmoji): void;
20 onDelete(form: DeleteCustomEmoji): void;
24 interface EmojiFormState {
25 siteRes: GetSiteResponse;
26 customEmojis: CustomEmojiViewForm[];
30 interface CustomEmojiViewForm {
41 export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
42 private isoData = setIsoData(this.context);
43 private itemsPerPage = 15;
44 private emptyState: EmojiFormState = {
45 siteRes: this.isoData.site_res,
46 customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
47 id: x.custom_emoji.id,
48 category: x.custom_emoji.category,
49 shortcode: x.custom_emoji.shortcode,
50 image_url: x.custom_emoji.image_url,
51 alt_text: x.custom_emoji.alt_text,
52 keywords: x.keywords.map(x => x.keyword).join(" "),
54 page: 1 + Math.floor(index / this.itemsPerPage),
58 state: EmojiFormState;
59 private scrollRef: any = {};
60 constructor(props: any, context: any) {
61 super(props, context);
62 this.state = this.emptyState;
64 this.handlePageChange = this.handlePageChange.bind(this);
65 this.handleEmojiClick = this.handleEmojiClick.bind(this);
67 get documentTitle(): string {
68 return I18NextService.i18n.t("custom_emojis");
73 <div className="home-emojis-form col-12">
75 title={this.documentTitle}
76 path={this.context.router.route.match.url}
78 <h5 className="col-12">{I18NextService.i18n.t("custom_emojis")}</h5>
79 {customEmojisLookup.size > 0 && (
82 onEmojiClick={this.handleEmojiClick}
83 pickerOptions={this.configurePicker()}
87 <div className="table-responsive">
88 <table id="emojis_table" className="table table-sm table-hover">
89 <thead className="pointer">
91 <th>{I18NextService.i18n.t("column_emoji")}</th>
92 <th className="text-right">
93 {I18NextService.i18n.t("column_shortcode")}
95 <th className="text-right">
96 {I18NextService.i18n.t("column_category")}
98 <th className="text-right d-lg-table-cell d-none">
99 {I18NextService.i18n.t("column_imageurl")}
101 <th className="text-right">
102 {I18NextService.i18n.t("column_alttext")}
104 <th className="text-right d-lg-table-cell">
105 {I18NextService.i18n.t("column_keywords")}
107 <th style="width:121px"></th>
111 {this.state.customEmojis
113 Number((this.state.page - 1) * this.itemsPerPage),
115 (this.state.page - 1) * this.itemsPerPage +
119 .map((cv, index) => (
120 <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
121 <td style="text-align:center;">
123 htmlFor={index.toString()}
124 className="pointer text-muted small font-weight-bold"
126 {cv.image_url.length > 0 && (
129 className="icon-emoji-admin"
133 {cv.image_url.length == 0 && (
134 <span className="btn btn-sm btn-secondary">
140 name={index.toString()}
141 id={index.toString()}
146 { form: this, index: index },
147 this.handleImageUpload
151 <td className="text-right">
154 placeholder="ShortCode"
155 className="form-control"
159 { form: this, index: index },
160 this.handleEmojiShortCodeChange
164 <td className="text-right">
167 placeholder="Category"
168 className="form-control"
171 { form: this, index: index },
172 this.handleEmojiCategoryChange
176 <td className="text-right d-lg-table-cell d-none">
180 className="form-control"
183 { form: this, index: index, overrideValue: null },
184 this.handleEmojiImageUrlChange
188 <td className="text-right">
191 placeholder="Alt Text"
192 className="form-control"
195 { form: this, index: index },
196 this.handleEmojiAltTextChange
200 <td className="text-right d-lg-table-cell">
203 placeholder="Keywords"
204 className="form-control"
207 { form: this, index: index },
208 this.handleEmojiKeywordChange
214 <span title={this.getEditTooltip(cv)}>
217 (cv.changed ? "text-success " : "text-muted ") +
218 "btn btn-link btn-animate"
222 this.handleEditEmojiClick
224 data-tippy-content={I18NextService.i18n.t("save")}
225 aria-label={I18NextService.i18n.t("save")}
227 this.props.loading ||
234 classes={`icon-inline`}
240 className="btn btn-link btn-animate text-muted"
242 { i: this, index: index, cv: cv },
243 this.handleDeleteEmojiClick
245 data-tippy-content={I18NextService.i18n.t("delete")}
246 aria-label={I18NextService.i18n.t("delete")}
247 disabled={this.props.loading}
248 title={I18NextService.i18n.t("delete")}
252 classes={`icon-inline text-danger`}
263 className="btn btn-sm btn-secondary me-2"
264 onClick={linkEvent(this, this.handleAddEmojiClick)}
266 {I18NextService.i18n.t("add_custom_emoji")}
269 <Paginator page={this.state.page} onChange={this.handlePageChange} />
275 canEdit(cv: CustomEmojiViewForm) {
276 const noEmptyFields =
277 cv.alt_text.length > 0 &&
278 cv.category.length > 0 &&
279 cv.image_url.length > 0 &&
280 cv.shortcode.length > 0;
281 const noDuplicateShortCodes =
282 this.state.customEmojis.filter(
283 x => x.shortcode == cv.shortcode && x.id != cv.id
285 return noEmptyFields && noDuplicateShortCodes;
288 getEditTooltip(cv: CustomEmojiViewForm) {
289 if (this.canEdit(cv)) return I18NextService.i18n.t("save");
290 else return I18NextService.i18n.t("custom_emoji_save_validation");
293 handlePageChange(page: number) {
294 this.setState({ page: page });
297 handleEmojiClick(e: any) {
298 const view = customEmojisLookup.get(e.id);
300 const page = this.state.customEmojis.find(
301 x => x.id == view.custom_emoji.id
304 this.setState({ page: page });
305 this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
310 handleEmojiCategoryChange(
311 props: { form: EmojiForm; index: number },
314 const custom_emojis = [...props.form.state.customEmojis];
316 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
318 ...props.form.state.customEmojis[pagedIndex],
319 category: event.target.value,
322 custom_emojis[Number(pagedIndex)] = item;
323 props.form.setState({ customEmojis: custom_emojis });
326 handleEmojiShortCodeChange(
327 props: { form: EmojiForm; index: number },
330 const custom_emojis = [...props.form.state.customEmojis];
332 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
334 ...props.form.state.customEmojis[pagedIndex],
335 shortcode: event.target.value,
338 custom_emojis[Number(pagedIndex)] = item;
339 props.form.setState({ customEmojis: custom_emojis });
342 handleEmojiImageUrlChange(
343 props: { form: EmojiForm; index: number; overrideValue: string | null },
346 const custom_emojis = [...props.form.state.customEmojis];
348 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
350 ...props.form.state.customEmojis[pagedIndex],
351 image_url: props.overrideValue ?? event.target.value,
354 custom_emojis[Number(pagedIndex)] = item;
355 props.form.setState({ customEmojis: custom_emojis });
358 handleEmojiAltTextChange(
359 props: { form: EmojiForm; index: number },
362 const custom_emojis = [...props.form.state.customEmojis];
364 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
366 ...props.form.state.customEmojis[pagedIndex],
367 alt_text: event.target.value,
370 custom_emojis[Number(pagedIndex)] = item;
371 props.form.setState({ customEmojis: custom_emojis });
374 handleEmojiKeywordChange(
375 props: { form: EmojiForm; index: number },
378 const custom_emojis = [...props.form.state.customEmojis];
380 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
382 ...props.form.state.customEmojis[pagedIndex],
383 keywords: event.target.value,
386 custom_emojis[Number(pagedIndex)] = item;
387 props.form.setState({ customEmojis: custom_emojis });
390 handleDeleteEmojiClick(d: {
393 cv: CustomEmojiViewForm;
395 const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
399 auth: myAuthRequired(),
402 const custom_emojis = [...d.i.state.customEmojis];
403 custom_emojis.splice(Number(pagedIndex), 1);
404 d.i.setState({ customEmojis: custom_emojis });
408 handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
409 const keywords = d.cv.keywords
411 .filter(x => x.length > 0) as string[];
412 const uniqueKeywords = Array.from(new Set(keywords));
416 category: d.cv.category,
417 image_url: d.cv.image_url,
418 alt_text: d.cv.alt_text,
419 keywords: uniqueKeywords,
420 auth: myAuthRequired(),
424 category: d.cv.category,
425 shortcode: d.cv.shortcode,
426 image_url: d.cv.image_url,
427 alt_text: d.cv.alt_text,
428 keywords: uniqueKeywords,
429 auth: myAuthRequired(),
434 handleAddEmojiClick(form: EmojiForm, event: any) {
435 event.preventDefault();
436 const custom_emojis = [...form.state.customEmojis];
438 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
439 const item: CustomEmojiViewForm = {
449 custom_emojis.push(item);
450 form.setState({ customEmojis: custom_emojis, page: page });
453 handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
456 event.preventDefault();
457 file = event.target.files[0];
462 HttpService.client.uploadImage({ image: file }).then(res => {
463 console.log("pictrs upload:");
465 if (res.state === "success") {
466 if (res.data.msg === "ok") {
467 pictrsDeleteToast(file.name, res.data.delete_url as string);
469 toast(JSON.stringify(res), "danger");
470 const hash = res.data.files?.at(0)?.file;
471 const url = `${res.data.url}/${hash}`;
472 props.form.handleEmojiImageUrlChange(
473 { form: props.form, index: props.index, overrideValue: url },
477 } else if (res.state === "failed") {
478 console.error(res.msg);
479 toast(res.msg, "danger");
484 configurePicker(): any {
486 data: { categories: [], emojis: [], aliases: [] },