mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Add Custom Emoji Support
This commit is contained in:
parent
578709b986
commit
1f15ea5aa8
11 changed files with 877 additions and 52 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit 7379716231b9f7e67f710751c839398b7ab5d65e
|
Subproject commit 819531ae64c6cba12cb406eb98333fd52988bf3e
|
|
@ -23,6 +23,7 @@
|
||||||
"@babel/preset-env": "7.20.2",
|
"@babel/preset-env": "7.20.2",
|
||||||
"@babel/preset-typescript": "^7.21.0",
|
"@babel/preset-typescript": "^7.21.0",
|
||||||
"@babel/runtime": "^7.21.0",
|
"@babel/runtime": "^7.21.0",
|
||||||
|
"@emoji-mart/data": "^1.1.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
"babel-plugin-inferno": "^6.6.0",
|
"babel-plugin-inferno": "^6.6.0",
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
|
"emoji-mart": "^5.4.0",
|
||||||
"emoji-short-name": "^2.0.0",
|
"emoji-short-name": "^2.0.0",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
"html-to-text": "^9.0.4",
|
"html-to-text": "^9.0.4",
|
||||||
|
@ -48,6 +50,7 @@
|
||||||
"lemmy-js-client": "0.17.2-rc.1",
|
"lemmy-js-client": "0.17.2-rc.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-container": "^3.0.0",
|
"markdown-it-container": "^3.0.0",
|
||||||
|
"markdown-it-emoji": "^2.0.2",
|
||||||
"markdown-it-footnote": "^3.0.3",
|
"markdown-it-footnote": "^3.0.3",
|
||||||
"markdown-it-html5-embed": "^1.0.0",
|
"markdown-it-html5-embed": "^1.0.0",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
|
|
|
@ -62,15 +62,19 @@
|
||||||
.md-div h1 {
|
.md-div h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-div h2 {
|
.md-div h2 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-div h3 {
|
.md-div h3 {
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-div h4 {
|
.md-div h4 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-div h5 {
|
.md-div h5 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
@ -129,10 +133,52 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-emoji {
|
||||||
|
width: 4em;
|
||||||
|
height: auto;
|
||||||
|
max-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-emoji-admin {
|
||||||
|
max-width: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-inline {
|
.icon-inline {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-picker-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
z-index: 1000;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 992px) {
|
||||||
|
.emoji-picker-container {
|
||||||
|
width: 100vw;
|
||||||
|
transform: translateX(0%);
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-picker-container>section {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-away-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, .3);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
.spinner-large {
|
.spinner-large {
|
||||||
display: grid;
|
display: grid;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -149,6 +195,7 @@
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(359deg);
|
transform: rotate(359deg);
|
||||||
}
|
}
|
||||||
|
@ -340,19 +387,24 @@ br.big {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: var(--light);
|
background: var(--light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tribute-container li {
|
.tribute-container li {
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tribute-container li.highlight {
|
.tribute-container li.highlight {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tribute-container li span {
|
.tribute-container li span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tribute-container li.no-match {
|
.tribute-container li.no-match {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tribute-container .menu-highlighted {
|
.tribute-container .menu-highlighted {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
@ -376,7 +428,6 @@ br.big {
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-select-action {
|
.lang-select-action {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
@ -384,3 +435,6 @@ br.big {
|
||||||
.lang-select-action:focus {
|
.lang-select-action:focus {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
em-emoji-picker{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
|
33
src/shared/components/common/emoji-mart.tsx
Normal file
33
src/shared/components/common/emoji-mart.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Component } from "inferno";
|
||||||
|
import { getEmojiMart } from "../../utils";
|
||||||
|
|
||||||
|
|
||||||
|
interface EmojiMartProps {
|
||||||
|
onEmojiClick?(val: any): any;
|
||||||
|
pickerOptions: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmojiMart extends Component<
|
||||||
|
EmojiMartProps
|
||||||
|
> {
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
let div: any = document.getElementById("emoji-picker");
|
||||||
|
if (div) {
|
||||||
|
div.appendChild(getEmojiMart(this.handleEmojiClick, this.props.pickerOptions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="emoji-picker"></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmojiClick(e: any) {
|
||||||
|
this.props.onEmojiClick?.(e);
|
||||||
|
}
|
||||||
|
}
|
62
src/shared/components/common/emoji-picker.tsx
Normal file
62
src/shared/components/common/emoji-picker.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { Component, linkEvent } from "inferno";
|
||||||
|
import { i18n } from "../../i18next";
|
||||||
|
import { EmojiMart } from "./emoji-mart";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
|
interface EmojiPickerProps {
|
||||||
|
onEmojiClick?(val: any): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmojiPickerState {
|
||||||
|
showPicker: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmojiPicker extends Component<
|
||||||
|
EmojiPickerProps,
|
||||||
|
EmojiPickerState
|
||||||
|
> {
|
||||||
|
private emptyState: EmojiPickerState = {
|
||||||
|
showPicker: false,
|
||||||
|
};
|
||||||
|
state: EmojiPickerState;
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
|
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm text-muted"
|
||||||
|
data-tippy-content={i18n.t("emoji")}
|
||||||
|
aria-label={i18n.t("emoji")}
|
||||||
|
onClick={linkEvent(this,this.togglePicker)}
|
||||||
|
>
|
||||||
|
<Icon icon="smile" classes="icon-inline" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{this.state.showPicker && (
|
||||||
|
<>
|
||||||
|
<div className="emoji-picker-container">
|
||||||
|
<EmojiMart onEmojiClick={this.handleEmojiClick} pickerOptions={({})}></EmojiMart>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={linkEvent(this,this.togglePicker)}
|
||||||
|
className="click-away-container"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePicker(i: EmojiPicker, e: any) {
|
||||||
|
e.preventDefault();
|
||||||
|
i.setState({ showPicker: !i.state.showPicker });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmojiClick(e: any) {
|
||||||
|
this.props.onEmojiClick?.(e);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { pictrsUri } from "../../env";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { UserService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
import {
|
import {
|
||||||
|
customEmojisLookup,
|
||||||
isBrowser,
|
isBrowser,
|
||||||
markdownFieldCharacterLimit,
|
markdownFieldCharacterLimit,
|
||||||
markdownHelpUrl,
|
markdownHelpUrl,
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
setupTribute,
|
setupTribute,
|
||||||
toast,
|
toast,
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
|
import { EmojiPicker } from "./emoji-picker";
|
||||||
import { Icon, Spinner } from "./icon";
|
import { Icon, Spinner } from "./icon";
|
||||||
import { LanguageSelect } from "./language-select";
|
import { LanguageSelect } from "./language-select";
|
||||||
|
|
||||||
|
@ -226,6 +228,7 @@ export class MarkdownTextArea extends Component<
|
||||||
>
|
>
|
||||||
<Icon icon="link" classes="icon-inline" />
|
<Icon icon="link" classes="icon-inline" />
|
||||||
</button>
|
</button>
|
||||||
|
<EmojiPicker onEmojiClick={(e) => this.handleEmoji(this,e)}></EmojiPicker>
|
||||||
<form className="btn btn-sm text-muted font-weight-bold">
|
<form className="btn btn-sm text-muted font-weight-bold">
|
||||||
<label
|
<label
|
||||||
htmlFor={`file-upload-${this.id}`}
|
htmlFor={`file-upload-${this.id}`}
|
||||||
|
@ -328,6 +331,22 @@ export class MarkdownTextArea extends Component<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEmoji(i: MarkdownTextArea, e: any) {
|
||||||
|
let value = e.native;
|
||||||
|
if (value == null){
|
||||||
|
let emoji = customEmojisLookup.get(e.id)?.custom_emoji;
|
||||||
|
if (emoji){
|
||||||
|
value = `![${emoji.alt_text}](${emoji.image_url} "${emoji.shortcode}")`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i.setState({
|
||||||
|
content: `${i.state.content ?? ""} ${value} `,
|
||||||
|
});
|
||||||
|
i.contentChange();
|
||||||
|
let textarea: any = document.getElementById(i.id);
|
||||||
|
autosize.update(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
handleImageUploadPaste(i: MarkdownTextArea, event: any) {
|
handleImageUploadPaste(i: MarkdownTextArea, event: any) {
|
||||||
let image = event.clipboardData.files[0];
|
let image = event.clipboardData.files[0];
|
||||||
if (image) {
|
if (image) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
import { EmojiForm } from "./emojis-form";
|
||||||
import { SiteForm } from "./site-form";
|
import { SiteForm } from "./site-form";
|
||||||
|
|
||||||
interface AdminSettingsState {
|
interface AdminSettingsState {
|
||||||
|
@ -35,6 +36,7 @@ interface AdminSettingsState {
|
||||||
banned: PersonViewSafe[];
|
banned: PersonViewSafe[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
leaveAdminTeamLoading: boolean;
|
leaveAdminTeamLoading: boolean;
|
||||||
|
currentTab: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminSettings extends Component<any, AdminSettingsState> {
|
export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
|
@ -46,6 +48,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
banned: [],
|
banned: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
leaveAdminTeamLoading: false,
|
leaveAdminTeamLoading: false,
|
||||||
|
currentTab: "site",
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -99,8 +102,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get documentTitle(): string {
|
get documentTitle(): string {
|
||||||
return `${i18n.t("admin_settings")} - ${
|
return `${i18n.t("admin_settings")} - ${this.state.siteRes.site_view.site.name
|
||||||
this.state.siteRes.site_view.site.name
|
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,12 +114,31 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
<Spinner large />
|
<Spinner large />
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
) : (
|
||||||
<div className="row">
|
<div>
|
||||||
<div className="col-12 col-md-6">
|
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
/>
|
/>
|
||||||
|
<ul className="nav nav-tabs mb-2">
|
||||||
|
<li className="nav-item">
|
||||||
|
<button
|
||||||
|
className={`nav-link btn ${this.state.currentTab == "site" && "active"}`}
|
||||||
|
onClick={linkEvent({ ctx: this, tab: "site" }, this.handleSwitchTab) }>
|
||||||
|
{i18n.t("site")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<button
|
||||||
|
className={`nav-link btn ${this.state.currentTab == "emojis" && "active"
|
||||||
|
}`}
|
||||||
|
onClick={linkEvent({ ctx: this, tab: "emojis" }, this.handleSwitchTab) }>
|
||||||
|
{i18n.t("emojis")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{this.state.currentTab == "site" &&
|
||||||
|
<div className="row"><div className="col-12 col-md-6">
|
||||||
|
|
||||||
<SiteForm
|
<SiteForm
|
||||||
siteRes={this.state.siteRes}
|
siteRes={this.state.siteRes}
|
||||||
showLocal={showLocal(this.isoData)}
|
showLocal={showLocal(this.isoData)}
|
||||||
|
@ -128,6 +149,13 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
{this.bannedUsers()}
|
{this.bannedUsers()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
{this.state.currentTab == "emojis" &&
|
||||||
|
<div className="row">
|
||||||
|
<EmojiForm></EmojiForm>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -179,6 +207,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSwitchTab(i: { ctx: AdminSettings; tab: string }, event: any) {
|
||||||
|
i.ctx.setState({ currentTab: i.tab });
|
||||||
|
}
|
||||||
|
|
||||||
handleLeaveAdminTeam(i: AdminSettings) {
|
handleLeaveAdminTeam(i: AdminSettings) {
|
||||||
let auth = myAuth();
|
let auth = myAuth();
|
||||||
if (auth) {
|
if (auth) {
|
||||||
|
|
445
src/shared/components/home/emojis-form.tsx
Normal file
445
src/shared/components/home/emojis-form.tsx
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
import { Component, linkEvent } from "inferno";
|
||||||
|
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 { i18n } from "../../i18next";
|
||||||
|
import { customEmojisLookup, isBrowser, myAuth, removeFromEmojiDataModel, setIsoData, toast, updateEmojiDataModel, wsClient, wsSubscribe } from "../../utils";
|
||||||
|
import { EmojiMart } from "../common/emoji-mart";
|
||||||
|
import { HtmlTags } from "../common/html-tags";
|
||||||
|
import { Icon } from "../common/icon";
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
import { pictrsUri } from "../../env";
|
||||||
|
import { Paginator } from "../common/paginator";
|
||||||
|
|
||||||
|
interface EmojiFormState {
|
||||||
|
siteRes: GetSiteResponse;
|
||||||
|
customEmojis: CustomEmojiViewForm[];
|
||||||
|
loading: boolean;
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomEmojiViewForm {
|
||||||
|
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> {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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">{i18n.t("column_imageurl")}</th>
|
||||||
|
<th className="text-right">{i18n.t("column_alttext")}</th>
|
||||||
|
<th className="text-right d-none 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">
|
||||||
|
<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-none 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`}
|
||||||
|
/> */}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Paginator
|
||||||
|
page={this.state.page}
|
||||||
|
onChange={this.handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@ export class Home extends Component<any, HomeState> {
|
||||||
wsClient.communityJoin({ community_id: 0 })
|
wsClient.communityJoin({ community_id: 0 })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const taglines = this.state.siteRes.taglines;
|
const taglines = this.state?.siteRes?.taglines ?? [];
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
trendingCommunities: trendingRes?.communities ?? [],
|
trendingCommunities: trendingRes?.communities ?? [],
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import emojiShortName from "emoji-short-name";
|
import emojiShortName from "emoji-short-name";
|
||||||
|
import { Picker } from 'emoji-mart'
|
||||||
import {
|
import {
|
||||||
BlockCommunityResponse,
|
BlockCommunityResponse,
|
||||||
BlockPersonResponse,
|
BlockPersonResponse,
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
CommentView,
|
CommentView,
|
||||||
CommunityModeratorView,
|
CommunityModeratorView,
|
||||||
CommunityView,
|
CommunityView,
|
||||||
|
CustomEmojiView,
|
||||||
GetSiteMetadata,
|
GetSiteMetadata,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
Language,
|
Language,
|
||||||
|
@ -32,6 +34,7 @@ import markdown_it_footnote from "markdown-it-footnote";
|
||||||
import markdown_it_html5_embed from "markdown-it-html5-embed";
|
import markdown_it_html5_embed from "markdown-it-html5-embed";
|
||||||
import markdown_it_sub from "markdown-it-sub";
|
import markdown_it_sub from "markdown-it-sub";
|
||||||
import markdown_it_sup from "markdown-it-sup";
|
import markdown_it_sup from "markdown-it-sup";
|
||||||
|
import markdownitEmoji from 'markdown-it-emoji/bare';
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
import { delay, retryWhen, take } from "rxjs/operators";
|
import { delay, retryWhen, take } from "rxjs/operators";
|
||||||
|
@ -41,6 +44,8 @@ import { httpBase } from "./env";
|
||||||
import { i18n, languages } from "./i18next";
|
import { i18n, languages } from "./i18next";
|
||||||
import { DataType, IsoData } from "./interfaces";
|
import { DataType, IsoData } from "./interfaces";
|
||||||
import { UserService, WebSocketService } from "./services";
|
import { UserService, WebSocketService } from "./services";
|
||||||
|
import Renderer from "markdown-it/lib/renderer";
|
||||||
|
import Token from "markdown-it/lib/token";
|
||||||
|
|
||||||
var Tribute: any;
|
var Tribute: any;
|
||||||
if (isBrowser()) {
|
if (isBrowser()) {
|
||||||
|
@ -74,6 +79,9 @@ export const markdownFieldCharacterLimit = 50000;
|
||||||
|
|
||||||
export const relTags = "noopener nofollow";
|
export const relTags = "noopener nofollow";
|
||||||
|
|
||||||
|
let customEmojis: EmojiMartCategory[] = [];
|
||||||
|
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<string, CustomEmojiView>();
|
||||||
|
|
||||||
const DEFAULT_ALPHABET =
|
const DEFAULT_ALPHABET =
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
|
@ -124,26 +132,9 @@ const spoilerConfig = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const markdownItConfig: MarkdownIt.Options = {
|
export let md: MarkdownIt = new MarkdownIt();
|
||||||
html: false,
|
|
||||||
linkify: true,
|
|
||||||
typographer: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const md = new MarkdownIt(markdownItConfig)
|
export let mdNoImages: MarkdownIt = new MarkdownIt();
|
||||||
.use(markdown_it_sub)
|
|
||||||
.use(markdown_it_sup)
|
|
||||||
.use(markdown_it_footnote)
|
|
||||||
.use(markdown_it_html5_embed, html5EmbedConfig)
|
|
||||||
.use(markdown_it_container, "spoiler", spoilerConfig);
|
|
||||||
|
|
||||||
export const 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)
|
|
||||||
.disable("image");
|
|
||||||
|
|
||||||
export function hotRankComment(comment_view: CommentView): number {
|
export function hotRankComment(comment_view: CommentView): number {
|
||||||
return hotRank(comment_view.counts.score, comment_view.comment.published);
|
return hotRank(comment_view.counts.score, comment_view.comment.published);
|
||||||
|
@ -630,11 +621,20 @@ export function setupTribute() {
|
||||||
return `${item.original.val} ${shortName}`;
|
return `${item.original.val} ${shortName}`;
|
||||||
},
|
},
|
||||||
selectTemplate: (item: any) => {
|
selectTemplate: (item: any) => {
|
||||||
|
let customEmoji = customEmojisLookup[item.original.key]?.custom_emoji;
|
||||||
|
if (customEmoji == undefined)
|
||||||
return `${item.original.val}`;
|
return `${item.original.val}`;
|
||||||
|
else
|
||||||
|
return `![${customEmoji.alt_text}](${customEmoji.image_url} "${customEmoji.shortcode}")`;
|
||||||
},
|
},
|
||||||
values: Object.entries(emojiShortName).map(e => {
|
values: Object.entries(emojiShortName).map(e => {
|
||||||
return { key: e[1], val: e[0] };
|
return { key: e[1], val: e[0] };
|
||||||
}),
|
}).concat(
|
||||||
|
Object.entries(customEmojisLookup).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,
|
allowSpaces: false,
|
||||||
autocompleteMode: true,
|
autocompleteMode: true,
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -678,6 +678,118 @@ export function setupTribute() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
({
|
||||||
|
id: emoji.custom_emoji.shortcode,
|
||||||
|
name: emoji.custom_emoji.shortcode,
|
||||||
|
keywords: emoji.keywords.map(x => x.keyword),
|
||||||
|
skins: [{ src: emoji.custom_emoji.image_url }]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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 }]
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
if (emojiIndex == -1) {
|
||||||
|
customEmojis[categoryIndex].emojis.push(emoji)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
customEmojis[categoryIndex].emojis[emojiIndex] = emoji;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customEmojisLookup[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);
|
||||||
|
|
||||||
|
customEmojisLookup.delete(view?.custom_emoji.shortcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMarkdown() {
|
||||||
|
const markdownItConfig: MarkdownIt.Options = {
|
||||||
|
html: false,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const emojiDefs = Object.fromEntries(Object.entries(customEmojisLookup).map((k) => [k[0], k[0]]));
|
||||||
|
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(markdownitEmoji, {
|
||||||
|
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(markdownitEmoji, {
|
||||||
|
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) {
|
||||||
|
//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 }
|
||||||
|
return new Picker(pickerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
var tippyInstance: any;
|
var tippyInstance: any;
|
||||||
if (isBrowser()) {
|
if (isBrowser()) {
|
||||||
tippyInstance = tippy("[data-tippy-content]");
|
tippyInstance = tippy("[data-tippy-content]");
|
||||||
|
@ -1300,6 +1412,8 @@ export function personSelectName(pvs: PersonViewSafe): string {
|
||||||
export function initializeSite(site: GetSiteResponse) {
|
export function initializeSite(site: GetSiteResponse) {
|
||||||
UserService.Instance.myUserInfo = site.my_user;
|
UserService.Instance.myUserInfo = site.my_user;
|
||||||
i18n.changeLanguage(getLanguages()[0]);
|
i18n.changeLanguage(getLanguages()[0]);
|
||||||
|
setupEmojiDataModel(site.custom_emojis);
|
||||||
|
setupMarkdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", {
|
const SHORTNUM_SI_FORMAT = new Intl.NumberFormat("en-US", {
|
||||||
|
@ -1405,8 +1519,8 @@ export function nsfwCheck(
|
||||||
return !nsfw || (nsfw && myShowNsfw);
|
return !nsfw || (nsfw && myShowNsfw);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRandomFromList<T>(list?: T[]): T | undefined {
|
export function getRandomFromList<T>(list: T[]): T | undefined {
|
||||||
return list?.at(Math.floor(Math.random() * list.length));
|
return list.length == 0 ? undefined : list.at(Math.floor(Math.random() * list.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1440,3 +1554,25 @@ export function selectableLanguages(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
interface EmojiMartCategory {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
emojis: EmojiMartCustomEmoji[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmojiMartCustomEmoji {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
keywords: string[],
|
||||||
|
skins: EmojiMartSkin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmojiMartSkin {
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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[] });
|
43
yarn.lock
43
yarn.lock
|
@ -1220,6 +1220,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||||
|
|
||||||
|
"@emoji-mart/data@^1.1.0":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.1.2.tgz#777c976f8f143df47cbb23a7077c9ca9fe5fc513"
|
||||||
|
integrity sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==
|
||||||
|
|
||||||
"@eslint/eslintrc@^1.4.1":
|
"@eslint/eslintrc@^1.4.1":
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e"
|
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e"
|
||||||
|
@ -1405,6 +1410,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/emoji-mart@^3.0.9":
|
||||||
|
version "3.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.9.tgz#2f7ef5d9ec194f28029c46c81a5fc1e5b0efa73c"
|
||||||
|
integrity sha512-qdBo/2Y8MXaJ/2spKjDZocuq79GpnOhkwMHnK2GnVFa8WYFgfA+ei6sil3aeWQPCreOKIx9ogPpR5+7MaOqYAA==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/eslint-scope@^3.7.3":
|
"@types/eslint-scope@^3.7.3":
|
||||||
version "3.7.4"
|
version "3.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
||||||
|
@ -1547,6 +1559,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.0.tgz#94c47b9217bbac49d4a67a967fdcdeed89ebb7d0"
|
||||||
integrity sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==
|
integrity sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||||
|
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||||
|
|
||||||
"@types/qs@*":
|
"@types/qs@*":
|
||||||
version "6.9.7"
|
version "6.9.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||||
|
@ -1557,6 +1574,15 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||||
|
|
||||||
|
"@types/react@*":
|
||||||
|
version "18.0.28"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
|
||||||
|
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/retry@0.12.0":
|
"@types/retry@0.12.0":
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||||
|
@ -1569,6 +1595,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
htmlparser2 "^8.0.0"
|
htmlparser2 "^8.0.0"
|
||||||
|
|
||||||
|
"@types/scheduler@*":
|
||||||
|
version "0.16.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||||
|
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||||
|
|
||||||
"@types/semver@^7.3.12":
|
"@types/semver@^7.3.12":
|
||||||
version "7.3.13"
|
version "7.3.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
|
||||||
|
@ -2963,7 +2994,7 @@ cssesc@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||||
|
|
||||||
csstype@^3.1.0, csstype@^3.1.1:
|
csstype@^3.0.2, csstype@^3.1.0, csstype@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
|
||||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||||
|
@ -3264,6 +3295,11 @@ electron-to-chromium@^1.4.251:
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.264.tgz#2f68a062c38b7a04bf57f3e6954b868672fbdcd3"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.264.tgz#2f68a062c38b7a04bf57f3e6954b868672fbdcd3"
|
||||||
integrity sha512-AZ6ZRkucHOQT8wke50MktxtmcWZr67kE17X/nAXFf62NIdMdgY6xfsaJD5Szoy84lnkuPWH+4tTNE3s2+bPCiw==
|
integrity sha512-AZ6ZRkucHOQT8wke50MktxtmcWZr67kE17X/nAXFf62NIdMdgY6xfsaJD5Szoy84lnkuPWH+4tTNE3s2+bPCiw==
|
||||||
|
|
||||||
|
emoji-mart@^5.4.0:
|
||||||
|
version "5.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
|
||||||
|
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
|
||||||
|
|
||||||
emoji-regex@^7.0.1:
|
emoji-regex@^7.0.1:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||||
|
@ -5736,6 +5772,11 @@ markdown-it-container@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b"
|
resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b"
|
||||||
integrity sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==
|
integrity sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==
|
||||||
|
|
||||||
|
markdown-it-emoji@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz#cd42421c2fda1537d9cc12b9923f5c8aeb9029c8"
|
||||||
|
integrity sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==
|
||||||
|
|
||||||
markdown-it-footnote@^3.0.3:
|
markdown-it-footnote@^3.0.3:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8"
|
resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8"
|
||||||
|
|
Loading…
Reference in a new issue