+import { myAuthRequired, setIsoData } from "@utils/app";
+import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import {
CreateCustomEmoji,
EditCustomEmoji,
GetSiteResponse,
} from "lemmy-js-client";
-import { i18n } from "../../i18next";
-import { HttpService } from "../../services/HttpService";
-import {
- customEmojisLookup,
- myAuthRequired,
- pictrsDeleteToast,
- setIsoData,
- toast,
-} from "../../utils";
+import { customEmojisLookup } from "../../markdown";
+import { HttpService, I18NextService } from "../../services";
+import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiMart } from "../common/emoji-mart";
import { HtmlTags } from "../common/html-tags";
-import { Icon } from "../common/icon";
+import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
interface EmojiFormProps {
onEdit(form: EditCustomEmoji): void;
onCreate(form: CreateCustomEmoji): void;
onDelete(form: DeleteCustomEmoji): void;
- loading: boolean;
}
interface EmojiFormState {
keywords: string;
changed: boolean;
page: number;
+ loading: boolean;
}
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
keywords: x.keywords.map(x => x.keyword).join(" "),
changed: false,
page: 1 + Math.floor(index / this.itemsPerPage),
+ loading: false,
})),
page: 1,
};
this.handleEmojiClick = this.handleEmojiClick.bind(this);
}
get documentTitle(): string {
- return i18n.t("custom_emojis");
+ return I18NextService.i18n.t("custom_emojis");
}
render() {
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
+ <h5 className="col-12">{I18NextService.i18n.t("custom_emojis")}</h5>
{customEmojisLookup.size > 0 && (
<div>
<EmojiMart
</div>
)}
<div className="table-responsive">
- <table id="emojis_table" className="table table-sm table-hover">
+ <table
+ id="emojis_table"
+ className="table table-sm table-hover align-middle"
+ >
<thead className="pointer">
<tr>
- <th>{i18n.t("column_emoji")}</th>
- <th className="text-right">{i18n.t("column_shortcode")}</th>
- <th className="text-right">{i18n.t("column_category")}</th>
+ <th>{I18NextService.i18n.t("column_emoji")}</th>
+ <th className="text-right">
+ {I18NextService.i18n.t("column_shortcode")}
+ </th>
+ <th className="text-right">
+ {I18NextService.i18n.t("column_category")}
+ </th>
<th className="text-right d-lg-table-cell d-none">
- {i18n.t("column_imageurl")}
+ {I18NextService.i18n.t("column_imageurl")}
+ </th>
+ <th className="text-right">
+ {I18NextService.i18n.t("column_alttext")}
</th>
- <th className="text-right">{i18n.t("column_alttext")}</th>
<th className="text-right d-lg-table-cell">
- {i18n.t("column_keywords")}
+ {I18NextService.i18n.t("column_keywords")}
</th>
<th style="width:121px"></th>
</tr>
.map((cv, index) => (
<tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
<td style="text-align:center;">
- <label
- htmlFor={index.toString()}
- className="pointer text-muted small font-weight-bold"
- >
- {cv.image_url.length > 0 && (
- <img
- className="icon-emoji-admin"
- src={cv.image_url}
+ {cv.image_url.length > 0 && (
+ <img
+ className="icon-emoji-admin"
+ src={cv.image_url}
+ alt={cv.alt_text}
+ />
+ )}
+ {cv.image_url.length === 0 && (
+ <label
+ // TODO: Fix this linting violation
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
+ tabIndex={0}
+ className="btn btn-sm btn-secondary pointer"
+ htmlFor={`file-uploader-${index}`}
+ data-tippy-content={I18NextService.i18n.t(
+ "upload_image"
+ )}
+ >
+ {capitalizeFirstLetter(
+ I18NextService.i18n.t("upload")
+ )}
+ <input
+ name={`file-uploader-${index}`}
+ id={`file-uploader-${index}`}
+ type="file"
+ accept="image/*"
+ className="d-none"
+ onChange={linkEvent(
+ { form: this, index: index },
+ this.handleImageUpload
+ )}
/>
- )}
- {cv.image_url.length == 0 && (
- <span className="btn btn-sm btn-secondary">
- Upload
- </span>
- )}
- </label>
- <input
- name={index.toString()}
- id={index.toString()}
- type="file"
- accept="image/*"
- className="d-none"
- onChange={linkEvent(
- { form: this, index: index },
- this.handleImageUpload
- )}
- />
+ </label>
+ )}
</td>
<td className="text-right">
<input
<span title={this.getEditTooltip(cv)}>
<button
className={
- (cv.changed ? "text-success " : "text-muted ") +
- "btn btn-link btn-animate"
+ (this.canEdit(cv)
+ ? "text-success "
+ : "text-muted ") + "btn btn-link btn-animate"
}
onClick={linkEvent(
{ i: this, cv: cv },
this.handleEditEmojiClick
)}
- data-tippy-content={i18n.t("save")}
- aria-label={i18n.t("save")}
- disabled={
- this.props.loading ||
- !this.canEdit(cv) ||
- !cv.changed
- }
+ data-tippy-content={I18NextService.i18n.t("save")}
+ aria-label={I18NextService.i18n.t("save")}
+ disabled={!this.canEdit(cv)}
>
- {/* <Icon
- icon="edit"
- classes={`icon-inline`}
- /> */}
- Save
+ {cv.loading ? (
+ <Spinner />
+ ) : (
+ capitalizeFirstLetter(
+ I18NextService.i18n.t("save")
+ )
+ )}
</button>
</span>
<button
{ i: this, index: index, cv: cv },
this.handleDeleteEmojiClick
)}
- data-tippy-content={i18n.t("delete")}
- aria-label={i18n.t("delete")}
- disabled={this.props.loading}
- title={i18n.t("delete")}
+ data-tippy-content={I18NextService.i18n.t("delete")}
+ aria-label={I18NextService.i18n.t("delete")}
+ disabled={cv.loading}
+ title={I18NextService.i18n.t("delete")}
>
<Icon
icon="trash"
- classes={`icon-inline text-danger`}
+ classes="icon-inline text-danger"
/>
</button>
</div>
className="btn btn-sm btn-secondary me-2"
onClick={linkEvent(this, this.handleAddEmojiClick)}
>
- {i18n.t("add_custom_emoji")}
+ {I18NextService.i18n.t("add_custom_emoji")}
</button>
<Paginator page={this.state.page} onChange={this.handlePageChange} />
this.state.customEmojis.filter(
x => x.shortcode == cv.shortcode && x.id != cv.id
).length == 0;
- return noEmptyFields && noDuplicateShortCodes;
+ return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
}
getEditTooltip(cv: CustomEmojiViewForm) {
- if (this.canEdit(cv)) return i18n.t("save");
- else return i18n.t("custom_emoji_save_validation");
+ if (this.canEdit(cv)) return I18NextService.i18n.t("save");
+ else return I18NextService.i18n.t("custom_emoji_save_validation");
}
handlePageChange(page: number) {
}
handleEmojiImageUrlChange(
- props: { form: EmojiForm; index: number; overrideValue: string | null },
+ {
+ form,
+ index,
+ overrideValue,
+ }: { form: EmojiForm; index: number; overrideValue: string | null },
event: any
) {
- const custom_emojis = [...props.form.state.customEmojis];
- const pagedIndex =
- (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- const item = {
- ...props.form.state.customEmojis[pagedIndex],
- image_url: props.overrideValue ?? event.target.value,
- changed: true,
- };
- custom_emojis[Number(pagedIndex)] = item;
- props.form.setState({ customEmojis: custom_emojis });
+ form.setState(prevState => {
+ const custom_emojis = [...form.state.customEmojis];
+ const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
+ const item = {
+ ...form.state.customEmojis[pagedIndex],
+ image_url: overrideValue ?? event.target.value,
+ changed: true,
+ };
+ custom_emojis[Number(pagedIndex)] = item;
+ return {
+ ...prevState,
+ customEmojis: prevState.customEmojis.map((ce, i) =>
+ i === pagedIndex
+ ? {
+ ...ce,
+ image_url: overrideValue ?? event.target.value,
+ changed: true,
+ loading: false,
+ }
+ : ce
+ ),
+ };
+ });
}
handleEmojiAltTextChange(
.split(" ")
.filter(x => x.length > 0) as string[];
const uniqueKeywords = Array.from(new Set(keywords));
- if (d.cv.id != 0) {
+ if (d.cv.id !== 0) {
d.i.props.onEdit({
id: d.cv.id,
category: d.cv.category,
handleAddEmojiClick(form: EmojiForm, event: any) {
event.preventDefault();
- const custom_emojis = [...form.state.customEmojis];
- const page =
- 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
- const item: CustomEmojiViewForm = {
- id: 0,
- shortcode: "",
- alt_text: "",
- category: "",
- image_url: "",
- keywords: "",
- changed: true,
- page: page,
- };
- custom_emojis.push(item);
- form.setState({ customEmojis: custom_emojis, page: page });
+ form.setState(prevState => {
+ const page =
+ 1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
+ const item: CustomEmojiViewForm = {
+ id: 0,
+ shortcode: "",
+ alt_text: "",
+ category: "",
+ image_url: "",
+ keywords: "",
+ changed: false,
+ page: page,
+ loading: false,
+ };
+
+ return {
+ ...prevState,
+ customEmojis: [...prevState.customEmojis, item],
+ page,
+ };
+ });
}
- handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
+ handleImageUpload(
+ { form, index }: { form: EmojiForm; index: number },
+ event: any
+ ) {
let file: any;
if (event.target) {
event.preventDefault();
file = event;
}
+ form.setState(prevState => ({
+ ...prevState,
+ customEmojis: prevState.customEmojis.map((cv, i) =>
+ i === index ? { ...cv, loading: true } : cv
+ ),
+ }));
+
HttpService.client.uploadImage({ image: file }).then(res => {
console.log("pictrs upload:");
console.log(res);
if (res.state === "success") {
if (res.data.msg === "ok") {
pictrsDeleteToast(file.name, res.data.delete_url as string);
- } else {
- toast(JSON.stringify(res), "danger");
- const hash = res.data.files?.at(0)?.file;
- const url = `${res.data.url}/${hash}`;
- props.form.handleEmojiImageUrlChange(
- { form: props.form, index: props.index, overrideValue: url },
+ form.handleEmojiImageUrlChange(
+ { form: form, index: index, overrideValue: res.data.url as string },
event
);
+ } else if (res.data.msg === "too_large") {
+ toast(I18NextService.i18n.t("upload_too_large"), "danger");
+ } else {
+ toast(JSON.stringify(res), "danger");
}
} else if (res.state === "failed") {
console.error(res.msg);