+import { myAuthRequired, setIsoData } from "@utils/app";
+import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import {
CreateCustomEmoji,
- CustomEmojiResponse,
DeleteCustomEmoji,
- DeleteCustomEmojiResponse,
EditCustomEmoji,
GetSiteResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
-import { i18n } from "../../i18next";
-import { WebSocketService } from "../../services";
-import {
- customEmojisLookup,
- isBrowser,
- myAuth,
- pictrsDeleteToast,
- removeFromEmojiDataModel,
- setIsoData,
- toast,
- updateEmojiDataModel,
- uploadImage,
- wsClient,
- wsSubscribe,
-} 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;
+}
+
interface EmojiFormState {
siteRes: GetSiteResponse;
customEmojis: CustomEmojiViewForm[];
- loading: boolean;
page: number;
}
keywords: string;
changed: boolean;
page: number;
+ loading: boolean;
}
-export class EmojiForm extends Component<any, EmojiFormState> {
+export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
private isoData = setIsoData(this.context);
- private subscription: Subscription | undefined;
private itemsPerPage = 15;
private emptyState: EmojiFormState = {
- loading: false,
siteRes: this.isoData.site_res,
customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
id: x.custom_emoji.id,
keywords: x.keywords.map(x => x.keyword).join(" "),
changed: false,
page: 1 + Math.floor(index / this.itemsPerPage),
+ loading: false,
})),
page: 1,
};
this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
this.handleEmojiClick = this.handleEmojiClick.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
}
get documentTitle(): string {
- return i18n.t("custom_emojis");
- }
-
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
- }
+ return I18NextService.i18n.t("custom_emojis");
}
render() {
return (
- <div className="col-12">
+ <div className="home-emojis-form col-12">
<HtmlTags
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(
- { form: this, cv: cv },
+ { i: this, cv: cv },
this.handleEditEmojiClick
)}
- data-tippy-content={i18n.t("save")}
- aria-label={i18n.t("save")}
- disabled={
- this.state.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
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
- { form: this, index: index, cv: cv },
+ { i: this, index: index, cv: cv },
this.handleDeleteEmojiClick
)}
- data-tippy-content={i18n.t("delete")}
- aria-label={i18n.t("delete")}
- disabled={this.state.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>
</table>
<br />
<button
- className="btn btn-sm btn-secondary mr-2"
+ 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(
props.form.setState({ customEmojis: custom_emojis });
}
- handleDeleteEmojiClick(props: {
- form: EmojiForm;
+ handleDeleteEmojiClick(d: {
+ i: EmojiForm;
index: number;
cv: CustomEmojiViewForm;
}) {
- const pagedIndex =
- (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- if (props.cv.id != 0) {
- const deleteForm: DeleteCustomEmoji = {
- id: props.cv.id,
- auth: myAuth() ?? "",
- };
- WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm));
+ const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
+ if (d.cv.id != 0) {
+ d.i.props.onDelete({
+ id: d.cv.id,
+ auth: myAuthRequired(),
+ });
} else {
- const custom_emojis = [...props.form.state.customEmojis];
+ const custom_emojis = [...d.i.state.customEmojis];
custom_emojis.splice(Number(pagedIndex), 1);
- props.form.setState({ customEmojis: custom_emojis });
+ d.i.setState({ customEmojis: custom_emojis });
}
}
- handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
- const keywords = props.cv.keywords
+ handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
+ const keywords = d.cv.keywords
.split(" ")
.filter(x => x.length > 0) as string[];
const uniqueKeywords = Array.from(new Set(keywords));
- if (props.cv.id != 0) {
- const editForm: EditCustomEmoji = {
- id: props.cv.id,
- category: props.cv.category,
- image_url: props.cv.image_url,
- alt_text: props.cv.alt_text,
+ if (d.cv.id !== 0) {
+ d.i.props.onEdit({
+ id: d.cv.id,
+ category: d.cv.category,
+ image_url: d.cv.image_url,
+ alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
- auth: myAuth() ?? "",
- };
- WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
+ auth: myAuthRequired(),
+ });
} else {
- const createForm: CreateCustomEmoji = {
- category: props.cv.category,
- shortcode: props.cv.shortcode,
- image_url: props.cv.image_url,
- alt_text: props.cv.alt_text,
+ d.i.props.onCreate({
+ category: d.cv.category,
+ shortcode: d.cv.shortcode,
+ image_url: d.cv.image_url,
+ alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
- auth: myAuth() ?? "",
- };
- WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
+ auth: myAuthRequired(),
+ });
}
}
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;
}
- uploadImage(file)
- .then(res => {
- console.log("pictrs upload:");
- console.log(res);
- if (res.msg === "ok") {
- pictrsDeleteToast(file.name, res.delete_url as string);
- } else {
- toast(JSON.stringify(res), "danger");
- const hash = res.files?.at(0)?.file;
- const url = `${res.url}/${hash}`;
- props.form.handleEmojiImageUrlChange(
- { form: props.form, index: props.index, overrideValue: url },
+ 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);
+ 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");
}
- })
- .catch(error => {
- console.error(error);
- toast(error, "danger");
- });
+ } else if (res.state === "failed") {
+ console.error(res.msg);
+ toast(res.msg, "danger");
+ }
+ });
}
configurePicker(): any {
dynamicWidth: true,
};
}
-
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.CreateCustomEmoji) {
- const data = wsJsonToRes<CustomEmojiResponse>(msg);
- const custom_emoji_view = data.custom_emoji;
- updateEmojiDataModel(custom_emoji_view);
- const currentEmojis = this.state.customEmojis;
- const newEmojiIndex = currentEmojis.findIndex(
- x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
- );
- currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id;
- currentEmojis[newEmojiIndex].changed = false;
- this.setState({ customEmojis: currentEmojis });
- toast(i18n.t("saved_emoji"));
- this.setState({ loading: false });
- } else if (op == UserOperation.EditCustomEmoji) {
- const data = wsJsonToRes<CustomEmojiResponse>(msg);
- const custom_emoji_view = data.custom_emoji;
- updateEmojiDataModel(data.custom_emoji);
- const currentEmojis = this.state.customEmojis;
- const newEmojiIndex = currentEmojis.findIndex(
- x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
- );
- currentEmojis[newEmojiIndex].changed = false;
- this.setState({ customEmojis: currentEmojis });
- toast(i18n.t("saved_emoji"));
- this.setState({ loading: false });
- } else if (op == UserOperation.DeleteCustomEmoji) {
- const data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
- if (data.success) {
- removeFromEmojiDataModel(data.id);
- const custom_emojis = [
- ...this.state.customEmojis.filter(x => x.id != data.id),
- ];
- this.setState({ customEmojis: custom_emojis });
- toast(i18n.t("deleted_emoji"));
- }
- this.setState({ loading: false });
- }
- }
}