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: BigInt(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 - 1n) * BigInt(this.itemsPerPage)),
130 (this.state.page - 1n) * BigInt(this.itemsPerPage) +
131 BigInt(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: bigint) {
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 let custom_emojis = [...props.form.state.customEmojis];
330 (props.form.state.page - 1n) * BigInt(props.form.itemsPerPage) +
333 ...props.form.state.customEmojis[Number(pagedIndex)],
334 category: event.target.value,
337 custom_emojis[Number(pagedIndex)] = item;
338 props.form.setState({ customEmojis: custom_emojis });
341 handleEmojiShortCodeChange(
342 props: { form: EmojiForm; index: number },
345 let custom_emojis = [...props.form.state.customEmojis];
347 (props.form.state.page - 1n) * BigInt(props.form.itemsPerPage) +
350 ...props.form.state.customEmojis[Number(pagedIndex)],
351 shortcode: event.target.value,
354 custom_emojis[Number(pagedIndex)] = item;
355 props.form.setState({ customEmojis: custom_emojis });
358 handleEmojiImageUrlChange(
359 props: { form: EmojiForm; index: number; overrideValue: string | null },
362 let custom_emojis = [...props.form.state.customEmojis];
364 (props.form.state.page - 1n) * BigInt(props.form.itemsPerPage) +
367 ...props.form.state.customEmojis[Number(pagedIndex)],
368 image_url: props.overrideValue ?? event.target.value,
371 custom_emojis[Number(pagedIndex)] = item;
372 props.form.setState({ customEmojis: custom_emojis });
375 handleEmojiAltTextChange(
376 props: { form: EmojiForm; index: number },
379 let custom_emojis = [...props.form.state.customEmojis];
381 (props.form.state.page - 1n) * BigInt(props.form.itemsPerPage) +
384 ...props.form.state.customEmojis[Number(pagedIndex)],
385 alt_text: event.target.value,
388 custom_emojis[Number(pagedIndex)] = item;
389 props.form.setState({ customEmojis: custom_emojis });
392 handleEmojiKeywordChange(
393 props: { form: EmojiForm; index: number },
396 let custom_emojis = [...props.form.state.customEmojis];
398 (props.form.state.page - 1n) * BigInt(props.form.itemsPerPage) +
401 ...props.form.state.customEmojis[Number(pagedIndex)],
402 keywords: event.target.value,
405 custom_emojis[Number(pagedIndex)] = item;
406 props.form.setState({ customEmojis: custom_emojis });
409 handleDeleteEmojiClick(props: {
412 cv: CustomEmojiViewForm;
415 (props.form.state.page - 1n) * BigInt(props.form.itemsPerPage) +
417 if (props.cv.id != 0) {
418 const deleteForm: DeleteCustomEmoji = {
420 auth: myAuth() ?? "",
422 WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm));
424 let custom_emojis = [...props.form.state.customEmojis];
425 custom_emojis.splice(Number(pagedIndex), 1);
426 props.form.setState({ customEmojis: custom_emojis });
430 handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
431 const keywords = props.cv.keywords
433 .filter(x => x.length > 0) as string[];
434 const uniqueKeywords = Array.from(new Set(keywords));
435 if (props.cv.id != 0) {
436 const editForm: EditCustomEmoji = {
438 category: props.cv.category,
439 image_url: props.cv.image_url,
440 alt_text: props.cv.alt_text,
441 keywords: uniqueKeywords,
442 auth: myAuth() ?? "",
444 WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
446 const createForm: CreateCustomEmoji = {
447 category: props.cv.category,
448 shortcode: props.cv.shortcode,
449 image_url: props.cv.image_url,
450 alt_text: props.cv.alt_text,
451 keywords: uniqueKeywords,
452 auth: myAuth() ?? "",
454 WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
458 handleAddEmojiClick(form: EmojiForm, event: any) {
459 event.preventDefault();
460 let custom_emojis = [...form.state.customEmojis];
462 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage)
464 let item: CustomEmojiViewForm = {
474 custom_emojis.push(item);
475 form.setState({ customEmojis: custom_emojis, page: page });
478 handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
481 event.preventDefault();
482 file = event.target.files[0];
489 console.log("pictrs upload:");
491 if (res.msg === "ok") {
492 pictrsDeleteToast(file.name, res.delete_url as string);
494 toast(JSON.stringify(res), "danger");
495 let hash = res.files?.at(0)?.file;
496 let url = `${res.url}/${hash}`;
497 props.form.handleEmojiImageUrlChange(
498 { form: props.form, index: props.index, overrideValue: url },
504 console.error(error);
505 toast(error, "danger");
509 configurePicker(): any {
511 data: { categories: [], emojis: [], aliases: [] },
517 parseMessage(msg: any) {
518 let op = wsUserOp(msg);
521 toast(i18n.t(msg.error), "danger");
522 this.context.router.history.push("/");
523 this.setState({ loading: false });
525 } else if (op == UserOperation.CreateCustomEmoji) {
526 let data = wsJsonToRes<CustomEmojiResponse>(msg);
527 const custom_emoji_view = data.custom_emoji;
528 updateEmojiDataModel(custom_emoji_view);
529 let currentEmojis = this.state.customEmojis;
530 let newEmojiIndex = currentEmojis.findIndex(
531 x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
533 currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id;
534 currentEmojis[newEmojiIndex].changed = false;
535 this.setState({ customEmojis: currentEmojis });
536 toast(i18n.t("saved_emoji"));
537 this.setState({ loading: false });
538 } else if (op == UserOperation.EditCustomEmoji) {
539 let data = wsJsonToRes<CustomEmojiResponse>(msg);
540 const custom_emoji_view = data.custom_emoji;
541 updateEmojiDataModel(data.custom_emoji);
542 let currentEmojis = this.state.customEmojis;
543 let newEmojiIndex = currentEmojis.findIndex(
544 x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
546 currentEmojis[newEmojiIndex].changed = false;
547 this.setState({ customEmojis: currentEmojis });
548 toast(i18n.t("saved_emoji"));
549 this.setState({ loading: false });
550 } else if (op == UserOperation.DeleteCustomEmoji) {
551 let data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
553 removeFromEmojiDataModel(data.id);
554 let custom_emojis = [
555 ...this.state.customEmojis.filter(x => x.id != data.id),
557 this.setState({ customEmojis: custom_emojis });
558 toast(i18n.t("deleted_emoji"));
560 this.setState({ loading: false });