Add emoji autocomplete when writing notes

This commit is contained in:
hzrd149 2023-08-31 12:48:31 -05:00
parent 7f28a3b83f
commit c10a17eee7
18 changed files with 4866 additions and 32004 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add emoji autocomplete when writing notes

View File

@ -18,10 +18,12 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@getalby/bitcoin-connect-react": "^1.0.0",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"bech32": "^2.0.0",
"cheerio": "^1.0.0-rc.12",
"dayjs": "^1.11.9",
"debug": "^4.3.4",
"emojilib": "2",
"framer-motion": "^10.16.0",
"hls.js": "^1.4.10",
"idb": "^7.1.1",
@ -29,6 +31,7 @@
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.79.0",
"light-bolt11-decoder": "^3.0.0",
"match-sorter": "^6.3.1",
"nanoid": "^4.0.2",
"ngeohash": "^0.6.3",
"noble-secp256k1": "^1.2.14",
@ -55,6 +58,7 @@
"@types/ngeohash": "^0.6.4",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
"@vitejs/plugin-react": "^4.0.4",
"cypress": "^12.17.4",
"prettier": "^3.0.2",

View File

@ -40,17 +40,44 @@ import UserListsTab from "./views/user/lists";
import "./services/emoji-packs";
import BrowseListView from "./views/lists/browse";
import { css, Global } from "@emotion/react";
const StreamsView = React.lazy(() => import("./views/streams"));
const StreamView = React.lazy(() => import("./views/streams/stream"));
const SearchView = React.lazy(() => import("./views/search"));
const MapView = React.lazy(() => import("./views/map"));
const overrideReactTextareaAutocompleteStyles = css`
.rta__autocomplete {
z-index: var(--chakra-zIndices-popover);
font-size: var(--chakra-fontSizes-md);
}
.rta__list {
background: var(--chakra-colors-chakra-subtle-bg);
color: var(--chakra-colors-chakra-body-text);
border: var(--chakra-borders-1px) var(--chakra-colors-chakra-border-color);
border-radius: var(--chakra-sizes-1);
overflow: hidden;
}
.rta__entity {
background: none;
color: inherit;
padding: var(--chakra-sizes-1) var(--chakra-sizes-2);
}
.rta__entity--selected {
background: var(--chakra-ring-color);
}
.rta__item:not(:last-child) {
border-bottom: var(--chakra-borders-1px) var(--chakra-colors-chakra-border-color);
}
`;
const RootPage = () => {
useSetColorMode();
return (
<PageProviders>
<Global styles={overrideReactTextareaAutocompleteStyles} />
<Layout>
<ScrollRestoration />
<Suspense fallback={<Spinner />}>

View File

@ -1,17 +1,16 @@
import { Image } from "@chakra-ui/react";
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import { DraftNostrEvent, NostrEvent, isEmojiTag } from "../../types/nostr-event";
import { getMatchEmoji } from "../../helpers/regexp";
export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNostrEvent) {
return embedJSX(content, {
regexp: /:([a-zA-Z0-9_]+):/gi,
regexp: getMatchEmoji(),
render: (match) => {
const emojiTag = note.tags.find(
(tag) => tag[0] === "emoji" && tag[1].toLowerCase() === match[1].toLowerCase() && tag[2],
);
const emojiTag = note.tags.filter(isEmojiTag).find((t) => t[1].toLowerCase() === match[1].toLowerCase());
if (emojiTag) {
return (
<Image src={emojiTag[2]} height="1.5em" display="inline-block" verticalAlign="middle" title={match[1]} />
<Image src={emojiTag[2]} h="1.2em" w="1.2em" display="inline-block" verticalAlign="middle" title={match[1]} />
);
}
return null;

View File

@ -0,0 +1,47 @@
import React from "react";
import { Image, Textarea, TextareaProps } from "@chakra-ui/react";
import ReactTextareaAutocomplete, {
ItemComponentProps,
TextareaProps as ReactTextareaAutocompleteProps,
} from "@webscopeio/react-textarea-autocomplete";
import "@webscopeio/react-textarea-autocomplete/style.css";
import { matchSorter } from "match-sorter/dist/match-sorter.esm.js";
import { Emoji, useContextEmojis } from "../providers/emoji-provider";
const Item = ({ entity: { name, char, url } }: ItemComponentProps<Emoji>) => {
if (url)
return (
<span>
{name}: <Image src={url} h="1.2em" w="1.2em" display="inline-block" verticalAlign="middle" title={name} />
</span>
);
else return <span>{`${name}: ${char}`}</span>;
};
const Loading: ReactTextareaAutocompleteProps<
Emoji,
React.TextareaHTMLAttributes<HTMLTextAreaElement>
>["loadingComponent"] = ({ data }) => <div>Loading</div>;
export default function MagicTextArea({ ...props }: TextareaProps) {
const emojis = useContextEmojis();
return (
<Textarea
{...props}
as={ReactTextareaAutocomplete<Emoji>}
loadingComponent={Loading}
renderToBody
minChar={0}
trigger={{
":": {
dataProvider: (token: string) => {
return matchSorter(emojis, token.trim(), { keys: ["keywords"] }).slice(0, 10);
},
component: Item,
output: (item: Emoji) => item.char,
},
}}
/>
);
}

View File

@ -1,4 +1,4 @@
import React, { useRef, useState } from "react";
import React, { useMemo, useRef, useState } from "react";
import {
Modal,
ModalOverlay,
@ -6,12 +6,12 @@ import {
ModalBody,
Flex,
Button,
Textarea,
Text,
useDisclosure,
VisuallyHiddenInput,
IconButton,
useToast,
Box,
Heading,
} from "@chakra-ui/react";
import dayjs from "dayjs";
@ -25,8 +25,10 @@ import { NoteLink } from "../note-link";
import { NoteContents } from "../note/note-contents";
import { PublishDetails } from "../publish-details";
import { TrustProvider } from "../../providers/trust";
import { ensureNotifyPubkeys, finalizeNote, getContentMentions } from "../../helpers/nostr/post";
import { createEmojiTags, ensureNotifyPubkeys, finalizeNote, getContentMentions } from "../../helpers/nostr/post";
import { UserAvatarStack } from "../compact-user-stack";
import MagicTextArea from "../magic-textarea";
import { useContextEmojis } from "../../providers/emoji-provider";
function emptyDraft(): DraftNostrEvent {
return {
@ -47,12 +49,12 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
const toast = useToast();
const { requestSignature } = useSigningContext();
const writeRelays = useWriteRelayUrls();
const [waiting, setWaiting] = useState(false);
const [signing, setSigning] = useState(false);
const [publishAction, setPublishAction] = useState<NostrPublishAction>();
const { isOpen: showPreview, onToggle: togglePreview } = useDisclosure();
const [draft, setDraft] = useState<DraftNostrEvent>(() => Object.assign(emptyDraft(), initialDraft));
const imageUploadRef = useRef<HTMLInputElement | null>(null);
const [uploading, setUploading] = useState(false);
const emojis = useContextEmojis();
const uploadImage = async (imageFile: File) => {
try {
@ -77,14 +79,19 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
setDraft((d) => ({ ...d, content: event.target.value }));
};
const finalDraft = useMemo(() => {
let updatedDraft = finalizeNote(draft);
const contentMentions = getContentMentions(draft.content);
updatedDraft = createEmojiTags(updatedDraft, emojis);
updatedDraft = ensureNotifyPubkeys(updatedDraft, contentMentions);
return updatedDraft;
}, [draft, emojis]);
const handleSubmit = async () => {
try {
setWaiting(true);
let updatedDraft = finalizeNote(draft);
const contentMentions = getContentMentions(draft.content);
updatedDraft = ensureNotifyPubkeys(updatedDraft, contentMentions);
const signed = await requestSignature(updatedDraft);
setWaiting(false);
setSigning(true);
const signed = await requestSignature(finalDraft);
setSigning(false);
const pub = new NostrPublishAction("Post", writeRelays, signed);
setPublishAction(pub);
@ -115,22 +122,26 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
Replying to: <NoteLink noteId={refs.replyId} />
</Text>
)}
{showPreview ? (
<TrustProvider trust>
<NoteContents event={finalizeNote(draft)} />
</TrustProvider>
) : (
<Textarea
autoFocus
mb="2"
value={draft.content}
onChange={handleContentChange}
rows={5}
onPaste={(e) => {
const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
if (imageFile) uploadImage(imageFile);
}}
/>
<MagicTextArea
autoFocus
mb="2"
value={draft.content}
onChange={handleContentChange}
rows={5}
onPaste={(e) => {
const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
if (imageFile) uploadImage(imageFile);
}}
/>
{draft.content.length > 0 && (
<Box>
<Heading size="sm">Preview:</Heading>
<Box borderWidth={1} borderRadius="md" p="2">
<TrustProvider trust>
<NoteContents event={finalDraft} />
</TrustProvider>
</Box>
</Box>
)}
<Flex gap="2" alignItems="center" justifyContent="flex-end">
<Flex mr="auto" gap="2">
@ -152,9 +163,8 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
/>
</Flex>
<UserAvatarStack label="Mentions" pubkeys={getContentMentions(draft.content)} />
{draft.content.length > 0 && <Button onClick={togglePreview}>Preview</Button>}
<Button onClick={onClose}>Cancel</Button>
<Button colorScheme="blue" type="submit" isLoading={waiting} onClick={handleSubmit} isDisabled={!canSubmit}>
<Button colorScheme="blue" type="submit" isLoading={signing} onClick={handleSubmit} isDisabled={!canSubmit}>
Post
</Button>
</Flex>
@ -166,7 +176,9 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
<Modal isOpen={isOpen} onClose={onClose} size="4xl" closeOnOverlayClick={!!publishAction}>
<ModalOverlay />
<ModalContent>
<ModalBody padding={["2", "2", "4"]}>{renderContent()}</ModalBody>
<ModalBody display="flex" flexDirection="column" padding={["2", "2", "4"]} gap="2">
{renderContent()}
</ModalBody>
</ModalContent>
</Modal>
);

View File

@ -25,6 +25,7 @@ function EmojiPack({ addr, onSelect }: { addr: string; onSelect: ReactionPickerP
key={emoji.name}
icon={<Image src={emoji.url} height="1.2rem" />}
aria-label={emoji.name}
title={emoji.name}
variant="outline"
size="sm"
onClick={() => onSelect(emoji.name, emoji.url)}

View File

@ -1,10 +1,11 @@
import { DraftNostrEvent, NostrEvent, PTag, Tag } from "../../types/nostr-event";
import { getMatchHashtag } from "../regexp";
import { getMatchEmoji, getMatchHashtag } from "../regexp";
import { normalizeToHex } from "../nip19";
import { getReferences } from "./events";
import { getEventRelays } from "../../services/event-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
import { getPubkey, safeDecode } from "../nip19";
import { Emoji } from "../../providers/emoji-provider";
function addTag(tags: Tag[], tag: Tag, overwrite = false) {
if (tags.some((t) => t[0] === tag[0] && t[1] === tag[1])) {
@ -91,6 +92,21 @@ export function createHashtagTags(draft: DraftNostrEvent) {
return updatedDraft;
}
export function createEmojiTags(draft: DraftNostrEvent, emojis: Emoji[]) {
const updatedDraft: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
// create tags for all occurrences of #hashtag
const matches = updatedDraft.content.matchAll(getMatchEmoji());
for (const [_, name] of matches) {
const emoji = emojis.find((e) => e.name === name);
if (emoji?.url) {
updatedDraft.tags = addTag(updatedDraft.tags, ["emoji", emoji.name, emoji.url]);
}
}
return updatedDraft;
}
export function finalizeNote(draft: DraftNostrEvent) {
let updated = draft;
updated = createHashtagTags(updated);

View File

@ -3,6 +3,7 @@ export const getMatchNostrLink = () =>
export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}]+)/gu;
export const getMatchLink = () =>
/https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{Letter}\p{Number}&\.-\/\?=#\-@%\+_,:]*)/gu;
export const getMatchEmoji = () => /:([a-zA-Z0-9_]+):/gi;
// read more https://www.regular-expressions.info/unicode.html#category
export function stripInvisibleChar(str?: string) {

View File

@ -1,3 +1,4 @@
import "./polyfill";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import { GlobalProviders } from "./providers";

1
src/polyfill.ts Normal file
View File

@ -0,0 +1 @@
window.global = window;

View File

@ -0,0 +1,46 @@
import { PropsWithChildren, createContext, useContext } from "react";
import { lib } from "emojilib";
import useUserEmojiPacks from "../hooks/use-users-emoji-packs";
import useReplaceableEvents from "../hooks/use-replaceable-events";
import { useCurrentAccount } from "../hooks/use-current-account";
import { isEmojiTag } from "../types/nostr-event";
const defaultEmojis = Object.entries(lib).map(([name, emojiObject]) => ({
...emojiObject,
keywords: [name, ...emojiObject.keywords],
name,
}));
export type Emoji = { name: string; keywords: string[]; char: string; url?: string };
const EmojiContext = createContext<Emoji[]>([]);
export function useContextEmojis() {
return useContext(EmojiContext);
}
export function DefaultEmojiProvider({ children }: PropsWithChildren) {
return <EmojiProvider emojis={defaultEmojis}>{children}</EmojiProvider>;
}
export function UserEmojiProvider({ children }: PropsWithChildren) {
const account = useCurrentAccount();
const userPacks = useUserEmojiPacks(account?.pubkey);
const events = useReplaceableEvents(userPacks?.packs);
const emojis = events
.map((event) =>
event.tags.filter(isEmojiTag).map((t) => ({ name: t[1], url: t[2], keywords: [t[1]], char: `:${t[1]}:` })),
)
.flat();
console.log(userPacks, emojis);
return <EmojiProvider emojis={emojis}>{children}</EmojiProvider>;
}
export default function EmojiProvider({ children, emojis }: PropsWithChildren & { emojis: Emoji[] }) {
const parent = useContext(EmojiContext);
return <EmojiContext.Provider value={[...parent, ...emojis]}>{children}</EmojiContext.Provider>;
}

View File

@ -7,6 +7,7 @@ import DeleteEventProvider from "./delete-event-provider";
import { InvoiceModalProvider } from "./invoice-modal";
import NotificationTimelineProvider from "./notification-timeline";
import PostModalProvider from "./post-modal-provider";
import { DefaultEmojiProvider, UserEmojiProvider } from "./emoji-provider";
// Top level providers, should be render as close to the root as possible
export const GlobalProviders = ({ children }: { children: React.ReactNode }) => {
@ -27,7 +28,11 @@ export function PageProviders({ children }: { children: React.ReactNode }) {
<DeleteEventProvider>
<InvoiceModalProvider>
<NotificationTimelineProvider>
<PostModalProvider>{children}</PostModalProvider>
<DefaultEmojiProvider>
<UserEmojiProvider>
<PostModalProvider>{children}</PostModalProvider>
</UserEmojiProvider>
</DefaultEmojiProvider>
</NotificationTimelineProvider>
</InvoiceModalProvider>
</DeleteEventProvider>

11
src/types/emojilib.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
type EmojiShape = {
keywords: string[];
char: string;
fitzpatrick_scale: boolean;
category: string;
};
declare module "emojilib" {
const lib: { [key: string]: EmojiShape };
export { lib };
}

View File

@ -3,6 +3,7 @@ export type ATag = ["a", string] | ["a", string, string];
export type PTag = ["p", string] | ["p", string, string] | ["p", string, string, string];
export type RTag = ["r", string] | ["r", string, string];
export type DTag = ["d"] | ["d", string];
export type EmojiTag = ["emoji", string, string];
export type Tag = string[] | ETag | PTag | RTag | DTag | ATag;
export type NostrEvent = {
@ -38,3 +39,6 @@ export function isDTag(tag: Tag): tag is DTag {
export function isATag(tag: Tag): tag is ATag {
return tag[0] === "a" && tag[1] !== undefined;
}
export function isEmojiTag(tag: Tag): tag is EmojiTag {
return tag[0] === "emoji" && tag[1] !== undefined && tag[2] !== undefined;
}

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { Box, Button, ButtonGroup, Flex, Textarea, useDisclosure, useToast } from "@chakra-ui/react";
import { Box, Button, ButtonGroup, Flex, useDisclosure, useToast } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { Kind } from "nostr-tools";
import dayjs from "dayjs";
@ -8,21 +8,20 @@ import { NostrEvent } from "../../../types/nostr-event";
import { UserAvatarStack } from "../../../components/compact-user-stack";
import { ThreadItem, getThreadMembers } from "../../../helpers/thread";
import { NoteContents } from "../../../components/note/note-contents";
import { addReplyTags, ensureNotifyPubkeys, finalizeNote, getContentMentions } from "../../../helpers/nostr/post";
import {
addReplyTags,
createEmojiTags,
ensureNotifyPubkeys,
finalizeNote,
getContentMentions,
} from "../../../helpers/nostr/post";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import { useSigningContext } from "../../../providers/signing-provider";
import { useWriteRelayUrls } from "../../../hooks/use-client-relays";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import { unique } from "../../../helpers/array";
function NoteContentPreview({ content }: { content: string }) {
const draft = useMemo(
() => finalizeNote({ kind: Kind.Text, content, created_at: dayjs().unix(), tags: [] }),
[content],
);
return <NoteContents event={draft} />;
}
import MagicTextArea from "../../../components/magic-textarea";
import { useContextEmojis } from "../../../providers/emoji-provider";
export type ReplyFormProps = {
item: ThreadItem;
@ -33,12 +32,12 @@ export type ReplyFormProps = {
export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProps) {
const toast = useToast();
const account = useCurrentAccount();
const showPreview = useDisclosure();
const emojis = useContextEmojis();
const { requestSignature } = useSigningContext();
const writeRelays = useWriteRelayUrls();
const threadMembers = useMemo(() => getThreadMembers(item, account?.pubkey), [item, account?.pubkey]);
const { register, getValues, watch, handleSubmit } = useForm({
const { setValue, getValues, watch, handleSubmit } = useForm({
defaultValues: {
content: "",
},
@ -48,15 +47,17 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
watch("content");
const draft = useMemo(() => {
let updated = finalizeNote({ kind: Kind.Text, content: getValues().content, created_at: dayjs().unix(), tags: [] });
updated = createEmojiTags(updated, emojis);
updated = addReplyTags(updated, item.event);
updated = ensureNotifyPubkeys(updated, notifyPubkeys);
return updated;
}, [getValues().content, emojis]);
const submit = handleSubmit(async (values) => {
try {
let draft = finalizeNote({ kind: Kind.Text, content: values.content, created_at: dayjs().unix(), tags: [] });
draft = addReplyTags(draft, item.event);
draft = ensureNotifyPubkeys(draft, notifyPubkeys);
const signed = await requestSignature(draft);
if (!signed) return;
// TODO: write to other users inbox relays
const pub = new NostrPublishAction("Reply", writeRelays, signed);
if (onSubmitted) onSubmitted(signed);
@ -67,24 +68,28 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
return (
<form onSubmit={submit}>
{showPreview.isOpen ? (
<MagicTextArea
placeholder="Reply"
autoFocus
mb="2"
rows={5}
isRequired
value={getValues().content}
onChange={(e) => setValue("content", e.target.value)}
/>
{getValues().content.length > 0 && (
<Box p="2" borderWidth={1} borderRadius="md" mb="2">
<NoteContentPreview content={getValues().content} />
<NoteContents event={draft} />
</Box>
) : (
<Textarea placeholder="Reply" {...register("content")} autoFocus mb="2" rows={5} required />
)}
<Flex gap="2" alignItems="center">
<ButtonGroup size="sm">
<Button onClick={onCancel}>Cancel</Button>
<Button type="submit">Submit</Button>
</ButtonGroup>
<UserAvatarStack label="Notify" pubkeys={notifyPubkeys} />
{getValues().content.length > 0 && (
<Button size="sm" ml="auto" onClick={showPreview.onToggle}>
Preview
</Button>
)}
<Button type="submit" colorScheme="brand" size="sm" ml="auto">
Submit
</Button>
</Flex>
</form>
);

36506
stats.html

File diff suppressed because one or more lines are too long

View File

@ -2762,6 +2762,13 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
"@types/webscopeio__react-textarea-autocomplete@^4.7.2":
version "4.7.2"
resolved "https://registry.yarnpkg.com/@types/webscopeio__react-textarea-autocomplete/-/webscopeio__react-textarea-autocomplete-4.7.2.tgz#605e8a6b4194fb4b6e55df8a19bc8fcd56319cfa"
integrity sha512-e1DZGD+eH19BnllTWCGXAdrMa2kI53wEMuhn/d+wUmnu8//ZI6BiuK/EPdw07fI4+tlyo5qdPZdXdpkoXHJVOw==
dependencies:
"@types/react" "*"
"@types/yauzl@^2.9.1":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
@ -2779,6 +2786,14 @@
"@babel/plugin-transform-react-jsx-source" "^7.22.5"
react-refresh "^0.14.0"
"@webscopeio/react-textarea-autocomplete@^4.9.2":
version "4.9.2"
resolved "https://registry.yarnpkg.com/@webscopeio/react-textarea-autocomplete/-/react-textarea-autocomplete-4.9.2.tgz#b39e57d8048ad2e8790d70073afe63eafa877345"
integrity sha512-9l5lbyA709d5HHvI/COflSnblBJeYGxB2/0ghP3m3YViLzXRMzJwaXqnqz6oA96y7QdR3pQWYtVmkUKA0AUVAA==
dependencies:
custom-event "^1.0.1"
textarea-caret "3.0.2"
"@xobotyi/scrollbar-width@^1.9.5":
version "1.9.5"
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
@ -3462,6 +3477,11 @@ csv@^5.5.3:
csv-stringify "^5.6.5"
stream-transform "^2.1.3"
custom-event@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==
cypress@^12.17.4:
version "12.17.4"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c"
@ -3676,6 +3696,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emojilib@2:
version "2.4.0"
resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e"
integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@ -4981,6 +5006,14 @@ map-obj@^4.0.0:
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
match-sorter@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
dependencies:
"@babel/runtime" "^7.12.5"
remove-accents "0.4.2"
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
@ -5745,6 +5778,11 @@ regjsparser@^0.9.1:
dependencies:
jsesc "~0.5.0"
remove-accents@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
request-progress@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
@ -6295,6 +6333,11 @@ terser@^5.0.0:
commander "^2.20.0"
source-map-support "~0.5.20"
textarea-caret@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.0.2.tgz#f360c48699aa1abf718680a43a31a850665c2caf"
integrity sha512-gRzeti2YS4did7UJnPQ47wrjD+vp+CJIe9zbsu0bJ987d8QVLvLNG9757rqiQTIy4hGIeFauTTJt5Xkn51UkXg==
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"