diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1 @@
+{}
diff --git a/package.json b/package.json
index 9b34a33..e7f9ad0 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,10 @@
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
+ "@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
+ "@webscopeio/react-textarea-autocomplete": "^4.9.2",
"hls.js": "^1.4.6",
+ "lodash": "^4.17.21",
"qr-code-styling": "^1.6.0-rc.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -43,7 +46,9 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
+ "@types/lodash": "^4.14.195",
"@webbtc/webln-types": "^1.0.12",
+ "prettier": "^2.8.8",
"typescript": "^5.1.3"
}
}
diff --git a/src/element/emoji.css b/src/element/emoji.css
new file mode 100644
index 0000000..bf193f2
--- /dev/null
+++ b/src/element/emoji.css
@@ -0,0 +1,6 @@
+.emoji {
+ width: 21px;
+ height: 21px;
+ display: inline-block;
+ margin-bottom: -5px;
+}
diff --git a/src/element/emoji.tsx b/src/element/emoji.tsx
new file mode 100644
index 0000000..1c04b25
--- /dev/null
+++ b/src/element/emoji.tsx
@@ -0,0 +1,33 @@
+import "./emoji.css";
+import { useMemo } from "react";
+
+export type EmojiProps = {
+ name: string;
+ url: string;
+};
+
+export function Emoji({ name, url }: EmojiProps) {
+ return ;
+}
+
+export type EmojiTag = ["emoji", string, string];
+
+export function Emojify({
+ content,
+ emoji,
+}: {
+ content: string;
+ emoji: EmojiTag[];
+}) {
+ const emojified = useMemo(() => {
+ return content.split(/:(\w+):/g).map((i) => {
+ const t = emoji.find((t) => t[1] === i);
+ if (t) {
+ return ;
+ } else {
+ return i;
+ }
+ });
+ }, [content, emoji]);
+ return <>{emojified}>;
+}
diff --git a/src/element/live-chat.css b/src/element/live-chat.css
index b07728a..3f66675 100644
--- a/src/element/live-chat.css
+++ b/src/element/live-chat.css
@@ -74,4 +74,4 @@
display: flex;
align-items: center;
gap: 8px;
-}
\ No newline at end of file
+}
diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx
index 86723cb..2fa0705 100644
--- a/src/element/live-chat.tsx
+++ b/src/element/live-chat.tsx
@@ -1,99 +1,75 @@
import "./live-chat.css";
-import { EventKind, NostrLink, TaggedRawEvent, EventPublisher, parseZap } from "@snort/system";
-import { useState } from "react";
+import {
+ EventKind,
+ NostrLink,
+ TaggedRawEvent,
+ EventPublisher,
+ parseZap,
+} from "@snort/system";
+import { useState, type KeyboardEvent, type ChangeEvent } from "react";
+import useEmoji from "hooks/emoji";
import { System } from "index";
import { useLiveChatFeed } from "hooks/live-chat";
import AsyncButton from "./async-button";
import { Profile } from "./profile";
import { Icon } from "./icon";
import { Text } from "./text";
+import { Textarea } from "./textarea";
import Spinner from "./spinner";
import { useLogin } from "hooks/login";
import { useUserProfile } from "@snort/system-react";
-import { formatSats, formatShort } from "number";
+import { formatSats } from "number";
export interface LiveChatOptions {
- canWrite?: boolean,
- showHeader?: boolean
+ canWrite?: boolean;
+ showHeader?: boolean;
}
-export function LiveChat({ link, options }: { link: NostrLink, options?: LiveChatOptions }) {
- const [chat, setChat] = useState("");
+export function LiveChat({
+ link,
+ options,
+}: {
+ link: NostrLink;
+ options?: LiveChatOptions;
+}) {
const messages = useLiveChatFeed(link);
const login = useLogin();
-
- async function sendChatMessage() {
- const pub = await EventPublisher.nip7();
- if (chat.length > 1) {
- const reply = await pub?.generic(eb => {
- return eb
- .kind(1311 as EventKind)
- .content(chat)
- .tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
- .processContent();
- });
- if (reply) {
- console.debug(reply);
- System.BroadcastEvent(reply);
- }
- setChat("");
- }
- }
-
- function writeMessage() {
- return <>
-
- setChat(v.target.value)}
- value={chat}
- placeholder="Message"
- onKeyDown={async e => {
- if (e.code === "Enter") {
- e.preventDefault();
- await sendChatMessage();
- }
- }}
- />
-
-
-
- Send
-
- >
- }
-
return (
- {(options?.showHeader ?? true) &&
- Stream Chat
-
}
+ {(options?.showHeader ?? true) && (
+
Stream Chat
+ )}
{[...(messages.data ?? [])]
.sort((a, b) => b.created_at - a.created_at)
- .map(a => {
+ .map((a) => {
switch (a.kind) {
case 1311: {
return ;
}
case EventKind.ZapReceipt: {
- return
+ return ;
}
}
return null;
})}
{messages.data === undefined && }
- {(options?.canWrite ?? true) &&
- {login ? writeMessage() :
Please login to write messages!
}
-
}
+ {(options?.canWrite ?? true) && (
+
+ {login ? (
+
+ ) : (
+
Please login to write messages!
+ )}
+
+ )}
);
}
-function ChatMessage({ ev, link }: { ev: TaggedRawEvent, link: NostrLink }) {
+function ChatMessage({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) {
return (
@@ -113,19 +89,82 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
-
- zapped
-
+
+ zapped
{formatSats(parsed.amount)}
-
- sats
+ sats
- {parsed.content &&
- {parsed.content}
-
}
+ {parsed.content &&
{parsed.content}
}
);
-}
\ No newline at end of file
+}
+
+function WriteMessage({ link }: { link: NostrLink }) {
+ const [chat, setChat] = useState("");
+ const login = useLogin();
+ const emojis = useEmoji(login!.pubkey);
+ const names = emojis.map((t) => t.at(1));
+
+ async function sendChatMessage() {
+ const pub = await EventPublisher.nip7();
+ if (chat.length > 1) {
+ let messageEmojis: string[][] = [];
+ for (const name of names) {
+ if (chat.includes(`:${name}:`)) {
+ const e = emojis.find((t) => t.at(1) === name);
+ messageEmojis.push(e as string[]);
+ }
+ }
+ const reply = await pub?.generic((eb) => {
+ eb.kind(1311 as EventKind)
+ .content(chat)
+ .tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
+ .processContent();
+ for (const e of messageEmojis) {
+ eb.tag(e);
+ }
+ return eb;
+ });
+ if (reply) {
+ console.debug(reply);
+ System.BroadcastEvent(reply);
+ }
+ setChat("");
+ }
+ }
+
+ async function onKeyDown(e: KeyboardEvent) {
+ if (e.code === "Enter") {
+ e.preventDefault();
+ await sendChatMessage();
+ }
+ }
+
+ async function onChange(e: ChangeEvent) {
+ // @ts-expect-error
+ setChat(e.target.value);
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Send
+
+ >
+ );
+}
diff --git a/src/element/profile.css b/src/element/profile.css
index 24f5208..a8ca9d6 100644
--- a/src/element/profile.css
+++ b/src/element/profile.css
@@ -14,4 +14,5 @@
background: #A7A7A7;
border: unset;
outline: unset;
-}
\ No newline at end of file
+ object-fit: cover;
+}
diff --git a/src/element/profile.tsx b/src/element/profile.tsx
index 3f7e6a4..c218064 100644
--- a/src/element/profile.tsx
+++ b/src/element/profile.tsx
@@ -5,28 +5,39 @@ import { hexToBech32 } from "@snort/shared";
import { System } from "index";
export interface ProfileOptions {
- showName?: boolean
- showAvatar?: boolean
- suffix?: string
- overrideName?: string
+ showName?: boolean;
+ showAvatar?: boolean;
+ suffix?: string;
+ overrideName?: string;
}
export function getName(pk: string, user?: UserMetadata) {
- const shortPubkey = hexToBech32("npub", pk).slice(0, 12);
- if ((user?.display_name?.length ?? 0) > 0) {
- return user?.display_name;
- }
- if ((user?.name?.length ?? 0) > 0) {
- return user?.name;
- }
- return shortPubkey;
+ const shortPubkey = hexToBech32("npub", pk).slice(0, 12);
+ if ((user?.display_name?.length ?? 0) > 0) {
+ return user?.display_name;
+ }
+ if ((user?.name?.length ?? 0) > 0) {
+ return user?.name;
+ }
+ return shortPubkey;
}
-export function Profile({ pubkey, options }: { pubkey: string, options?: ProfileOptions }) {
- const profile = useUserProfile(System, pubkey);
+export function Profile({
+ pubkey,
+ options,
+}: {
+ pubkey: string;
+ options?: ProfileOptions;
+}) {
+ const profile = useUserProfile(System, pubkey);
- return
- {(options?.showAvatar ?? true) &&
}
- {(options?.showName ?? true) && (options?.overrideName ?? getName(pubkey, profile))}
+ return (
+
+ {(options?.showAvatar ?? true) && (
+
+ )}
+ {(options?.showName ?? true) &&
+ (options?.overrideName ?? getName(pubkey, profile))}
-}
\ No newline at end of file
+ );
+}
diff --git a/src/element/qr-code.tsx b/src/element/qr-code.tsx
index 790633b..91dfc8e 100644
--- a/src/element/qr-code.tsx
+++ b/src/element/qr-code.tsx
@@ -44,7 +44,12 @@ export default function QrCode(props: QrCodeProps) {
} else if (qrRef.current) {
qrRef.current.innerHTML = "";
}
- }, [props.data, props.link]);
+ }, [props.data, props.link, props.width, props.height, props.avatar]);
- return
;
+ return (
+
+ );
}
diff --git a/src/element/text.tsx b/src/element/text.tsx
index 91984b4..d2f8c00 100644
--- a/src/element/text.tsx
+++ b/src/element/text.tsx
@@ -1,25 +1,14 @@
-import "./text.css";
import { useMemo } from "react";
import { TaggedRawEvent } from "@snort/system";
-
-type Emoji = [string, string];
-
-function replaceEmoji(content: string, emoji: Emoji[]) {
- return content.split(/:(\w+):/g).map((i) => {
- const t = emoji.find((a) => a[0] === i);
- if (t) {
- return
;
- } else {
- return i;
- }
- });
-}
+import { type EmojiTag, Emojify } from "./emoji";
export function Text({ ev }: { ev: TaggedRawEvent }) {
const emojis = useMemo(() => {
- return ev.tags
- .filter((t) => t.at(0) === "emoji")
- .map((t) => t.slice(1) as Emoji);
+ return ev.tags.filter((t) => t.at(0) === "emoji").map((t) => t as EmojiTag);
}, [ev]);
- return
{replaceEmoji(ev.content, emojis)};
+ return (
+
+
+
+ );
}
diff --git a/src/element/textarea.css b/src/element/textarea.css
new file mode 100644
index 0000000..df253bf
--- /dev/null
+++ b/src/element/textarea.css
@@ -0,0 +1,29 @@
+.rta__list {
+ border: none;
+}
+.rta__item:not(:last-child) {
+ border: none;
+}
+.rta__entity--selected .emoji-item {
+ text-decoration: none;
+ background: #F838D9;
+}
+
+.emoji-item {
+ color: white;
+ background: #171717;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: 16px;
+ padding: 10px;
+}
+
+.emoji-item:hover {
+ color: #171717;
+ background: white;
+}
+
+.emoji-item .emoji-image {
+ margin: 0 8px;
+}
diff --git a/src/element/textarea.tsx b/src/element/textarea.tsx
new file mode 100644
index 0000000..344b778
--- /dev/null
+++ b/src/element/textarea.tsx
@@ -0,0 +1,63 @@
+import "./textarea.css";
+import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
+import "@webscopeio/react-textarea-autocomplete/style.css";
+import type { KeyboardEvent, ChangeEvent } from "react";
+import { Emoji, type EmojiTag } from "./emoji";
+import uniqWith from "lodash/uniqWith";
+import isEqual from "lodash/isEqual";
+
+interface EmojiItemProps {
+ name: string;
+ url: string;
+}
+
+const EmojiItem = ({ entity: { name, url } }: { entity: EmojiItemProps }) => {
+ return (
+
+ );
+};
+
+interface TextareaProps {
+ emojis: EmojiTag[];
+ value: string;
+ onChange: (e: ChangeEvent
) => void;
+ onKeyDown: (e: KeyboardEvent) => void;
+}
+
+export function Textarea({ emojis, ...props }: TextareaProps) {
+ const emojiDataProvider = async (token: string) => {
+ const results = emojis
+ .map((t) => {
+ return {
+ name: t.at(1) || "",
+ url: t.at(2) || "",
+ };
+ })
+ .filter(({ name }) => name.toLowerCase().includes(token.toLowerCase()));
+ return uniqWith(results, isEqual).slice(0, 5);
+ };
+ const trigger = {
+ ":": {
+ dataProvider: emojiDataProvider,
+ component: EmojiItem,
+ output: (item: EmojiItemProps) => `:${item.name}:`,
+ },
+ };
+
+ return (
+ Loading...}
+ placeholder="Message"
+ autoFocus={false}
+ // @ts-expect-error
+ trigger={trigger}
+ {...props}
+ />
+ );
+}
diff --git a/src/hooks/emoji.tsx b/src/hooks/emoji.tsx
new file mode 100644
index 0000000..818a416
--- /dev/null
+++ b/src/hooks/emoji.tsx
@@ -0,0 +1,70 @@
+import { RequestBuilder, EventKind, FlatNoteStore } from "@snort/system";
+import { useRequestBuilder } from "@snort/system-react";
+import { System } from "index";
+import { useMemo } from "react";
+import type { EmojiTag } from "../element/emoji";
+
+export default function useEmoji(pubkey: string) {
+ const sub = useMemo(() => {
+ const rb = new RequestBuilder(`emoji:${pubkey}`);
+
+ rb.withFilter()
+ .authors([pubkey])
+ .kinds([10030 as EventKind, 30030 as EventKind]);
+
+ return rb;
+ }, [pubkey]);
+
+ const { data } = useRequestBuilder(System, FlatNoteStore, sub);
+ const userEmoji = data ?? [];
+
+ const related = useMemo(() => {
+ if (userEmoji) {
+ const tags = userEmoji.at(0)?.tags ?? [];
+ return tags.filter(
+ (t) => t.at(0) === "a" && t.at(1)?.startsWith(`30030:`)
+ );
+ }
+ return [];
+ }, [userEmoji]);
+
+ const subRelated = useMemo(() => {
+ const splitted = related.map((t) => t.at(1)!.split(":"));
+ const authors = splitted
+ .map((s) => s.at(1))
+ .filter((s) => s)
+ .map((s) => s as string);
+ const identifiers = splitted
+ .map((s) => s.at(2))
+ .filter((s) => s)
+ .map((s) => s as string);
+
+ const rb = new RequestBuilder(`emoji:${pubkey}`);
+
+ rb.withFilter()
+ .kinds([30030 as EventKind])
+ .authors(authors)
+ // @ts-expect-error
+ .tag(["d", identifiers]);
+
+ return rb;
+ }, [related]);
+
+ const { data: relatedData } = useRequestBuilder(
+ System,
+ FlatNoteStore,
+ subRelated
+ );
+ const emojiPacks = relatedData ?? [];
+
+ const emojis = useMemo(() => {
+ return userEmoji
+ .concat(emojiPacks)
+ .map((ev) => {
+ return ev.tags.filter((t) => t.at(0) === "emoji");
+ })
+ .flat() as EmojiTag[];
+ }, [userEmoji, emojiPacks]);
+
+ return emojis;
+}
diff --git a/src/index.tsx b/src/index.tsx
index 0e8f109..9453d28 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,56 +1,56 @@
-import './index.css';
+import "./index.css";
-import React from 'react';
-import ReactDOM from 'react-dom/client';
+import React from "react";
+import ReactDOM from "react-dom/client";
import { NostrSystem } from "@snort/system";
-import { RouterProvider, createBrowserRouter } from 'react-router-dom';
+import { RouterProvider, createBrowserRouter } from "react-router-dom";
-import { RootPage } from './pages/root';
-import { LayoutPage } from 'pages/layout';
-import { StreamPage } from 'pages/stream-page';
-import { ChatPopout } from 'pages/chat-popout';
-import { LoginStore } from 'login';
+import { RootPage } from "./pages/root";
+import { LayoutPage } from "pages/layout";
+import { StreamPage } from "pages/stream-page";
+import { ChatPopout } from "pages/chat-popout";
+import { LoginStore } from "login";
-export const System = new NostrSystem({
-
-});
+export const System = new NostrSystem({});
export const Login = new LoginStore();
export const Relays = [
"wss://relay.snort.social",
"wss://nos.lol",
"wss://relay.damus.io",
- "wss://nostr.wine"
+ "wss://nostr.wine",
];
-Relays.forEach(r => System.ConnectToRelay(r, { read: true, write: true }));
+Relays.forEach((r) => System.ConnectToRelay(r, { read: true, write: true }));
const router = createBrowserRouter([
{
element: ,
- loader: async() => {
+ loader: async () => {
await System.Init();
return null;
},
children: [
{
path: "/",
- element:
+ element: ,
},
{
path: "/live/:id",
- element:
- }
- ]
+ element: ,
+ },
+ ],
},
{
path: "/chat/:id",
- element:
- }
-])
-const root = ReactDOM.createRoot(document.getElementById('root') as HTMLDivElement);
+ element: ,
+ },
+]);
+const root = ReactDOM.createRoot(
+ document.getElementById("root") as HTMLDivElement
+);
root.render(
-);
\ No newline at end of file
+);
diff --git a/src/pages/layout.tsx b/src/pages/layout.tsx
index b5c479c..48de82d 100644
--- a/src/pages/layout.tsx
+++ b/src/pages/layout.tsx
@@ -11,59 +11,72 @@ import { NewStream } from "element/new-stream";
import { useState } from "react";
export function LayoutPage() {
- const navigate = useNavigate();
- const login = useLogin();
- const [newStream, setNewStream] = useState(false);
+ const navigate = useNavigate();
+ const login = useLogin();
+ const [newStream, setNewStream] = useState(false);
- async function doLogin() {
- const pub = await EventPublisher.nip7();
- if (pub) {
- Login.loginWithPubkey(pub.pubKey);
- }
+ async function doLogin() {
+ const pub = await EventPublisher.nip7();
+ if (pub) {
+ Login.loginWithPubkey(pub.pubKey);
}
+ }
- function loggedIn() {
- if (!login) return;
+ function loggedIn() {
+ if (!login) return;
- return <>
-
-
- >
- }
+ return (
+ <>
+
+
+ >
+ );
+ }
- function loggedOut() {
- if (login) return;
+ function loggedOut() {
+ if (login) return;
- return <>
-
- Login
-
-
- >
- }
+ return (
+ <>
+
+ Login
+
+
+ >
+ );
+ }
- return <>
-
-
- {newStream && setNewStream(false)} >
- navigate("/")} />
- }
+ return (
+ <>
+
+
+ {newStream && (
+ setNewStream(false)}>
+ navigate("/")} />
+
+ )}
>
-}
\ No newline at end of file
+ );
+}
diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx
index 028d4fa..6355377 100644
--- a/src/pages/stream-page.tsx
+++ b/src/pages/stream-page.tsx
@@ -53,33 +53,42 @@ export function StreamPage() {
{findTag(thisEvent.data, "title")}
{findTag(thisEvent.data, "summary")}
-
- {status}
-
+ {status}
{thisEvent.data?.tags
- .filter(a => a[0] === "t")
- .map(a => a[1])
- .map(a => (
+ .filter((a) => a[0] === "t")
+ .map((a) => a[1])
+ .map((a) => (
{a}
))}
- {isMine &&
-
-
- Delete
-
-
}
+ {isMine && (
+
+
+
+ Delete
+
+
+ )}
-
-
- {zap && zapTarget && thisEvent.data &&
setZap(false)}>
- setZap(false)} />
- }
- {edit && thisEvent.data &&
setEdit(false)}>
- window.location.reload()} />
- }
+ {zap && zapTarget && thisEvent.data && (
+
setZap(false)}>
+ setZap(false)}
+ />
+
+ )}
+ {edit && thisEvent.data && (
+
setEdit(false)}>
+ window.location.reload()}
+ />
+
+ )}
);
}
diff --git a/yarn.lock b/yarn.lock
index 86b54f5..9b10797 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2188,6 +2188,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+"@types/lodash@^4.14.195":
+ version "4.14.195"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632"
+ integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
+
"@types/mime@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
@@ -2318,6 +2323,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/ws@^8.5.5":
version "8.5.5"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb"
@@ -2561,6 +2573,14 @@
resolved "https://registry.yarnpkg.com/@webbtc/webln-types/-/webln-types-1.0.12.tgz#ddb5f0dbaa0a853ef21a4f36a603199d43cc8682"
integrity sha512-uCsJt78RaW/UYDXwAjjs6aj7fiXyozwMknWvPROCaGMx+rXoPddtDjMIMbMFLvUJVQmnyzpqGkx/0jBIvVaVvA==
+"@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"
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -3711,6 +3731,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
+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==
+
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -7550,6 +7575,11 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
+prettier@^2.8.8:
+ version "2.8.8"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
+ integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
+
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -8787,6 +8817,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+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==
+
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"