--- /dev/null
-import { GetSiteResponse, UserOperation, wsJsonToRes, wsUserOp } from "lemmy-js-client";
-import { CreateCustomEmoji, CustomEmojiResponse, DeleteCustomEmoji, DeleteCustomEmojiResponse, EditCustomEmoji } from "lemmy-js-client/dist/interfaces/api/custom_emoji";
-import { WebSocketService } from "../../services";
+ import { Component, linkEvent } from "inferno";
-import { customEmojisLookup, isBrowser, myAuth, removeFromEmojiDataModel, setIsoData, toast, updateEmojiDataModel, wsClient, wsSubscribe } from "../../utils";
++import {
++ GetSiteResponse,
++ UserOperation,
++ wsJsonToRes,
++ wsUserOp,
++} from "lemmy-js-client";
++import {
++ CreateCustomEmoji,
++ CustomEmojiResponse,
++ DeleteCustomEmoji,
++ DeleteCustomEmojiResponse,
++ EditCustomEmoji,
++} from "lemmy-js-client/dist/interfaces/api/custom_emoji";
++import { Subscription } from "rxjs";
+ import { i18n } from "../../i18next";
-import { Subscription } from "rxjs";
-import { pictrsUri } from "../../env";
++import { WebSocketService } from "../../services";
++import {
++ customEmojisLookup,
++ isBrowser,
++ myAuth,
++ pictrsDeleteToast,
++ removeFromEmojiDataModel,
++ setIsoData,
++ toast,
++ updateEmojiDataModel,
++ uploadImage,
++ wsClient,
++ wsSubscribe,
++} from "../../utils";
+ import { EmojiMart } from "../common/emoji-mart";
+ import { HtmlTags } from "../common/html-tags";
+ import { Icon } from "../common/icon";
- siteRes: GetSiteResponse;
- customEmojis: CustomEmojiViewForm[];
- loading: boolean;
- page: number;
+ import { Paginator } from "../common/paginator";
+
+ interface EmojiFormState {
- id: number;
- category: string;
- shortcode: string;
- image_url: string;
- alt_text: string;
- keywords: string;
- changed: boolean;
- page: number;
++ siteRes: GetSiteResponse;
++ customEmojis: CustomEmojiViewForm[];
++ loading: boolean;
++ page: number;
+ }
+
+ interface CustomEmojiViewForm {
- 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,
- category: x.custom_emoji.category,
- shortcode: x.custom_emoji.shortcode,
- image_url: x.custom_emoji.image_url,
- alt_text: x.custom_emoji.alt_text,
- keywords: x.keywords.map(x => x.keyword).join(" "),
- changed: false,
- page: 1 + (Math.floor(index / this.itemsPerPage))
- })),
- page: 1
- };
- state: EmojiFormState;
- private scrollRef: any = {};
- constructor(props: any, context: any) {
- super(props, context);
- this.state = this.emptyState;
++ id: number;
++ category: string;
++ shortcode: string;
++ image_url: string;
++ alt_text: string;
++ keywords: string;
++ changed: boolean;
++ page: number;
+ }
+
+ export class EmojiForm extends Component<any, EmojiFormState> {
- 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");
- }
++ 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,
++ category: x.custom_emoji.category,
++ shortcode: x.custom_emoji.shortcode,
++ image_url: x.custom_emoji.image_url,
++ alt_text: x.custom_emoji.alt_text,
++ keywords: x.keywords.map(x => x.keyword).join(" "),
++ changed: false,
++ page: 1 + Math.floor(index / this.itemsPerPage),
++ })),
++ page: 1,
++ };
++ state: EmojiFormState;
++ private scrollRef: any = {};
++ constructor(props: any, context: any) {
++ super(props, context);
++ this.state = this.emptyState;
+
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
- }
++ 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");
++ }
+
- render() {
- return (
- <div className="col-12">
- <HtmlTags
- title={this.documentTitle}
- path={this.context.router.route.match.url}
- />
- <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
- {customEmojisLookup.size > 0 && <div>
- <EmojiMart onEmojiClick={this.handleEmojiClick} pickerOptions={this.configurePicker()}></EmojiMart>
- </div>}
- <div className="table-responsive">
- <table
- id="emojis_table"
- className="table table-sm table-hover"
- >
- <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 className="text-right d-lg-table-cell d-none">{i18n.t("column_imageurl")}</th>
- <th className="text-right">{i18n.t("column_alttext")}</th>
- <th className="text-right d-lg-table-cell">{i18n.t("column_keywords")}</th>
- <th style="width:121px"></th>
- </tr>
- </thead>
- <tbody>
- {this.state.customEmojis.slice((this.state.page - 1) * this.itemsPerPage, ((this.state.page - 1) * this.itemsPerPage) + this.itemsPerPage)
- .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 && <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)}
- />
- </td>
- <td className="text-right">
- <input
- type="text"
- placeholder="ShortCode"
- className="form-control"
- disabled={cv.id > 0}
- value={cv.shortcode}
- onInput={linkEvent({form: this, index: index},this.handleEmojiShortCodeChange)}
- />
- </td>
- <td className="text-right">
- <input
- type="text"
- placeholder="Category"
- className="form-control"
- value={cv.category}
- onInput={linkEvent({form: this, index: index},this.handleEmojiCategoryChange)}
- />
- </td>
- <td className="text-right d-lg-table-cell d-none">
- <input
- type="text"
- placeholder="Url"
- className="form-control"
- value={cv.image_url}
- onInput={linkEvent({form: this, index: index, overrideValue: null},this.handleEmojiImageUrlChange)}
- />
- </td>
- <td className="text-right">
- <input
- type="text"
- placeholder="Alt Text"
- className="form-control"
- value={cv.alt_text}
- onInput={linkEvent({form: this, index: index},this.handleEmojiAltTextChange)}
- />
- </td>
- <td className="text-right d-lg-table-cell">
- <input
- type="text"
- placeholder="Keywords"
- className="form-control"
- value={cv.keywords}
- onInput={linkEvent({form: this, index: index},this.handleEmojiKeywordChange)}
- />
- </td>
- <td>
- <div>
- <span
- title={this.getEditTooltip(cv)}>
- <button
- className={(cv.changed ? "text-success " : "text-muted ") + "btn btn-link btn-animate"}
- onClick={linkEvent({form: 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}
- >
- {/* <Icon
++ componentWillUnmount() {
++ if (isBrowser()) {
++ this.subscription?.unsubscribe();
+ }
++ }
+
- Save
- </button>
- </span>
- <button
- className="btn btn-link btn-animate text-muted"
- onClick={linkEvent({form: 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")}
- >
- <Icon
- icon="trash"
- classes={`icon-inline text-danger`}
- />
- </button>
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- <br />
- <button
- className="btn btn-sm btn-secondary mr-2"
- onClick={linkEvent(this,this.handleAddEmojiClick)}
- >
- {i18n.t("add_custom_emoji")}
- </button>
++ render() {
++ return (
++ <div className="col-12">
++ <HtmlTags
++ title={this.documentTitle}
++ path={this.context.router.route.match.url}
++ />
++ <h5 className="col-12">{i18n.t("custom_emojis")}</h5>
++ {customEmojisLookup.size > 0 && (
++ <div>
++ <EmojiMart
++ onEmojiClick={this.handleEmojiClick}
++ pickerOptions={this.configurePicker()}
++ ></EmojiMart>
++ </div>
++ )}
++ <div className="table-responsive">
++ <table id="emojis_table" className="table table-sm table-hover">
++ <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 className="text-right d-lg-table-cell d-none">
++ {i18n.t("column_imageurl")}
++ </th>
++ <th className="text-right">{i18n.t("column_alttext")}</th>
++ <th className="text-right d-lg-table-cell">
++ {i18n.t("column_keywords")}
++ </th>
++ <th style="width:121px"></th>
++ </tr>
++ </thead>
++ <tbody>
++ {this.state.customEmojis
++ .slice(
++ (this.state.page - 1) * this.itemsPerPage,
++ (this.state.page - 1) * this.itemsPerPage + this.itemsPerPage
++ )
++ .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 && (
++ <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
++ )}
++ />
++ </td>
++ <td className="text-right">
++ <input
++ type="text"
++ placeholder="ShortCode"
++ className="form-control"
++ disabled={cv.id > 0}
++ value={cv.shortcode}
++ onInput={linkEvent(
++ { form: this, index: index },
++ this.handleEmojiShortCodeChange
++ )}
++ />
++ </td>
++ <td className="text-right">
++ <input
++ type="text"
++ placeholder="Category"
++ className="form-control"
++ value={cv.category}
++ onInput={linkEvent(
++ { form: this, index: index },
++ this.handleEmojiCategoryChange
++ )}
++ />
++ </td>
++ <td className="text-right d-lg-table-cell d-none">
++ <input
++ type="text"
++ placeholder="Url"
++ className="form-control"
++ value={cv.image_url}
++ onInput={linkEvent(
++ { form: this, index: index, overrideValue: null },
++ this.handleEmojiImageUrlChange
++ )}
++ />
++ </td>
++ <td className="text-right">
++ <input
++ type="text"
++ placeholder="Alt Text"
++ className="form-control"
++ value={cv.alt_text}
++ onInput={linkEvent(
++ { form: this, index: index },
++ this.handleEmojiAltTextChange
++ )}
++ />
++ </td>
++ <td className="text-right d-lg-table-cell">
++ <input
++ type="text"
++ placeholder="Keywords"
++ className="form-control"
++ value={cv.keywords}
++ onInput={linkEvent(
++ { form: this, index: index },
++ this.handleEmojiKeywordChange
++ )}
++ />
++ </td>
++ <td>
++ <div>
++ <span title={this.getEditTooltip(cv)}>
++ <button
++ className={
++ (cv.changed ? "text-success " : "text-muted ") +
++ "btn btn-link btn-animate"
++ }
++ onClick={linkEvent(
++ { form: 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
++ }
++ >
++ {/* <Icon
+ icon="edit"
+ classes={`icon-inline`}
+ /> */}
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
- </div>
- </div>
- );
- }
++ Save
++ </button>
++ </span>
++ <button
++ className="btn btn-link btn-animate text-muted"
++ onClick={linkEvent(
++ { form: 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")}
++ >
++ <Icon
++ icon="trash"
++ classes={`icon-inline text-danger`}
++ />
++ </button>
++ </div>
++ </td>
++ </tr>
++ ))}
++ </tbody>
++ </table>
++ <br />
++ <button
++ className="btn btn-sm btn-secondary mr-2"
++ onClick={linkEvent(this, this.handleAddEmojiClick)}
++ >
++ {i18n.t("add_custom_emoji")}
++ </button>
+
- canEdit(cv: CustomEmojiViewForm) {
- const noEmptyFields = (cv.alt_text.length > 0 && cv.category.length > 0 && cv.image_url.length > 0 && cv.shortcode.length > 0);
- const noDuplicateShortCodes = this.state.customEmojis.filter(x => x.shortcode == cv.shortcode && x.id != cv.id).length == 0;
- return noEmptyFields && noDuplicateShortCodes;
- }
++ <Paginator page={this.state.page} onChange={this.handlePageChange} />
++ </div>
++ </div>
++ );
++ }
+
- getEditTooltip(cv: CustomEmojiViewForm) {
- if (this.canEdit(cv))
- return i18n.t("save");
- else
- return i18n.t("custom_emoji_save_validation");
- }
++ canEdit(cv: CustomEmojiViewForm) {
++ const noEmptyFields =
++ cv.alt_text.length > 0 &&
++ cv.category.length > 0 &&
++ cv.image_url.length > 0 &&
++ cv.shortcode.length > 0;
++ const noDuplicateShortCodes =
++ this.state.customEmojis.filter(
++ x => x.shortcode == cv.shortcode && x.id != cv.id
++ ).length == 0;
++ return noEmptyFields && noDuplicateShortCodes;
++ }
+
- handlePageChange(page: number) {
++ getEditTooltip(cv: CustomEmojiViewForm) {
++ if (this.canEdit(cv)) return i18n.t("save");
++ else return i18n.t("custom_emoji_save_validation");
++ }
++
++ handlePageChange(page: number) {
++ this.setState({ page: page });
++ }
+
- handleEmojiClick(e: any) {
- const view = customEmojisLookup.get(e.id);
- if (view) {
- const page = this.state.customEmojis.find(x => x.id == view.custom_emoji.id)?.page;
- if (page) {
- this.setState({ page: page });
- this.scrollRef[view.custom_emoji.shortcode].scrollIntoView()
- }
- }
- }
++ handleEmojiClick(e: any) {
++ const view = customEmojisLookup.get(e.id);
++ if (view) {
++ const page = this.state.customEmojis.find(
++ x => x.id == view.custom_emoji.id
++ )?.page;
++ if (page) {
+ this.setState({ page: page });
++ this.scrollRef[view.custom_emoji.shortcode].scrollIntoView();
++ }
+ }
++ }
+
- handleEmojiCategoryChange(props: { form: EmojiForm, index: number }, event: any) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
- let item = {
- ... props.form.state.customEmojis[pagedIndex],
- category: event.target.value,
- changed: true,
- }
- custom_emojis[pagedIndex] = item;
- props.form.setState({ customEmojis: custom_emojis });
- }
++ handleEmojiCategoryChange(
++ props: { form: EmojiForm; index: number },
++ event: any
++ ) {
++ let custom_emojis = [...props.form.state.customEmojis];
++ let pagedIndex =
++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
++ let item = {
++ ...props.form.state.customEmojis[pagedIndex],
++ category: event.target.value,
++ changed: true,
++ };
++ custom_emojis[pagedIndex] = item;
++ props.form.setState({ customEmojis: custom_emojis });
++ }
+
- handleEmojiShortCodeChange(props: { form: EmojiForm, index: number }, event: any) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
- let item = {
- ... props.form.state.customEmojis[pagedIndex],
- shortcode: event.target.value,
- changed: true,
- }
- custom_emojis[pagedIndex] = item;
- props.form.setState({ customEmojis: custom_emojis });
- }
++ handleEmojiShortCodeChange(
++ props: { form: EmojiForm; index: number },
++ event: any
++ ) {
++ let custom_emojis = [...props.form.state.customEmojis];
++ let pagedIndex =
++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
++ let item = {
++ ...props.form.state.customEmojis[pagedIndex],
++ shortcode: event.target.value,
++ changed: true,
++ };
++ custom_emojis[pagedIndex] = item;
++ props.form.setState({ customEmojis: custom_emojis });
++ }
+
- handleEmojiImageUrlChange(props: { form: EmojiForm, index: number, overrideValue: string | null }, event: any) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
- let item = {
- ... props.form.state.customEmojis[pagedIndex],
- image_url: props.overrideValue ?? event.target.value,
- changed: true,
- }
- custom_emojis[pagedIndex] = item;
- props.form.setState({ customEmojis: custom_emojis });
- }
++ handleEmojiImageUrlChange(
++ props: { form: EmojiForm; index: number; overrideValue: string | null },
++ event: any
++ ) {
++ let custom_emojis = [...props.form.state.customEmojis];
++ let pagedIndex =
++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
++ let item = {
++ ...props.form.state.customEmojis[pagedIndex],
++ image_url: props.overrideValue ?? event.target.value,
++ changed: true,
++ };
++ custom_emojis[pagedIndex] = item;
++ props.form.setState({ customEmojis: custom_emojis });
++ }
+
- handleEmojiAltTextChange(props: { form: EmojiForm, index: number }, event: any) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
- let item = {
- ... props.form.state.customEmojis[pagedIndex],
- alt_text: event.target.value,
- changed: true,
- }
- custom_emojis[pagedIndex] = item;
- props.form.setState({ customEmojis: custom_emojis });
- }
++ handleEmojiAltTextChange(
++ props: { form: EmojiForm; index: number },
++ event: any
++ ) {
++ let custom_emojis = [...props.form.state.customEmojis];
++ let pagedIndex =
++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
++ let item = {
++ ...props.form.state.customEmojis[pagedIndex],
++ alt_text: event.target.value,
++ changed: true,
++ };
++ custom_emojis[pagedIndex] = item;
++ props.form.setState({ customEmojis: custom_emojis });
++ }
+
- handleEmojiKeywordChange(props: { form: EmojiForm, index: number }, event: any) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index;
- let item = {
- ... props.form.state.customEmojis[pagedIndex],
- keywords: event.target.value,
- changed: true,
- }
- custom_emojis[pagedIndex] = item;
- props.form.setState({ customEmojis: custom_emojis });
++ handleEmojiKeywordChange(
++ props: { form: EmojiForm; index: number },
++ event: any
++ ) {
++ let custom_emojis = [...props.form.state.customEmojis];
++ let pagedIndex =
++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
++ let item = {
++ ...props.form.state.customEmojis[pagedIndex],
++ keywords: event.target.value,
++ changed: true,
++ };
++ custom_emojis[pagedIndex] = item;
++ props.form.setState({ customEmojis: custom_emojis });
++ }
+
- handleDeleteEmojiClick(props: { form: EmojiForm, index: number, cv: CustomEmojiViewForm }) {
- let 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));
- }
- else {
- let custom_emojis = [...props.form.state.customEmojis];
- custom_emojis.splice(pagedIndex, 1);
- props.form.setState({ customEmojis: custom_emojis });
- }
++ handleDeleteEmojiClick(props: {
++ form: EmojiForm;
++ index: number;
++ cv: CustomEmojiViewForm;
++ }) {
++ let 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));
++ } else {
++ let custom_emojis = [...props.form.state.customEmojis];
++ custom_emojis.splice(pagedIndex, 1);
++ props.form.setState({ customEmojis: custom_emojis });
+ }
++ }
+
- handleEditEmojiClick(props: { form: EmojiForm, cv: CustomEmojiViewForm }) {
- const keywords = props.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,
- keywords: uniqueKeywords,
- auth: myAuth() ?? ""
- };
- WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
- }
- else {
- const createForm: CreateCustomEmoji = {
- category: props.cv.category,
- shortcode: props.cv.shortcode,
- image_url: props.cv.image_url,
- alt_text: props.cv.alt_text,
- keywords: uniqueKeywords,
- auth: myAuth() ?? ""
- };
- WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
- }
- }
++ handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
++ const keywords = props.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,
++ keywords: uniqueKeywords,
++ auth: myAuth() ?? "",
++ };
++ WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
++ } else {
++ const createForm: CreateCustomEmoji = {
++ category: props.cv.category,
++ shortcode: props.cv.shortcode,
++ image_url: props.cv.image_url,
++ alt_text: props.cv.alt_text,
++ keywords: uniqueKeywords,
++ auth: myAuth() ?? "",
++ };
++ WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
+ }
++ }
+
- handleAddEmojiClick(form: EmojiForm, event: any) {
- event.preventDefault();
- let custom_emojis = [...form.state.customEmojis];
- const page = 1 + (Math.floor((form.state.customEmojis.length) / form.itemsPerPage))
- let 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 });
++ handleAddEmojiClick(form: EmojiForm, event: any) {
++ event.preventDefault();
++ let custom_emojis = [...form.state.customEmojis];
++ const page =
++ 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
++ let 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 });
++ }
+
- async handleImageUpload(props: { form: EmojiForm, index: number }, event: any) {
- event.preventDefault();
- let file = event.target.files[0];
- const formData = new FormData();
- formData.append("images[]", file);
-
- props.form.setState({ loading: true });
- let res: any = await fetch(pictrsUri, {
- method: "POST",
- body: formData,
- });
- let data = await res.json();
- props.form.setState({ loading: false });
- if (data.msg != "ok") {
- toast(JSON.stringify(data), "danger");
- }
- else {
- let hash = data.files[0].file;
- let url = `${pictrsUri}/${hash}`;
- props.form.handleEmojiImageUrlChange({form: props.form, index: props.index, overrideValue: url}, event)
++ handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
++ let file: any;
++ if (event.target) {
++ event.preventDefault();
++ file = event.target.files[0];
++ } else {
++ file = event;
+ }
+
- }
++ uploadImage(file)
++ .then(res => {
++ console.log("pictrs upload:");
++ console.log(res);
++ if (res.msg === "ok") {
++ pictrsDeleteToast(
++ `${i18n.t("click_to_delete_picture")}: ${file.name}`,
++ `${i18n.t("picture_deleted")}: ${file.name}`,
++ `${i18n.t("failed_to_delete_picture")}: ${file.name}`,
++ res.delete_url as string
++ );
++ } else {
++ toast(JSON.stringify(res), "danger");
++ let hash = res.files?.at(0)?.file;
++ let url = `${res.url}/${hash}`;
++ props.form.handleEmojiImageUrlChange(
++ { form: props.form, index: props.index, overrideValue: url },
++ event
++ );
+ }
- configurePicker(): any {
- return {
- data: { categories: [], emojis: [], aliases: [] },
- maxFrequentRows: 0,
- dynamicWidth: true,
- };
- }
++ })
++ .catch(error => {
++ console.error(error);
++ toast(error, "danger");
++ });
++ }
+
- parseMessage(msg: any) {
- let 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) {
- let data = wsJsonToRes<CustomEmojiResponse>(msg);
- const custom_emoji_view = data.custom_emoji;
- updateEmojiDataModel(custom_emoji_view)
- let currentEmojis = this.state.customEmojis;
- let 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) {
- let data = wsJsonToRes<CustomEmojiResponse>(msg);
- const custom_emoji_view = data.custom_emoji;
- updateEmojiDataModel(data.custom_emoji)
- let currentEmojis = this.state.customEmojis;
- let 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) {
- let data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
- if (data.success) {
- removeFromEmojiDataModel(data.id);
- let 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 });
- }
++ configurePicker(): any {
++ return {
++ data: { categories: [], emojis: [], aliases: [] },
++ maxFrequentRows: 0,
++ dynamicWidth: true,
++ };
++ }
+
-
++ parseMessage(msg: any) {
++ let 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) {
++ let data = wsJsonToRes<CustomEmojiResponse>(msg);
++ const custom_emoji_view = data.custom_emoji;
++ updateEmojiDataModel(custom_emoji_view);
++ let currentEmojis = this.state.customEmojis;
++ let 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) {
++ let data = wsJsonToRes<CustomEmojiResponse>(msg);
++ const custom_emoji_view = data.custom_emoji;
++ updateEmojiDataModel(data.custom_emoji);
++ let currentEmojis = this.state.customEmojis;
++ let 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) {
++ let data = wsJsonToRes<DeleteCustomEmojiResponse>(msg);
++ if (data.success) {
++ removeFromEmojiDataModel(data.id);
++ let 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 });
+ }
++ }
+ }
++import { Picker } from "emoji-mart";
import emojiShortName from "emoji-short-name";
-import { Picker } from 'emoji-mart'
import {
BlockCommunityResponse,
BlockPersonResponse,
} from "lemmy-js-client";
import { default as MarkdownIt } from "markdown-it";
import markdown_it_container from "markdown-it-container";
++import markdown_it_emoji from "markdown-it-emoji/bare";
import markdown_it_footnote from "markdown-it-footnote";
import markdown_it_html5_embed from "markdown-it-html5-embed";
import markdown_it_sub from "markdown-it-sub";
import markdown_it_sup from "markdown-it-sup";
-import markdown_it_emoji from 'markdown-it-emoji/bare';
++import Renderer from "markdown-it/lib/renderer";
++import Token from "markdown-it/lib/token";
import moment from "moment";
import { Subscription } from "rxjs";
import { delay, retryWhen, take } from "rxjs/operators";
export const relTags = "noopener nofollow";
-export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<string, CustomEmojiView>();
+ let customEmojis: EmojiMartCategory[] = [];
++export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
++ string,
++ CustomEmojiView
++>();
+
const DEFAULT_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return `${item.original.val} ${shortName}`;
},
selectTemplate: (item: any) => {
- return `${item.original.val}`;
- let customEmoji = customEmojisLookup.get(item.original.key)?.custom_emoji;
- if (customEmoji == undefined)
- return `${item.original.val}`;
++ let customEmoji = customEmojisLookup.get(
++ item.original.key
++ )?.custom_emoji;
++ if (customEmoji == undefined) return `${item.original.val}`;
+ else
+ return `![${customEmoji.alt_text}](${customEmoji.image_url} "${customEmoji.shortcode}")`;
},
-- values: Object.entries(emojiShortName).map(e => {
-- return { key: e[1], val: e[0] };
- }),
- }).concat(
- Array.from(customEmojisLookup.entries()).map((k) => ({
- key: k[0],
- val: `<img class="icon icon-emoji" src="${k[1].custom_emoji.image_url}" title="${k[1].custom_emoji.shortcode}" alt="${k[1].custom_emoji.alt_text}" />`
- }))
- ),
++ values: Object.entries(emojiShortName)
++ .map(e => {
++ return { key: e[1], val: e[0] };
++ })
++ .concat(
++ Array.from(customEmojisLookup.entries()).map(k => ({
++ key: k[0],
++ val: `<img class="icon icon-emoji" src="${k[1].custom_emoji.image_url}" title="${k[1].custom_emoji.shortcode}" alt="${k[1].custom_emoji.alt_text}" />`,
++ }))
++ ),
allowSpaces: false,
autocompleteMode: true,
// TODO
});
}
-
-
+ function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) {
+ let groupedEmojis = groupBy(custom_emoji_views, x => x.custom_emoji.category);
+ for (const [category, emojis] of Object.entries(groupedEmojis)) {
+ customEmojis.push({
+ id: category,
+ name: category,
- emojis: emojis.map(emoji =>
- ({
++ emojis: emojis.map(emoji => ({
+ id: emoji.custom_emoji.shortcode,
+ name: emoji.custom_emoji.shortcode,
+ keywords: emoji.keywords.map(x => x.keyword),
- skins: [{ src: emoji.custom_emoji.image_url }]
- }))
- })
++ skins: [{ src: emoji.custom_emoji.image_url }],
++ })),
++ });
+ }
- customEmojisLookup = new Map(custom_emoji_views.map(view => [view.custom_emoji.shortcode, view]));
++ customEmojisLookup = new Map(
++ custom_emoji_views.map(view => [view.custom_emoji.shortcode, view])
++ );
+ }
+
+ export function updateEmojiDataModel(custom_emoji_view: CustomEmojiView) {
+ const emoji: EmojiMartCustomEmoji = {
+ id: custom_emoji_view.custom_emoji.shortcode,
+ name: custom_emoji_view.custom_emoji.shortcode,
+ keywords: custom_emoji_view.keywords.map(x => x.keyword),
- skins: [{ src: custom_emoji_view.custom_emoji.image_url }]
++ skins: [{ src: custom_emoji_view.custom_emoji.image_url }],
+ };
- let categoryIndex = customEmojis.findIndex(x => x.id == custom_emoji_view.custom_emoji.category);
++ let categoryIndex = customEmojis.findIndex(
++ x => x.id == custom_emoji_view.custom_emoji.category
++ );
+ if (categoryIndex == -1) {
+ customEmojis.push({
+ id: custom_emoji_view.custom_emoji.category,
+ name: custom_emoji_view.custom_emoji.category,
- emojis: [emoji]
- })
- }
- else {
- let emojiIndex = customEmojis[categoryIndex].emojis.findIndex(x => x.id == custom_emoji_view.custom_emoji.shortcode);
++ emojis: [emoji],
++ });
++ } else {
++ let emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
++ x => x.id == custom_emoji_view.custom_emoji.shortcode
++ );
+ if (emojiIndex == -1) {
- customEmojis[categoryIndex].emojis.push(emoji)
- }
- else {
++ customEmojis[categoryIndex].emojis.push(emoji);
++ } else {
+ customEmojis[categoryIndex].emojis[emojiIndex] = emoji;
+ }
+ }
- customEmojisLookup.set(custom_emoji_view.custom_emoji.shortcode,custom_emoji_view);
++ customEmojisLookup.set(
++ custom_emoji_view.custom_emoji.shortcode,
++ custom_emoji_view
++ );
+ }
+
+ export function removeFromEmojiDataModel(id: number) {
+ let view: CustomEmojiView | undefined;
+ for (let item of customEmojisLookup.values()) {
+ if (item.custom_emoji.id === id) {
+ view = item;
+ break;
+ }
+ }
- if (!view)
- return;
- const categoryIndex = customEmojis.findIndex(x => x.id == view?.custom_emoji.category);
- const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(x => x.id == view?.custom_emoji.shortcode)
- customEmojis[categoryIndex].emojis = customEmojis[categoryIndex].emojis.splice(emojiIndex, 1);
++ if (!view) return;
++ const categoryIndex = customEmojis.findIndex(
++ x => x.id == view?.custom_emoji.category
++ );
++ const emojiIndex = customEmojis[categoryIndex].emojis.findIndex(
++ x => x.id == view?.custom_emoji.shortcode
++ );
++ customEmojis[categoryIndex].emojis = customEmojis[
++ categoryIndex
++ ].emojis.splice(emojiIndex, 1);
+
+ customEmojisLookup.delete(view?.custom_emoji.shortcode);
+ }
+
+ function setupMarkdown() {
+ const markdownItConfig: MarkdownIt.Options = {
+ html: false,
+ linkify: true,
+ typographer: true,
+ };
+
- const emojiDefs = Array.from(customEmojisLookup.entries()).reduce((main, [key, value]) => ({...main, [key]: value}), {})
++ const emojiDefs = Array.from(customEmojisLookup.entries()).reduce(
++ (main, [key, value]) => ({ ...main, [key]: value }),
++ {}
++ );
+ md = new MarkdownIt(markdownItConfig)
+ .use(markdown_it_sub)
+ .use(markdown_it_sup)
+ .use(markdown_it_footnote)
+ .use(markdown_it_html5_embed, html5EmbedConfig)
+ .use(markdown_it_container, "spoiler", spoilerConfig)
+ .use(markdown_it_emoji, {
- defs: emojiDefs
++ defs: emojiDefs,
+ });
+
+ mdNoImages = new MarkdownIt(markdownItConfig)
+ .use(markdown_it_sub)
+ .use(markdown_it_sup)
+ .use(markdown_it_footnote)
+ .use(markdown_it_html5_embed, html5EmbedConfig)
+ .use(markdown_it_container, "spoiler", spoilerConfig)
+ .use(markdown_it_emoji, {
- defs: emojiDefs
++ defs: emojiDefs,
+ })
+ .disable("image");
+ var defaultRenderer = md.renderer.rules.image;
- md.renderer.rules.image = function (tokens: Token[], idx: number, options: MarkdownIt.Options, env: any, self: Renderer) {
++ md.renderer.rules.image = function (
++ tokens: Token[],
++ idx: number,
++ options: MarkdownIt.Options,
++ env: any,
++ self: Renderer
++ ) {
+ //Provide custom renderer for our emojis to allow us to add a css class and force size dimensions on them.
+ const item = tokens[idx] as any;
+ const title = item.attrs.length >= 3 ? item.attrs[2][1] : "";
+ const src: string = item.attrs[0][1];
+ const isCustomEmoji = customEmojisLookup.get(title) != undefined;
+ if (!isCustomEmoji) {
+ return defaultRenderer?.(tokens, idx, options, env, self) ?? "";
+ }
+ const alt_text = item.content;
+ return `<img class="icon icon-emoji" src="${src}" title="${title}" alt="${alt_text}"/>`;
- }
++ };
+ }
+
-export function getEmojiMart(onEmojiSelect: (e: any) => void, customPickerOptions: any = {}) {
- const pickerOptions = { ...customPickerOptions, onEmojiSelect: onEmojiSelect, custom: customEmojis }
++export function getEmojiMart(
++ onEmojiSelect: (e: any) => void,
++ customPickerOptions: any = {}
++) {
++ const pickerOptions = {
++ ...customPickerOptions,
++ onEmojiSelect: onEmojiSelect,
++ custom: customEmojis,
++ };
+ return new Picker(pickerOptions);
+ }
+
var tippyInstance: any;
if (isBrowser()) {
tippyInstance = tippy("[data-tippy-content]");
return !nsfw || (nsfw && myShowNsfw);
}
- export function getRandomFromList<T>(list?: T[]): T | undefined {
- return list?.at(Math.floor(Math.random() * list.length));
+ export function getRandomFromList<T>(list: T[]): T | undefined {
- return list.length == 0 ? undefined : list.at(Math.floor(Math.random() * list.length));
++ return list.length == 0
++ ? undefined
++ : list.at(Math.floor(Math.random() * list.length));
}
/**
}
}
}
- id: string,
- name: string,
- emojis: EmojiMartCustomEmoji[]
+
+export function uploadImage(image: File): Promise<UploadImageResponse> {
+ const client = new LemmyHttp(httpBase);
+
+ return client.uploadImage({ image });
+}
++
+ interface EmojiMartCategory {
- id: string,
- name: string,
- keywords: string[],
- skins: EmojiMartSkin[]
++ id: string;
++ name: string;
++ emojis: EmojiMartCustomEmoji[];
+ }
+
+ interface EmojiMartCustomEmoji {
- src: string
++ id: string;
++ name: string;
++ keywords: string[];
++ skins: EmojiMartSkin[];
+ }
+
+ interface EmojiMartSkin {
-const groupBy = <T>(array: T[], predicate: (value: T, index: number, array: T[]) => string) =>
++ src: string;
+ }
+
- }, {} as { [key: string]: T[] });
++const groupBy = <T>(
++ array: T[],
++ predicate: (value: T, index: number, array: T[]) => string
++) =>
+ array.reduce((acc, value, index, array) => {
+ (acc[predicate(value, index, array)] ||= []).push(value);
+ return acc;
++ }, {} as { [key: string]: T[] });