1 import { Component, linkEvent } from "inferno";
6 DeleteCustomEmojiResponse,
12 } from "lemmy-js-client";
13 import { Subscription } from "rxjs";
14 import { i18n } from "../../i18next";
15 import { WebSocketService } from "../../services";
21 removeFromEmojiDataModel,
29 import { EmojiMart } from "../common/emoji-mart";
30 import { HtmlTags } from "../common/html-tags";
31 import { Icon } from "../common/icon";
32 import { Paginator } from "../common/paginator";
34 interface EmojiFormState {
35 siteRes: GetSiteResponse;
36 customEmojis: CustomEmojiViewForm[];
41 interface CustomEmojiViewForm {
52 export class EmojiForm extends Component<any, EmojiFormState> {
53 private isoData = setIsoData(this.context);
54 private subscription: Subscription | undefined;
55 private itemsPerPage = 15;
56 private emptyState: EmojiFormState = {
58 siteRes: this.isoData.site_res,
59 customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
60 id: x.custom_emoji.id,
61 category: x.custom_emoji.category,
62 shortcode: x.custom_emoji.shortcode,
63 image_url: x.custom_emoji.image_url,
64 alt_text: x.custom_emoji.alt_text,
65 keywords: x.keywords.map(x => x.keyword).join(" "),
67 page: 1 + Math.floor(index / this.itemsPerPage),
71 state: EmojiFormState;
72 private scrollRef: any = {};
73 constructor(props: any, context: any) {
74 super(props, context);
75 this.state = this.emptyState;
77 this.handlePageChange = this.handlePageChange.bind(this);
78 this.parseMessage = this.parseMessage.bind(this);
79 this.handleEmojiClick = this.handleEmojiClick.bind(this);
80 this.subscription = wsSubscribe(this.parseMessage);
82 get documentTitle(): string {
83 return i18n.t("custom_emojis");
86 componentWillUnmount() {
88 this.subscription?.unsubscribe();
94 <div className="col-12">
96 title={this.documentTitle}
97 path={this.context.router.route.match.url}
99 <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
100 {customEmojisLookup.size > 0 && (
103 onEmojiClick={this.handleEmojiClick}
104 pickerOptions={this.configurePicker()}
108 <div className="table-responsive">
109 <table id="emojis_table" className="table table-sm table-hover">
110 <thead className="pointer">
112 <th>{i18n.t("column_emoji")}</th>
113 <th className="text-right">{i18n.t("column_shortcode")}</th>
114 <th className="text-right">{i18n.t("column_category")}</th>
115 <th className="text-right d-lg-table-cell d-none">
116 {i18n.t("column_imageurl")}
118 <th className="text-right">{i18n.t("column_alttext")}</th>
119 <th className="text-right d-lg-table-cell">
120 {i18n.t("column_keywords")}
122 <th style="width:121px"></th>
126 {this.state.customEmojis
128 Number((this.state.page - 1) * this.itemsPerPage),
130 (this.state.page - 1) * this.itemsPerPage +
134 .map((cv, index) => (
135 <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
136 <td style="text-align:center;">
138 htmlFor={index.toString()}
139 className="pointer text-muted small font-weight-bold"
141 {cv.image_url.length > 0 && (
143 className="icon-emoji-admin"
147 {cv.image_url.length == 0 && (
148 <span className="btn btn-sm btn-secondary">
154 name={index.toString()}
155 id={index.toString()}
160 { form: this, index: index },
161 this.handleImageUpload
165 <td className="text-right">
168 placeholder="ShortCode"
169 className="form-control"
173 { form: this, index: index },
174 this.handleEmojiShortCodeChange
178 <td className="text-right">
181 placeholder="Category"
182 className="form-control"
185 { form: this, index: index },
186 this.handleEmojiCategoryChange
190 <td className="text-right d-lg-table-cell d-none">
194 className="form-control"
197 { form: this, index: index, overrideValue: null },
198 this.handleEmojiImageUrlChange
202 <td className="text-right">
205 placeholder="Alt Text"
206 className="form-control"
209 { form: this, index: index },
210 this.handleEmojiAltTextChange
214 <td className="text-right d-lg-table-cell">
217 placeholder="Keywords"
218 className="form-control"
221 { form: this, index: index },
222 this.handleEmojiKeywordChange
228 <span title={this.getEditTooltip(cv)}>
231 (cv.changed ? "text-success " : "text-muted ") +
232 "btn btn-link btn-animate"
235 { form: this, cv: cv },
236 this.handleEditEmojiClick
238 data-tippy-content={i18n.t("save")}
239 aria-label={i18n.t("save")}
241 this.state.loading ||
248 classes={`icon-inline`}
254 className="btn btn-link btn-animate text-muted"
256 { form: this, index: index, cv: cv },
257 this.handleDeleteEmojiClick
259 data-tippy-content={i18n.t("delete")}
260 aria-label={i18n.t("delete")}
261 disabled={this.state.loading}
262 title={i18n.t("delete")}
266 classes={`icon-inline text-danger`}
277 className="btn btn-sm btn-secondary mr-2"
278 onClick={linkEvent(this, this.handleAddEmojiClick)}
280 {i18n.t("add_custom_emoji")}
283 <Paginator page={this.state.page} onChange={this.handlePageChange} />
289 canEdit(cv: CustomEmojiViewForm) {
290 const noEmptyFields =
291 cv.alt_text.length > 0 &&
292 cv.category.length > 0 &&
293 cv.image_url.length > 0 &&
294 cv.shortcode.length > 0;
295 const noDuplicateShortCodes =
296 this.state.customEmojis.filter(
297 x => x.shortcode == cv.shortcode && x.id != cv.id
299 return noEmptyFields && noDuplicateShortCodes;
302 getEditTooltip(cv: CustomEmojiViewForm) {
303 if (this.canEdit(cv)) return i18n.t("save");
304 else return i18n.t("custom_emoji_save_validation");
307 handlePageChange(page: number) {
308 this.setState({ page: page });
311 handleEmojiClick(e: any) {
312 const view = customEmojisLookup.get(e.id);
314 const page = this.state.customEmojis.find(
315 x => x.id == view.custom_emoji.id
318 this.setState({ page: page });
319 this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
324 handleEmojiCategoryChange(
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 category: event.target.value,
336 custom_emojis[Number(pagedIndex)] = item;
337 props.form.setState({ customEmojis: custom_emojis });
340 handleEmojiShortCodeChange(
341 props: { form: EmojiForm; index: number },
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 shortcode: event.target.value,
352 custom_emojis[Number(pagedIndex)] = item;
353 props.form.setState({ customEmojis: custom_emojis });
356 handleEmojiImageUrlChange(
357 props: { form: EmojiForm; index: number; overrideValue: string | null },
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 image_url: props.overrideValue ?? event.target.value,
368 custom_emojis[Number(pagedIndex)] = item;
369 props.form.setState({ customEmojis: custom_emojis });
372 handleEmojiAltTextChange(
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 alt_text: event.target.value,
384 custom_emojis[Number(pagedIndex)] = item;
385 props.form.setState({ customEmojis: custom_emojis });
388 handleEmojiKeywordChange(
389 props: { form: EmojiForm; index: number },
392 const custom_emojis = [...props.form.state.customEmojis];
394 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
396 ...props.form.state.customEmojis[pagedIndex],
397 keywords: event.target.value,
400 custom_emojis[Number(pagedIndex)] = item;
401 props.form.setState({ customEmojis: custom_emojis });
404 handleDeleteEmojiClick(props: {
407 cv: CustomEmojiViewForm;
410 (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
411 if (props.cv.id != 0) {
412 const deleteForm: DeleteCustomEmoji = {
414 auth: myAuth() ?? "",
416 WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm));
418 const custom_emojis = [...props.form.state.customEmojis];
419 custom_emojis.splice(Number(pagedIndex), 1);
420 props.form.setState({ customEmojis: custom_emojis });
424 handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
425 const keywords = props.cv.keywords
427 .filter(x => x.length > 0) as string[];
428 const uniqueKeywords = Array.from(new Set(keywords));
429 if (props.cv.id != 0) {
430 const editForm: EditCustomEmoji = {
432 category: props.cv.category,
433 image_url: props.cv.image_url,
434 alt_text: props.cv.alt_text,
435 keywords: uniqueKeywords,
436 auth: myAuth() ?? "",
438 WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
440 const createForm: CreateCustomEmoji = {
441 category: props.cv.category,
442 shortcode: props.cv.shortcode,
443 image_url: props.cv.image_url,
444 alt_text: props.cv.alt_text,
445 keywords: uniqueKeywords,
446 auth: myAuth() ?? "",
448 WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
452 handleAddEmojiClick(form: EmojiForm, event: any) {
453 event.preventDefault();
454 const custom_emojis = [...form.state.customEmojis];
456 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
457 const item: CustomEmojiViewForm = {
467 custom_emojis.push(item);
468 form.setState({ customEmojis: custom_emojis, page: page });
471 handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
474 event.preventDefault();
475 file = event.target.files[0];
482 console.log("pictrs upload:");
484 if (res.msg === "ok") {
485 pictrsDeleteToast(file.name, res.delete_url as string);
487 toast(JSON.stringify(res), "danger");
488 const hash = res.files?.at(0)?.file;
489 const url = `${res.url}/${hash}`;
490 props.form.handleEmojiImageUrlChange(
491 { form: props.form, index: props.index, overrideValue: url },
497 console.error(error);
498 toast(error, "danger");
502 configurePicker(): any {
504 data: { categories: [], emojis: [], aliases: [] },
510 parseMessage(msg: any) {
511 const op = wsUserOp(msg);
514 toast(i18n.t(msg.error), "danger");
515 this.context.router.history.push("/");
516 this.setState({ loading: false });
518 } else if (op == UserOperation.CreateCustomEmoji) {
519 const data = wsJsonToRes<CustomEmojiResponse>(msg);
520 const custom_emoji_view = data.custom_emoji;
521 updateEmojiDataModel(custom_emoji_view);
522 const currentEmojis = this.state.customEmojis;
523 const newEmojiIndex = currentEmojis.findIndex(
524 x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
526 currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id;
527 currentEmojis[newEmojiIndex].changed = false;
528 this.setState({ customEmojis: currentEmojis });
529 toast(i18n.t("saved_emoji"));
530 this.setState({ loading: false });
531 } else if (op == UserOperation.EditCustomEmoji) {
532 const data = wsJsonToRes<CustomEmojiResponse>(msg);
533 const custom_emoji_view = data.custom_emoji;
534 updateEmojiDataModel(data.custom_emoji);
535 const currentEmojis = this.state.customEmojis;
536 const newEmojiIndex = currentEmojis.findIndex(
537 x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
539 currentEmojis[newEmojiIndex].changed = false;
540 this.setState({ customEmojis: currentEmojis });
541 toast(i18n.t("saved_emoji"));
542 this.setState({ loading: false });
543 } else if (op == UserOperation.DeleteCustomEmoji) {
544 const data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
546 removeFromEmojiDataModel(data.id);
547 const custom_emojis = [
548 ...this.state.customEmojis.filter(x => x.id != data.id),
550 this.setState({ customEmojis: custom_emojis });
551 toast(i18n.t("deleted_emoji"));
553 this.setState({ loading: false });