mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-25 15:12:08 -05:00
merge: pull request #730 from revoltchat/feat/emojis
This commit is contained in:
commit
2b65e98cd3
13 changed files with 279 additions and 66 deletions
2
external/lang
vendored
2
external/lang
vendored
|
@ -1 +1 @@
|
|||
Subproject commit d4bc47b729c7e69ce97216469692b39f4cd1640e
|
||||
Subproject commit 58408da6c4090dd3a7808a663eaa95b8b1da7603
|
|
@ -73,7 +73,7 @@
|
|||
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
||||
"@insertish/vite-plugin-babel-macros": "^1.0.5",
|
||||
"@preact/preset-vite": "^2.0.0",
|
||||
"@revoltchat/ui": "1.0.70",
|
||||
"@revoltchat/ui": "1.0.72",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@styled-icons/boxicons-logos": "^10.38.0",
|
||||
"@styled-icons/boxicons-regular": "^10.38.0",
|
||||
|
@ -123,7 +123,7 @@
|
|||
"preact-context-menu": "0.4.1",
|
||||
"preact-i18n": "^2.4.0-preactx",
|
||||
"prettier": "^2.3.1",
|
||||
"prismjs": "^1.23.0",
|
||||
"prismjs": "^1.28.0",
|
||||
"qrcode.react": "^3.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-device-detect": "2.2.2",
|
||||
|
|
|
@ -7,13 +7,7 @@ import { ulid } from "ulid";
|
|||
|
||||
import { Text } from "preact-i18n";
|
||||
import { memo } from "preact/compat";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "preact/hooks";
|
||||
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { IconButton, Picker } from "@revoltchat/ui";
|
||||
|
||||
|
@ -28,7 +22,7 @@ import {
|
|||
SMOOTH_SCROLL_ON_RECEIVE,
|
||||
} from "../../../lib/renderer/Singleton";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { state, useApplicationState } from "../../../mobx/State";
|
||||
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||
|
||||
import { emojiDictionary } from "../../../assets/emojis";
|
||||
|
@ -40,8 +34,8 @@ import {
|
|||
uploadFile,
|
||||
} from "../../../controllers/client/jsx/legacy/FileUploads";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
import { RenderEmoji } from "../../markdown/plugins/emoji";
|
||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||
import Emoji from "../Emoji";
|
||||
import { PermissionTooltip } from "../Tooltip";
|
||||
import FilePreview from "./bars/FilePreview";
|
||||
import ReplyBar from "./bars/ReplyBar";
|
||||
|
@ -149,6 +143,56 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$");
|
|||
// Tests for code block delimiters (``` at start of line)
|
||||
const RE_CODE_DELIMITER = new RegExp("^```", "gm");
|
||||
|
||||
const HackAlertThisFileWillBeReplaced = observer(({ channel }: Props) => {
|
||||
const renderEmoji = useMemo(
|
||||
() =>
|
||||
memo(({ emoji }: { emoji: string }) => (
|
||||
<a
|
||||
onClick={() => {
|
||||
const v = state.draft.get(channel._id);
|
||||
state.draft.set(
|
||||
channel._id,
|
||||
`${v ? `${v} ` : ""}:${emoji}:`,
|
||||
);
|
||||
}}>
|
||||
<RenderEmoji match={emoji} {...({} as any)} />
|
||||
</a>
|
||||
)),
|
||||
[],
|
||||
);
|
||||
|
||||
const emojis: Record<string, any> = {
|
||||
default: Object.keys(emojiDictionary),
|
||||
};
|
||||
|
||||
// ! FIXME: also expose typing from component
|
||||
const categories: any[] = [];
|
||||
|
||||
for (const server of state.ordering.orderedServers) {
|
||||
// ! FIXME: add a separate map on each server for emoji
|
||||
const list = [...channel.client.emojis.values()]
|
||||
.filter((emoji) => emoji.parent.id === server._id)
|
||||
.map((x) => x._id);
|
||||
|
||||
if (list.length > 0) {
|
||||
emojis[server._id] = list;
|
||||
categories.push({
|
||||
id: server._id,
|
||||
name: server.name,
|
||||
iconURL: server.generateIconURL({ max_side: 256 }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Picker
|
||||
emojis={emojis}
|
||||
categories={categories}
|
||||
renderEmoji={renderEmoji}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// ! FIXME: add to app config and load from app config
|
||||
export const CAN_UPLOAD_AT_ONCE = 5;
|
||||
|
||||
|
@ -312,6 +356,7 @@ export default observer(({ channel }: Props) => {
|
|||
async function sendFile(content: string) {
|
||||
if (uploadState.type !== "attached") return;
|
||||
const attachments: string[] = [];
|
||||
setMessage;
|
||||
|
||||
const cancel = Axios.CancelToken.source();
|
||||
const files = uploadState.files;
|
||||
|
@ -470,26 +515,6 @@ export default observer(({ channel }: Props) => {
|
|||
: undefined,
|
||||
});
|
||||
|
||||
const renderEmoji = useMemo(
|
||||
() =>
|
||||
memo(({ emoji }: { emoji: string }) => (
|
||||
<a
|
||||
onClick={() => {
|
||||
const v = state.draft.get(channel._id);
|
||||
setMessage(`${v ? `${v} ` : ""}:${emoji}:`);
|
||||
}}>
|
||||
<Emoji
|
||||
emoji={
|
||||
emojiDictionary[
|
||||
emoji as keyof typeof emojiDictionary
|
||||
]
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
)),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoComplete {...autoCompleteProps} />
|
||||
|
@ -533,10 +558,7 @@ export default observer(({ channel }: Props) => {
|
|||
/>
|
||||
<FloatingLayer>
|
||||
{picker && (
|
||||
<Picker
|
||||
emojis={Object.keys(emojiDictionary)}
|
||||
renderEmoji={renderEmoji}
|
||||
/>
|
||||
<HackAlertThisFileWillBeReplaced channel={channel} />
|
||||
)}
|
||||
</FloatingLayer>
|
||||
<Base>
|
||||
|
@ -659,11 +681,6 @@ export default observer(({ channel }: Props) => {
|
|||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{/*<Action>
|
||||
<IconButton>
|
||||
<Box size={24} />
|
||||
</IconButton>
|
||||
</Action>*/}
|
||||
{state.experiments.isEnabled("picker") && (
|
||||
<Action>
|
||||
<IconButton onClick={() => setPicker(!picker)}>
|
||||
|
|
|
@ -16,6 +16,7 @@ enum Badges {
|
|||
Paw = 128,
|
||||
EarlyAdopter = 256,
|
||||
ReservedRelevantJokeBadge1 = 512,
|
||||
ReservedRelevantJokeBadge2 = 1024,
|
||||
}
|
||||
|
||||
const BadgesBase = styled.div`
|
||||
|
@ -135,6 +136,13 @@ export default function UserBadges({ badges, uid }: Props) {
|
|||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.ReservedRelevantJokeBadge2 ? (
|
||||
<Tooltip content="It's Morbin Time">
|
||||
<img src="/assets/badges/amorbus.svg" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{badges & Badges.Paw ? (
|
||||
<Tooltip content="🦊">
|
||||
<img src="/assets/badges/paw.svg" />
|
||||
|
|
|
@ -180,7 +180,7 @@ const Container = styled.div<{ largeEmoji: boolean }>`
|
|||
/**
|
||||
* Regex for matching execessive blockquotes
|
||||
*/
|
||||
const RE_QUOTE = /(^[>\s][>\s])[>\s]+([^]+$)/gm;
|
||||
const RE_QUOTE = /(^(?:>\s){3})[>\s]+(.*$)/gm;
|
||||
|
||||
/**
|
||||
* Regex for matching open angled bracket
|
||||
|
|
|
@ -15,15 +15,19 @@ const Emoji = styled.img`
|
|||
`;
|
||||
|
||||
export function RenderEmoji({ match }: CustomComponentProps) {
|
||||
const url =
|
||||
match in emojiDictionary
|
||||
? parseEmoji(emojiDictionary[match as keyof typeof emojiDictionary])
|
||||
: clientController.getAvailableClient().emojis!.get(match)!
|
||||
.imageURL;
|
||||
|
||||
return (
|
||||
<Emoji
|
||||
alt={match}
|
||||
loading="lazy"
|
||||
className="emoji"
|
||||
draggable={false}
|
||||
src={parseEmoji(
|
||||
emojiDictionary[match as keyof typeof emojiDictionary],
|
||||
)}
|
||||
src={url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ export default function MemberList({
|
|||
)}
|
||||
{entry.type !== "no_offline" && (
|
||||
<>
|
||||
{" - "}
|
||||
{" – "}
|
||||
{entry.users.length}
|
||||
</>
|
||||
)}
|
||||
|
|
61
src/components/settings/customisation/EmojiUploader.tsx
Normal file
61
src/components/settings/customisation/EmojiUploader.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { Server } from "revolt.js";
|
||||
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { Form } from "@revoltchat/ui";
|
||||
|
||||
import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads";
|
||||
|
||||
interface Props {
|
||||
server: Server;
|
||||
}
|
||||
|
||||
export function EmojiUploader({ server }: Props) {
|
||||
const [fileId, setFileId] = useState<string>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Upload Emoji</h3>
|
||||
<Form
|
||||
schema={{
|
||||
name: "text",
|
||||
file: "custom",
|
||||
}}
|
||||
data={{
|
||||
name: {
|
||||
field: "Name",
|
||||
palette: "secondary",
|
||||
},
|
||||
file: {
|
||||
element: (
|
||||
<FileUploader
|
||||
style="icon"
|
||||
width={100}
|
||||
height={100}
|
||||
fileType="emojis"
|
||||
behaviour="upload"
|
||||
previewAfterUpload
|
||||
maxFileSize={500000}
|
||||
remove={async () => void setFileId("")}
|
||||
onUpload={async (id) => void setFileId(id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
submitBtn={{
|
||||
children: "Save",
|
||||
palette: "secondary",
|
||||
disabled: !fileId,
|
||||
}}
|
||||
onSubmit={async ({ name }) => {
|
||||
await server.client.api.put(`/custom/emoji/${fileId}`, {
|
||||
name,
|
||||
parent: { type: "Server", id: server._id },
|
||||
});
|
||||
|
||||
setFileId("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -17,7 +17,11 @@ import { takeError } from "../error";
|
|||
|
||||
type BehaviourType =
|
||||
| { behaviour: "ask"; onChange: (file: File) => void }
|
||||
| { behaviour: "upload"; onUpload: (id: string) => Promise<void> }
|
||||
| {
|
||||
behaviour: "upload";
|
||||
onUpload: (id: string) => Promise<void>;
|
||||
previewAfterUpload?: boolean;
|
||||
}
|
||||
| {
|
||||
behaviour: "multi";
|
||||
onChange: (files: File[]) => void;
|
||||
|
@ -48,7 +52,8 @@ type Props = BehaviourType &
|
|||
| "icons"
|
||||
| "avatars"
|
||||
| "attachments"
|
||||
| "banners";
|
||||
| "banners"
|
||||
| "emojis";
|
||||
maxFileSize: number;
|
||||
remove: () => Promise<void>;
|
||||
};
|
||||
|
@ -114,6 +119,17 @@ export function FileUploader(props: Props) {
|
|||
const client = useClient();
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [previewFile, setPreviewFile] = useState<File>(null!);
|
||||
const [generatedPreviewURL, setGeneratedPreviewURL] = useState("");
|
||||
useEffect(() => {
|
||||
if (previewFile) {
|
||||
const url: string = URL.createObjectURL(previewFile);
|
||||
setGeneratedPreviewURL(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
setGeneratedPreviewURL("");
|
||||
}, [previewFile]);
|
||||
|
||||
function onClick() {
|
||||
if (uploading) return;
|
||||
|
@ -136,6 +152,10 @@ export function FileUploader(props: Props) {
|
|||
files[0],
|
||||
),
|
||||
);
|
||||
|
||||
if (props.previewAfterUpload) {
|
||||
setPreviewFile(files[0]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return modalController.push({
|
||||
|
@ -164,7 +184,11 @@ export function FileUploader(props: Props) {
|
|||
} else {
|
||||
onClick();
|
||||
}
|
||||
} else if (props.previewURL) {
|
||||
} else if (props.previewURL || previewFile) {
|
||||
if (previewFile) {
|
||||
setPreviewFile(null!);
|
||||
}
|
||||
|
||||
props.remove();
|
||||
} else {
|
||||
onClick();
|
||||
|
@ -266,7 +290,11 @@ export function FileUploader(props: Props) {
|
|||
style={{
|
||||
backgroundImage:
|
||||
style === "icon"
|
||||
? `url('${previewURL ?? defaultPreview}')`
|
||||
? `url('${
|
||||
generatedPreviewURL ??
|
||||
previewURL ??
|
||||
defaultPreview
|
||||
}')`
|
||||
: previewURL
|
||||
? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')`
|
||||
: "none",
|
||||
|
@ -288,7 +316,7 @@ export function FileUploader(props: Props) {
|
|||
<span onClick={removeOrUpload}>
|
||||
{uploading ? (
|
||||
<Text id="app.main.channel.uploading_file" />
|
||||
) : props.previewURL ? (
|
||||
) : props.previewURL || previewFile ? (
|
||||
<Text id="app.settings.actions.remove" />
|
||||
) : (
|
||||
<Text id="app.settings.actions.upload" />
|
||||
|
|
|
@ -43,8 +43,9 @@ export const EXPERIMENTS: {
|
|||
"This will enable the experimental plugin API. Only touch this if you know what you're doing.",
|
||||
},
|
||||
picker: {
|
||||
title: "Emoji Picker",
|
||||
description: "This will enable a work-in-progress emoji picker.",
|
||||
title: "Custom Emoji",
|
||||
description:
|
||||
"This will enable a work-in-progress emoji picker and custom emoji settings.",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
Envelope,
|
||||
UserX,
|
||||
Trash,
|
||||
HappyBeaming,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||
|
@ -15,6 +16,8 @@ import { Text } from "preact-i18n";
|
|||
|
||||
import { LineDivider } from "@revoltchat/ui";
|
||||
|
||||
import { state } from "../../mobx/State";
|
||||
|
||||
import ButtonItem from "../../components/navigation/items/ButtonItem";
|
||||
import { useClient } from "../../controllers/client/ClientController";
|
||||
import RequiresOnline from "../../controllers/client/jsx/RequiresOnline";
|
||||
|
@ -22,6 +25,7 @@ import { modalController } from "../../controllers/modals/ModalController";
|
|||
import { GenericSettings } from "./GenericSettings";
|
||||
import { Bans } from "./server/Bans";
|
||||
import { Categories } from "./server/Categories";
|
||||
import { Emojis } from "./server/Emojis";
|
||||
import { Invites } from "./server/Invites";
|
||||
import { Members } from "./server/Members";
|
||||
import { Overview } from "./server/Overview";
|
||||
|
@ -68,6 +72,15 @@ export default observer(() => {
|
|||
title: <Text id="app.settings.server_pages.roles.title" />,
|
||||
hideTitle: true,
|
||||
},
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.server_pages.customisation.title" />
|
||||
),
|
||||
id: "emojis",
|
||||
icon: <HappyBeaming size={20} />,
|
||||
title: <Text id="app.settings.server_pages.emojis.title" />,
|
||||
hidden: !state.experiments.isEnabled("picker"),
|
||||
},
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.server_pages.management.title" />
|
||||
|
@ -116,6 +129,11 @@ export default observer(() => {
|
|||
<Roles server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>
|
||||
<Route path="/server/:server/settings/emojis">
|
||||
<RequiresOnline>
|
||||
<Emojis server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>
|
||||
<Route>
|
||||
<Overview server={server} />
|
||||
</Route>
|
||||
|
|
83
src/pages/settings/server/Emojis.tsx
Normal file
83
src/pages/settings/server/Emojis.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { Server } from "revolt.js";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Button, Column, Row, Stacked } from "@revoltchat/ui";
|
||||
|
||||
import UserShort from "../../../components/common/user/UserShort";
|
||||
import { EmojiUploader } from "../../../components/settings/customisation/EmojiUploader";
|
||||
import { modalController } from "../../../controllers/modals/ModalController";
|
||||
|
||||
interface Props {
|
||||
server: Server;
|
||||
}
|
||||
|
||||
const List = styled.div`
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const Emoji = styled(Column)`
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary-background);
|
||||
`;
|
||||
|
||||
const Preview = styled.img`
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
border-radius: var(--border-radius);
|
||||
`;
|
||||
|
||||
const UserInfo = styled(Row)`
|
||||
font-size: 12px;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Emojis = observer(({ server }: Props) => {
|
||||
const emoji = [...server.client.emojis.values()].filter(
|
||||
(x) => x.parent.id === server._id,
|
||||
);
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<EmojiUploader server={server} />
|
||||
<h3>
|
||||
<Text id="app.settings.server_pages.emojis.title" />
|
||||
{" – "}
|
||||
{emoji.length}
|
||||
</h3>
|
||||
<List>
|
||||
{emoji.map((emoji) => (
|
||||
<Emoji key={emoji._id} centred>
|
||||
<Stacked>
|
||||
<Preview src={emoji.imageURL} />
|
||||
</Stacked>
|
||||
<span>{`:${emoji.name}:`}</span>
|
||||
<UserInfo centred>
|
||||
<UserShort user={emoji.creator} />
|
||||
</UserInfo>
|
||||
<Button
|
||||
palette="plain"
|
||||
onClick={() =>
|
||||
modalController.writeText(emoji._id)
|
||||
}>
|
||||
<Text id="app.context_menu.copy_id" />
|
||||
</Button>
|
||||
<Button palette="plain" onClick={() => emoji.delete()}>
|
||||
<Text id="app.special.modals.actions.delete" />
|
||||
</Button>
|
||||
</Emoji>
|
||||
))}
|
||||
</List>
|
||||
</Column>
|
||||
);
|
||||
});
|
19
yarn.lock
19
yarn.lock
|
@ -2240,9 +2240,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@revoltchat/ui@npm:1.0.70":
|
||||
version: 1.0.70
|
||||
resolution: "@revoltchat/ui@npm:1.0.70"
|
||||
"@revoltchat/ui@npm:1.0.72":
|
||||
version: 1.0.72
|
||||
resolution: "@revoltchat/ui@npm:1.0.72"
|
||||
dependencies:
|
||||
"@styled-icons/boxicons-logos": ^10.38.0
|
||||
"@styled-icons/boxicons-regular": ^10.38.0
|
||||
|
@ -2256,7 +2256,7 @@ __metadata:
|
|||
react-virtuoso: ^2.12.0
|
||||
peerDependencies:
|
||||
revolt.js: "*"
|
||||
checksum: 70eba00f8b2d4fed3f83cdd64488ab4eb99acc9dd0427333a41f203912d9664f878ec04300bc3eaea2a67fef1620472cce9ff7048ef1280f950825593cf316a7
|
||||
checksum: 8ca6d68709591a9505cc62089ab42fe887fcd4ceaac81c7f673b41b7be1d850ba852d0c2dc09bb77c8e1bce22b3255ebfbf95922d57aa2c3d2288aacd7819a25
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -3626,7 +3626,7 @@ __metadata:
|
|||
"@hcaptcha/react-hcaptcha": ^0.3.6
|
||||
"@insertish/vite-plugin-babel-macros": ^1.0.5
|
||||
"@preact/preset-vite": ^2.0.0
|
||||
"@revoltchat/ui": 1.0.70
|
||||
"@revoltchat/ui": 1.0.72
|
||||
"@rollup/plugin-replace": ^2.4.2
|
||||
"@styled-icons/boxicons-logos": ^10.38.0
|
||||
"@styled-icons/boxicons-regular": ^10.38.0
|
||||
|
@ -3678,7 +3678,7 @@ __metadata:
|
|||
preact-context-menu: 0.4.1
|
||||
preact-i18n: ^2.4.0-preactx
|
||||
prettier: ^2.3.1
|
||||
prismjs: ^1.23.0
|
||||
prismjs: ^1.28.0
|
||||
qrcode.react: ^3.0.2
|
||||
react-beautiful-dnd: ^13.1.0
|
||||
react-device-detect: 2.2.2
|
||||
|
@ -7309,13 +7309,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prismjs@npm:^1.23.0":
|
||||
version: 1.24.1
|
||||
resolution: "prismjs@npm:1.24.1"
|
||||
checksum: e5d14a4ba56773122039295bd760c72106acc964e04cb9831b9ae7e7a58f67ccac6c053e77e21f1018a3684f31d35bb065c0c81fd4ff00b73b1570c3ace4aef0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prismjs@npm:^1.24.1, prismjs@npm:^1.28.0":
|
||||
version: 1.28.0
|
||||
resolution: "prismjs@npm:1.28.0"
|
||||
|
|
Loading…
Reference in a new issue