1 import { Component, linkEvent } from "inferno";
7 } from "lemmy-js-client";
12 DeleteCustomEmojiResponse,
14 } from "lemmy-js-client/dist/interfaces/api/custom_emoji";
15 import { Subscription } from "rxjs";
16 import { i18n } from "../../i18next";
17 import { WebSocketService } from "../../services";
23 removeFromEmojiDataModel,
31 import { EmojiMart } from "../common/emoji-mart";
32 import { HtmlTags } from "../common/html-tags";
33 import { Icon } from "../common/icon";
34 import { Paginator } from "../common/paginator";
36 interface EmojiFormState {
37 siteRes: GetSiteResponse;
38 customEmojis: CustomEmojiViewForm[];
43 interface CustomEmojiViewForm {
54 export class EmojiForm extends Component<any, EmojiFormState> {
55 private isoData = setIsoData(this.context);
56 private subscription: Subscription | undefined;
57 private itemsPerPage = 15;
58 private emptyState: EmojiFormState = {
60 siteRes: this.isoData.site_res,
61 customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
62 id: x.custom_emoji.id,
63 category: x.custom_emoji.category,
64 shortcode: x.custom_emoji.shortcode,
65 image_url: x.custom_emoji.image_url,
66 alt_text: x.custom_emoji.alt_text,
67 keywords: x.keywords.map(x => x.keyword).join(" "),
69 page: 1 + Math.floor(index / this.itemsPerPage),
73 state: EmojiFormState;
74 private scrollRef: any = {};
75 constructor(props: any, context: any) {
76 super(props, context);
77 this.state = this.emptyState;
79 this.handlePageChange = this.handlePageChange.bind(this);
80 this.parseMessage = this.parseMessage.bind(this);
81 this.handleEmojiClick = this.handleEmojiClick.bind(this);
82 this.subscription = wsSubscribe(this.parseMessage);
84 get documentTitle(): string {
85 return i18n.t("custom_emojis");
88 componentWillUnmount() {
90 this.subscription?.unsubscribe();
96 <div className="col-12">
98 title={this.documentTitle}
99 path={this.context.router.route.match.url}
101 <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
102 {customEmojisLookup.size > 0 && (
105 onEmojiClick={this.handleEmojiClick}
106 pickerOptions={this.configurePicker()}
110 <div className="table-responsive">
111 <table id="emojis_table" className="table table-sm table-hover">
112 <thead className="pointer">
114 <th>{i18n.t("column_emoji")}</th>
115 <th className="text-right">{i18n.t("column_shortcode")}</th>
116 <th className="text-right">{i18n.t("column_category")}</th>
117 <th className="text-right d-lg-table-cell d-none">
118 {i18n.t("column_imageurl")}
120 <th className="text-right">{i18n.t("column_alttext")}</th>
121 <th className="text-right d-lg-table-cell">
122 {i18n.t("column_keywords")}
124 <th style="width:121px"></th>
128 {this.state.customEmojis
130 (this.state.page - 1) * this.itemsPerPage,
131 (this.state.page - 1) * this.itemsPerPage + this.itemsPerPage
133 .map((cv, index) => (
134 <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
135 <td style="text-align:center;">
137 htmlFor={index.toString()}
138 className="pointer text-muted small font-weight-bold"
140 {cv.image_url.length > 0 && (
142 className="icon-emoji-admin"
146 {cv.image_url.length == 0 && (
147 <span className="btn btn-sm btn-secondary">
153 name={index.toString()}
154 id={index.toString()}
159 { form: this, index: index },
160 this.handleImageUpload
164 <td className="text-right">
167 placeholder="ShortCode"
168 className="form-control"
172 { form: this, index: index },
173 this.handleEmojiShortCodeChange
177 <td className="text-right">
180 placeholder="Category"
181 className="form-control"
184 { form: this, index: index },
185 this.handleEmojiCategoryChange
189 <td className="text-right d-lg-table-cell d-none">
193 className="form-control"
196 { form: this, index: index, overrideValue: null },
197 this.handleEmojiImageUrlChange
201 <td className="text-right">
204 placeholder="Alt Text"
205 className="form-control"
208 { form: this, index: index },
209 this.handleEmojiAltTextChange
213 <td className="text-right d-lg-table-cell">
216 placeholder="Keywords"
217 className="form-control"
220 { form: this, index: index },
221 this.handleEmojiKeywordChange
227 <span title={this.getEditTooltip(cv)}>
230 (cv.changed ? "text-success " : "text-muted ") +
231 "btn btn-link btn-animate"
234 { form: this, cv: cv },
235 this.handleEditEmojiClick
237 data-tippy-content={i18n.t("save")}
238 aria-label={i18n.t("save")}
240 this.state.loading ||
247 classes={`icon-inline`}
253 className="btn btn-link btn-animate text-muted"
255 { form: this, index: index, cv: cv },
256 this.handleDeleteEmojiClick
258 data-tippy-content={i18n.t("delete")}
259 aria-label={i18n.t("delete")}
260 disabled={this.state.loading}
261 title={i18n.t("delete")}
265 classes={`icon-inline text-danger`}
276 className="btn btn-sm btn-secondary mr-2"
277 onClick={linkEvent(this, this.handleAddEmojiClick)}
279 {i18n.t("add_custom_emoji")}
282 <Paginator page={this.state.page} onChange={this.handlePageChange} />
288 canEdit(cv: CustomEmojiViewForm) {
289 const noEmptyFields =
290 cv.alt_text.length > 0 &&
291 cv.category.length > 0 &&
292 cv.image_url.length > 0 &&
293 cv.shortcode.length > 0;
294 const noDuplicateShortCodes =
295 this.state.customEmojis.filter(
296 x => x.shortcode == cv.shortcode && x.id != cv.id
298 return noEmptyFields && noDuplicateShortCodes;
301 getEditTooltip(cv: CustomEmojiViewForm) {
302 if (this.canEdit(cv)) return i18n.t("save");
303 else return i18n.t("custom_emoji_save_validation");
306 handlePageChange(page: number) {
307 this.setState({ page: page });
310 handleEmojiClick(e: any) {
311 const view = customEmojisLookup.get(e.id);
313 const page = this.state.customEmojis.find(
314 x => x.id == view.custom_emoji.id
317 this.setState({ page: page });
318 this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
323 handleEmojiCategoryChange(
324 props: { form: EmojiForm; index: number },
327 let custom_emojis = [...props.form.state.customEmojis];
329 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
331 ...props.form.state.customEmojis[pagedIndex],
332 category: event.target.value,
335 custom_emojis[pagedIndex] = item;
336 props.form.setState({ customEmojis: custom_emojis });
339 handleEmojiShortCodeChange(
340 props: { form: EmojiForm; index: number },
343 let custom_emojis = [...props.form.state.customEmojis];
345 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
347 ...props.form.state.customEmojis[pagedIndex],
348 shortcode: event.target.value,
351 custom_emojis[pagedIndex] = item;
352 props.form.setState({ customEmojis: custom_emojis });
355 handleEmojiImageUrlChange(
356 props: { form: EmojiForm; index: number; overrideValue: string | null },
359 let custom_emojis = [...props.form.state.customEmojis];
361 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
363 ...props.form.state.customEmojis[pagedIndex],
364 image_url: props.overrideValue ?? event.target.value,
367 custom_emojis[pagedIndex] = item;
368 props.form.setState({ customEmojis: custom_emojis });
371 handleEmojiAltTextChange(
372 props: { form: EmojiForm; index: number },
375 let custom_emojis = [...props.form.state.customEmojis];
377 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
379 ...props.form.state.customEmojis[pagedIndex],
380 alt_text: event.target.value,
383 custom_emojis[pagedIndex] = item;
384 props.form.setState({ customEmojis: custom_emojis });
387 handleEmojiKeywordChange(
388 props: { form: EmojiForm; index: number },
391 let custom_emojis = [...props.form.state.customEmojis];
393 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
395 ...props.form.state.customEmojis[pagedIndex],
396 keywords: event.target.value,
399 custom_emojis[pagedIndex] = item;
400 props.form.setState({ customEmojis: custom_emojis });
403 handleDeleteEmojiClick(props: {
406 cv: CustomEmojiViewForm;
409 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
410 if (props.cv.id != 0) {
411 const deleteForm: DeleteCustomEmoji = {
413 auth: myAuth() ?? "",
415 WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm));
417 let custom_emojis = [...props.form.state.customEmojis];
418 custom_emojis.splice(pagedIndex, 1);
419 props.form.setState({ customEmojis: custom_emojis });
423 handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
424 const keywords = props.cv.keywords
426 .filter(x => x.length > 0) as string[];
427 const uniqueKeywords = Array.from(new Set(keywords));
428 if (props.cv.id != 0) {
429 const editForm: EditCustomEmoji = {
431 category: props.cv.category,
432 image_url: props.cv.image_url,
433 alt_text: props.cv.alt_text,
434 keywords: uniqueKeywords,
435 auth: myAuth() ?? "",
437 WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
439 const createForm: CreateCustomEmoji = {
440 category: props.cv.category,
441 shortcode: props.cv.shortcode,
442 image_url: props.cv.image_url,
443 alt_text: props.cv.alt_text,
444 keywords: uniqueKeywords,
445 auth: myAuth() ?? "",
447 WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
451 handleAddEmojiClick(form: EmojiForm, event: any) {
452 event.preventDefault();
453 let custom_emojis = [...form.state.customEmojis];
455 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
456 let item: CustomEmojiViewForm = {
466 custom_emojis.push(item);
467 form.setState({ customEmojis: custom_emojis, page: page });
470 handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
473 event.preventDefault();
474 file = event.target.files[0];
481 console.log("pictrs upload:");
483 if (res.msg === "ok") {
485 `${i18n.t("click_to_delete_picture")}: ${file.name}`,
486 `${i18n.t("picture_deleted")}: ${file.name}`,
487 `${i18n.t("failed_to_delete_picture")}: ${file.name}`,
488 res.delete_url as string
491 toast(JSON.stringify(res), "danger");
492 let hash = res.files?.at(0)?.file;
493 let url = `${res.url}/${hash}`;
494 props.form.handleEmojiImageUrlChange(
495 { form: props.form, index: props.index, overrideValue: url },
501 console.error(error);
502 toast(error, "danger");
506 configurePicker(): any {
508 data: { categories: [], emojis: [], aliases: [] },
514 parseMessage(msg: any) {
515 let op = wsUserOp(msg);
518 toast(i18n.t(msg.error), "danger");
519 this.context.router.history.push("/");
520 this.setState({ loading: false });
522 } else if (op == UserOperation.CreateCustomEmoji) {
523 let data = wsJsonToRes<CustomEmojiResponse>(msg);
524 const custom_emoji_view = data.custom_emoji;
525 updateEmojiDataModel(custom_emoji_view);
526 let currentEmojis = this.state.customEmojis;
527 let newEmojiIndex = currentEmojis.findIndex(
528 x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
530 currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id;
531 currentEmojis[newEmojiIndex].changed = false;
532 this.setState({ customEmojis: currentEmojis });
533 toast(i18n.t("saved_emoji"));
534 this.setState({ loading: false });
535 } else if (op == UserOperation.EditCustomEmoji) {
536 let data = wsJsonToRes<CustomEmojiResponse>(msg);
537 const custom_emoji_view = data.custom_emoji;
538 updateEmojiDataModel(data.custom_emoji);
539 let currentEmojis = this.state.customEmojis;
540 let newEmojiIndex = currentEmojis.findIndex(
541 x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
543 currentEmojis[newEmojiIndex].changed = false;
544 this.setState({ customEmojis: currentEmojis });
545 toast(i18n.t("saved_emoji"));
546 this.setState({ loading: false });
547 } else if (op == UserOperation.DeleteCustomEmoji) {
548 let data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
550 removeFromEmojiDataModel(data.id);
551 let custom_emojis = [
552 ...this.state.customEmojis.filter(x => x.id != data.id),
554 this.setState({ customEmojis: custom_emojis });
555 toast(i18n.t("deleted_emoji"));
557 this.setState({ loading: false });