delete /chat folder

pull/90/head
Aevann 2023-01-20 05:37:39 +02:00
parent d82835a968
commit 43aa2b5c0a
45 changed files with 0 additions and 3216 deletions

View File

@ -1,6 +0,0 @@
FEATURES_ACTIVITY=false
DEBUG=false
NODE_ENV="production"
EMOJI_INPUT_TOKEN=":"
QUICK_EMOJIS_MAX_COUNT = 20
APPROXIMATE_CHARACTER_WIDTH = 8

View File

@ -1,22 +0,0 @@
require('dotenv').config()
const package = require("./package.json");
const path = require("path");
const { build } = require("esbuild");
const options = {
entryPoints: ["./src/index.tsx"],
outfile: path.resolve(__dirname, "../files/assets/js/chat_done.js"),
bundle: true,
minify: process.env.NODE_ENV === "production",
define: {
"process.env.VERSION": `"${package.version}"`,
"process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
"process.env.DEBUG": process.env.DEBUG,
"process.env.FEATURES_ACTIVITY": process.env.FEATURES_ACTIVITY,
"process.env.EMOJI_INPUT_TOKEN": `"${process.env.EMOJI_INPUT_TOKEN}"`,
"process.env.QUICK_EMOJIS_MAX_COUNT": process.env.QUICK_EMOJIS_MAX_COUNT,
"process.env.APPROXIMATE_CHARACTER_WIDTH": process.env.APPROXIMATE_CHARACTER_WIDTH,
},
};
build(options).catch(() => process.exit(1));

25
chat/global.d.ts vendored
View File

@ -1,25 +0,0 @@
declare const process: {
env: Record<string, any>;
};
declare interface IChatMessage {
id: string;
username: string;
user_id?: string;
avatar: string;
hat: string;
namecolor: string;
text: string;
base_text_censored: string;
text_censored: string;
text_html: string;
time: number;
quotes: null | string;
dm: boolean;
}
declare interface EmojiModSelection {
large: boolean;
mirror: boolean;
pat: boolean;
}

View File

@ -1,40 +0,0 @@
{
"name": "chat",
"version": "0.1.27",
"main": "./src/index.tsx",
"license": "MIT",
"dependencies": {
"@types/humanize-duration": "^3.27.1",
"@types/lodash.clonedeep": "^4.5.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.throttle": "^4.1.7",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.7",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"classnames": "^2.3.2",
"dotenv": "^16.0.3",
"esbuild": "^0.15.11",
"humanize-duration": "^3.27.3",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.8",
"run-when-changed": "^2.1.0",
"socket.io-client": "^4.5.3",
"typescript": "^4.8.4",
"weak-key": "^1.0.2"
},
"scripts": {
"chat": "yarn check && yarn build && yarn css:move",
"chat:watch": "run-when-changed --watch \"**/*.*\" --exec \"yarn chat\"",
"check": "tsc",
"build": "node ./build",
"css:move": "mv ../files/assets/js/chat_done.css ../files/assets/css/chat_done.css"
}
}

View File

@ -1,134 +0,0 @@
html,
body {
overscroll-behavior-y: none;
}
html {
height: -webkit-fill-available;
}
body {
min-height: 100vh;
min-height: calc(var(--vh, 1vh) * 100);
overflow: hidden;
/* mobile viewport bug fix */
min-height: -webkit-fill-available;
}
.App {
position: fixed;
width: 100vw;
display: flex;
overflow: hidden;
}
.App-wrapper {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
margin: 0 2rem;
}
.App-heading {
flex-basis: 3rem;
display: flex;
align-items: center;
}
.App-heading small {
opacity: 0.2;
font-size: 10px;
}
.App-side {
height: 100%;
flex: 1;
background: var(--gray-500);
position: relative;
}
.App-content {
position: relative;
flex: 3;
height: 62vh;
height: calc(var(--vh, 1vh) * 72);
max-height: 1000px;
overflow: auto;
-ms-overflow-style: none;
scrollbar-width: none;
display: flex;
flex-direction: column;
}
.App-content::-webkit-scrollbar {
display: none;
}
.App-drawer {
z-index: 2;
display: flex;
background: rgb(var(--background));
height: 100%;
}
.App-center {
display: flex;
align-items: flex-start;
}
.App-bottom-wrapper {
display: flex;
align-items: flex-start;
}
.App-bottom {
flex: 3;
}
.App-bottom-dummy {
flex: 1;
}
.App-bottom-extra {
padding: .25rem;
}
/* On mobile, hide the sidebar and make the input full-width. */
@media screen and (max-width: 1100px) {
.App-wrapper {
margin: 0 auto;
}
.App-heading {
padding: 0 1rem;
}
.App-side {
display: none;
}
.App-bottom-dummy {
display: none;
}
.App-bottom-wrapper {
padding-right: 1rem;
padding-left: 1rem;
}
}
lite-youtube {
min-width: min(80vw, 500px);
}
.btn-secondary {
border: none !important;
}
.btn-secondary:focus {
border: none !important;
box-shadow: none !important;
}

View File

@ -1,158 +0,0 @@
import cx from "classnames";
import throttle from "lodash.throttle";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { DndProvider, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import "./App.css";
import {
ChatHeading,
ChatMessageList,
QuotedMessage,
UserInput,
UserList,
UsersTyping,
} from "./features";
import { ChatProvider, DrawerProvider, useChat, useDrawer } from "./hooks";
const SCROLL_CANCEL_THRESHOLD = 500;
const WINDOW_RESIZE_THROTTLE_WAIT = 250;
export function App() {
return (
<DndProvider backend={HTML5Backend}>
<DrawerProvider>
<ChatProvider>
<AppInner />
</ChatProvider>
</DrawerProvider>
</DndProvider>
);
}
function AppInner() {
const [_, dropRef] = useDrop({
accept: "drawer",
});
const { open, config } = useDrawer();
const contentWrapper = useRef<HTMLDivElement>(null);
const initiallyScrolledDown = useRef(false);
const { messages, quote, userToDm, updateUserToDm } = useChat();
const [focused, setFocused] = useState(false);
const toggleFocus = useCallback(() => {
setTimeout(() => {
setFocused(prev => !prev);
}, 0);
}, []);
// See: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
useEffect(() => {
const updateViewportHeightUnit = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty("--vh", `${vh}px`);
};
const throttledResizeHandler = throttle(
updateViewportHeightUnit,
WINDOW_RESIZE_THROTTLE_WAIT
);
throttledResizeHandler();
window.addEventListener("resize", throttledResizeHandler);
return () => {
window.removeEventListener("resize", throttledResizeHandler);
};
}, []);
useEffect(() => {
if (messages.length > 0) {
if (initiallyScrolledDown.current) {
/* We only want to scroll back down on a new message
if the user is not scrolled up looking at previous messages. */
const scrollableDistance =
contentWrapper.current.scrollHeight -
contentWrapper.current.clientHeight;
const scrolledDistance = contentWrapper.current.scrollTop;
const hasScrolledEnough =
scrollableDistance - scrolledDistance >= SCROLL_CANCEL_THRESHOLD;
if (hasScrolledEnough) {
return;
}
} else {
// Always scroll to the bottom on first load.
initiallyScrolledDown.current = true;
}
contentWrapper.current.scrollTop = contentWrapper.current.scrollHeight;
}
}, [messages]);
useEffect(() => {
if (!open) {
// Scroll to the bottom after any drawer closes.
contentWrapper.current.scrollTop = contentWrapper.current.scrollHeight;
}
}, [open]);
return (
<div className="App" ref={dropRef}>
<div className="App-wrapper">
<div className="App-heading">
<small>v{process.env.VERSION}</small>
<ChatHeading />
</div>
<div className="App-center">
<div
className={cx("App-content")}
ref={contentWrapper}
>
{open ? (
<div className="App-drawer">{config.content}</div>
) : (
<ChatMessageList />
)}
</div>
<div className="App-side">
<UserList />
</div>
</div>
<div className="App-bottom-wrapper">
<div className="App-bottom">
{quote && (
<div className="App-bottom-extra">
<QuotedMessage />
</div>
)}
{userToDm && (
<div
className="App-bottom-extra text-primary"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<em>Directly messaging @{userToDm.username}</em>
<button
type="button"
className="btn btn-secondary"
onClick={() => updateUserToDm(null)}
>
Cancel
</button>
</div>
)}
<UserInput
large={focused}
onFocus={toggleFocus}
onBlur={toggleFocus}
/>
<UsersTyping />
</div>
<div className="App-bottom-dummy" />
</div>
</div>
</div>
);
}

View File

@ -1,5 +0,0 @@
.BaseDrawer {
flex: 1;
padding-right: 2rem;
overflow: hidden;
}

View File

@ -1,10 +0,0 @@
import React, { PropsWithChildren, useEffect } from "react";
import "./BaseDrawer.css";
interface Props extends PropsWithChildren {
onClose?(): void;
}
export function BaseDrawer({ onClose, children }: Props) {
return <div className="BaseDrawer">{children}</div>;
}

View File

@ -1 +0,0 @@
export * from "./BaseDrawer"

View File

@ -1,35 +0,0 @@
.ActivityList {
margin-left: 2rem;
}
.ActivityList h4 {
display: flex;
align-items: center;
justify-content: space-between;
}
.ActivityList h4 hr {
flex: 1;
margin-right: 1rem;
}
.ActivityList-activity {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.ActivityList-activity-icon {
margin-right: 1rem;
}
.ActivityList-activity {
transition: background 0.4s ease-in-out;
padding: 0 1rem;
}
.ActivityList-activity:hover {
cursor: pointer;
background: #ffffff05;
}

View File

@ -1,69 +0,0 @@
import React from "react";
import cx from 'classnames'
import { useDrawer } from "../../hooks";
import "./ActivityList.css";
const ACTIVITIES = [
{
game: "Poker",
description: "Know when to hold 'em.",
icon: "fas fa-cards",
},
{
game: "Roulette",
description: "Table go brrrr.",
icon: "fas fa-circle",
},
{
game: "Slots",
description: "Is today your lucky day?",
icon: "fas fa-dollar-sign",
},
{
game: "Blackjack",
description: "Twenty one ways to change your life.",
icon: "fas fa-cards",
},
{
game: "Racing",
description: "Look at 'em go.",
icon: "fas fa-cards",
},
{
game: "Crossing",
description: "A simple life.",
icon: "fas fa-cards",
},
{
game: "Lottershe",
description: "Can't win if you don't play.",
icon: "fas fa-ticket",
},
];
export function ActivityList() {
const { toggle } = useDrawer();
return (
<div className="ActivityList">
<h4>
<hr />
<span>Activities</span>
</h4>
<section>
{ACTIVITIES.map(({ game, description, icon }) => (
<div key={game} role="button" onClick={toggle}>
<div className="ActivityList-activity">
<div className="ActivityList-activity">
<i className={cx("ActivityList-activity-icon", icon)} />
<h5>{game}<br /><small>{description}</small></h5>
</div>
<small><i className="far fa-user fa-sm" /> 0</small>
</div>
</div>
))}
</section>
</div>
);
}

View File

@ -1,10 +0,0 @@
.ChatHeading {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
.ChatHeading i {
margin-right: 0.5rem;
}

View File

@ -1,42 +0,0 @@
import React, { useCallback } from "react";
import { useChat, useDrawer } from "../../hooks";
import { UserList } from "./UserList";
import "./ChatHeading.css";
export function ChatHeading() {
const { open, hide, reveal } = useDrawer();
const { online } = useChat();
const handleToggleUserListDrawer = useCallback(() => {
if (open) {
hide();
} else {
reveal({
title: "Users in chat",
content: <UserList fluid={true} />,
});
}
}, [open]);
return (
<div className="ChatHeading">
<div />
<div>
{open ? (
<button
className="btn btn-secondary"
onClick={handleToggleUserListDrawer}
>Close</button>
) : (
<>
<i
role="button"
className="far fa-user"
onClick={handleToggleUserListDrawer}
/>
<em>{online.length} users online</em>
</>
)}
</div>
</div>
);
}

View File

@ -1,126 +0,0 @@
.ChatMessage {
position: relative;
padding-right: 1.5rem;
max-height: 300px;
overflow: scroll;
}
.ChatMessage__isDm {
background: var(--gray-800);
border-top: 1px dashed var(--primary);
border-bottom: 1px dashed var(--primary);
}
.ChatMessage__isOptimistic {
opacity: 0.5;
}
.ChatMessage p {
margin: 0;
}
.ChatMessage .btn {
border: none !important;
}
.ChatMessage-top {
display: flex;
align-items: center;
}
.ChatMessage-timestamp {
margin-left: 0.5rem;
opacity: 0.5;
font-size: 10px;
}
.ChatMessage-bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 30px;
overflow: hidden;
}
.ChatMessage-content {
margin-right: 0.5rem;
word-wrap: break-word;
display: inline-block;
}
.ChatMessage-button {
margin: 0 0.5rem;
}
.ChatMessage-button i {
margin-right: 0.5rem;
}
.ChatMessage-button__confirmed {
color: red !important;
}
.ChatMessage-quoted-link {
padding-left: 2rem;
}
.ChatMessage-actions-button {
position: absolute;
top: 0;
right: 0;
cursor: pointer;
z-index: 5;
background: none !important;
border: none !important;
box-shadow: none !important;
display: flex;
align-items: center;
}
.ChatMessage-actions-button button {
background: none !important;
border: none !important;
padding: 0 !important;
}
.ChatMessage-actions-button button i {
position: relative;
top: 3px;
margin-right: 1rem;
}
.ChatMessage-actions {
z-index: 1;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(20, 20, 20, 0.85);
display: flex;
align-items: center;
justify-content: flex-end;
padding: 1rem;
padding-right: 3rem;
animation: fading-in 0.3s ease-in-out forwards;
}
.ChatMessage-actions button {
font-size: 10px;
background: none !important;
}
/* List */
.ChatMessageList {
flex: 1;
}
.ChatMessageList-group {
margin-bottom: 1rem;
padding: 0.3rem;
border-radius: 8px;
}
.ChatMessageList-group:nth-child(even) {
background: rgba(255, 255, 255, 0.025);
}

View File

@ -1,337 +0,0 @@
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import cx from "classnames";
import key from "weak-key";
import humanizeDuration from "humanize-duration";
import cloneDeep from "lodash.clonedeep";
import { Username } from "./Username";
import {
DIRECT_MESSAGE_ID,
OPTIMISTIC_MESSAGE_ID,
useChat,
useRootContext,
} from "../../hooks";
import { QuotedMessageLink } from "./QuotedMessageLink";
import "./ChatMessage.css";
interface ChatMessageProps {
message: IChatMessage;
timestampUpdates: number;
showUser?: boolean;
actionsOpen: boolean;
onToggleActions(messageId: string): void;
}
const TIMESTAMP_UPDATE_INTERVAL = 20000;
export function ChatMessage({
message,
showUser = true,
timestampUpdates,
actionsOpen,
onToggleActions,
}: ChatMessageProps) {
const {
id,
user_id,
avatar,
namecolor,
username,
hat,
text,
text_html,
text_censored,
time,
quotes,
dm,
} = message;
const {
id: userId,
username: userUsername,
admin,
censored,
themeColor,
} = useRootContext();
const {
messageLookup,
userToDm,
quote,
deleteMessage,
quoteMessage,
updateUserToDm,
} = useChat();
const [confirmedDelete, setConfirmedDelete] = useState(false);
const quotedMessage = messageLookup[quotes];
const content = censored ? text_censored : text_html;
const isMention =
quotedMessage?.username === userUsername ||
(text_html.includes(`/id/${userId}`) &&
userUsername &&
username !== userUsername);
const isDirect = id === DIRECT_MESSAGE_ID;
const isOptimistic = id === OPTIMISTIC_MESSAGE_ID;
const timestamp = useMemo(
() => formatTimeAgo(time),
[time, timestampUpdates]
);
const handleDeleteMessage = useCallback(() => {
if (confirmedDelete) {
deleteMessage(text);
} else {
setConfirmedDelete(true);
}
}, [text, confirmedDelete]);
const handleQuoteMessageAction = useCallback(() => {
updateUserToDm(null);
quoteMessage(message);
onToggleActions(message.id);
}, [message, onToggleActions]);
const handleDirectMessage = useCallback(
(toggle?: boolean) => {
const userId = message.user_id ?? "";
if (userToDm && userToDm.id === userId) {
updateUserToDm(null);
} else if (userId) {
updateUserToDm({
id: userId,
username: message.username,
});
quoteMessage(null);
if (toggle) {
onToggleActions(message.id);
}
}
},
[userToDm, message.id, message.user_id, message.username]
);
useEffect(() => {
if (!actionsOpen) {
setConfirmedDelete(false);
}
}, [actionsOpen]);
return (
<div
className={cx("ChatMessage", {
ChatMessage__isDm: dm,
ChatMessage__isOptimistic: isOptimistic,
})}
id={id}
style={
isMention && !dm
? {
background: `#${themeColor}25`,
borderLeft: `1px solid #${themeColor}`,
}
: {}
}
>
{!isDirect && !isOptimistic && !actionsOpen && (
<div className="ChatMessage-actions-button">
<button
className="btn btn-secondary"
onClick={() => {
updateUserToDm(null);
quoteMessage(quote ? null : message);
}}
>
<i className="fas fa-reply" />
</button>
<button
className="btn btn-secondary"
onClick={() => onToggleActions(id)}
>
...
</button>
</div>
)}
{!isDirect && !isOptimistic && actionsOpen && (
<div className="ChatMessage-actions">
{userId && parseInt(userId) !== parseInt(user_id) && (
<button
className="btn btn-secondary ChatMessage-button"
onClick={() => handleDirectMessage(true)}
>
📨 DM @{message.username}
</button>
)}
<button
className="btn btn-secondary ChatMessage-button"
onClick={handleQuoteMessageAction}
>
<i className="fas fa-reply" /> Reply
</button>
{admin && (
<button
className={cx("btn btn-secondary ChatMessage-button", {
"ChatMessage-button__confirmed": confirmedDelete,
})}
onClick={handleDeleteMessage}
>
<i className="fas fa-trash-alt" />{" "}
{confirmedDelete ? "Are you sure?" : "Delete"}
</button>
)}
<button
className="btn btn-secondary ChatMessage-button"
onClick={() => onToggleActions(id)}
>
<i>X</i> Close
</button>
</div>
)}
{showUser && (
<div className="ChatMessage-top">
<Username
avatar={avatar}
name={username}
color={namecolor}
hat={hat}
/>
<div className="ChatMessage-timestamp">{timestamp}</div>
</div>
)}
{quotes && quotedMessage && (
<div className="ChatMessage-quoted-link">
<QuotedMessageLink message={quotedMessage} />
</div>
)}
{!isDirect && dm && (
<small className="ChatMessage-quoted-link text-primary">
<em>(Sent only to you)</em>
</small>
)}
<div className="ChatMessage-bottom">
<div>
<span
className="ChatMessage-content"
title={content}
dangerouslySetInnerHTML={{
__html: content,
}}
/>
</div>
</div>
</div>
);
}
export function ChatMessageList() {
const listRef = useRef<HTMLDivElement>(null);
const { messages } = useChat();
const [timestampUpdates, setTimestampUpdates] = useState(0);
const groupedMessages = useMemo(() => groupMessages(messages), [messages]);
const [actionsOpenForMessage, setActionsOpenForMessage] = useState<
string | null
>(null);
const handleToggleActionsForMessage = useCallback(
(messageId: string) =>
setActionsOpenForMessage(
messageId === actionsOpenForMessage ? null : messageId
),
[actionsOpenForMessage]
);
useEffect(() => {
const updatingTimestamps = setInterval(
() => setTimestampUpdates((prev) => prev + 1),
TIMESTAMP_UPDATE_INTERVAL
);
return () => {
clearInterval(updatingTimestamps);
};
}, []);
useLayoutEffect(() => {
const images = Array.from(
listRef.current.getElementsByTagName("img")
).filter((image) => image.dataset.src);
for (const image of images) {
image.src = image.dataset.src;
}
}, [messages]);
return (
<div className="ChatMessageList" ref={listRef}>
{groupedMessages.map((group) => (
<div key={key(group)} className="ChatMessageList-group">
{group.map((message, index) => (
<ChatMessage
key={key(message)}
message={message}
timestampUpdates={timestampUpdates}
showUser={index === 0}
actionsOpen={actionsOpenForMessage === message.id}
onToggleActions={handleToggleActionsForMessage}
/>
))}
</div>
))}
</div>
);
}
function formatTimeAgo(time: number) {
const shortEnglishHumanizer = humanizeDuration.humanizer({
language: "shortEn",
languages: {
shortEn: {
y: () => "y",
mo: () => "mo",
w: () => "w",
d: () => "d",
h: () => "h",
m: () => "m",
s: () => "s",
ms: () => "ms",
},
},
round: true,
units: ["h", "m", "s"],
largest: 2,
spacer: "",
delimiter: ", ",
});
const now = new Date().getTime();
const humanized = `${shortEnglishHumanizer(time * 1000 - now)} ago`;
return humanized === "0s ago" ? "just now" : humanized;
}
function groupMessages(messages: IChatMessage[]) {
const grouped: IChatMessage[][] = [];
let lastUsername = "";
let temp: IChatMessage[] = [];
for (const message of messages) {
if (!lastUsername) {
lastUsername = message.username;
}
if (message.username === lastUsername) {
temp.push(message);
} else {
grouped.push(cloneDeep(temp));
lastUsername = message.username;
temp = [message];
}
}
if (temp.length > 0) {
grouped.push(cloneDeep(temp));
}
return grouped;
}

View File

@ -1,15 +0,0 @@
.QuotedMessage {
display: flex;
align-items: center;
justify-content: space-between;
}
.QuotedMessage-content {
margin-left: 1rem;
flex: 1;
max-width: 420px;
max-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 1rem;
}

View File

@ -1,21 +0,0 @@
import React from "react";
import { useChat } from "../../hooks";
import "./QuotedMessage.css";
import { QuotedMessageLink } from "./QuotedMessageLink";
export function QuotedMessage() {
const { quote, quoteMessage } = useChat();
return (
<div className="QuotedMessage">
<QuotedMessageLink message={quote} />
<button
type="button"
className="btn btn-secondary"
onClick={() => quoteMessage(null)}
>
Cancel
</button>
</div>
);
}

View File

@ -1,3 +0,0 @@
.QuotedMessageLink {
font-size: 10px;
}

View File

@ -1,49 +0,0 @@
import React, { useCallback, useMemo } from "react";
import { useRootContext } from "../../hooks";
import "./QuotedMessageLink.css";
const SCROLL_TO_QUOTED_OVERFLOW = 250;
const QUOTED_MESSAGE_CONTEXTUAL_HIGHLIGHTING_DURATION = 2500;
const QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH = 30;
export function QuotedMessageLink({ message }: { message: IChatMessage }) {
const { censored, themeColor } = useRootContext();
const handleLinkClick = useCallback(() => {
const element = document.getElementById(message.id);
if (element) {
element.scrollIntoView();
element.style.background = `#${themeColor}33`;
setTimeout(() => {
element.style.background = "unset";
}, QUOTED_MESSAGE_CONTEXTUAL_HIGHLIGHTING_DURATION);
const [appContent] = Array.from(
document.getElementsByClassName("App-content")
);
if (appContent) {
appContent.scrollTop -= SCROLL_TO_QUOTED_OVERFLOW;
}
}
}, []);
const replyText = useMemo(() => {
const textToUse = censored ? message.base_text_censored || message.text : message.text;
const slicedText = textToUse.slice(
0,
QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH
);
return textToUse.length >= QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH
? `${slicedText}...`
: slicedText;
}, [message, censored]);
return (
<a className="QuotedMessageLink" href="#" onClick={handleLinkClick}>
<i className="fas fa-reply" /> @{message.username}:{" "}
<em>"{replyText}"</em>
</a>
);
}

View File

@ -1,28 +0,0 @@
.UserInput {
position: relative;
display: flex;
align-items: center;
}
.UserInput-input {
flex: 1;
margin-right: 2rem;
min-height: 50px;
height: 50px;
max-height: 50px;
}
@media (min-width: 768px) {
.UserInput-input {
min-height: 100px !important;
height: 100px !important;
max-height: 100px !important;
}
}
.UserInput-emoji {
cursor: pointer;
font-size: 20px;
transform: rotateY(180deg);
margin-right: 1rem;
}

View File

@ -1,187 +0,0 @@
import React, {
ChangeEvent,
KeyboardEvent,
FormEvent,
useCallback,
useRef,
useMemo,
useState,
useEffect,
} from "react";
import cx from "classnames";
import { useChat, useEmojis } from "../../hooks";
import { QuickEmojis } from "../emoji";
import "./UserInput.css";
interface Props {
large?: boolean;
onFocus(): void;
onBlur(): void;
}
export function UserInput({ large = false, onFocus, onBlur }: Props) {
const { draft, userToDm, sendMessage, updateDraft } = useChat();
const builtChatInput = useRef<HTMLTextAreaElement>(null);
const { visible, addQuery } = useEmojis();
const form = useRef<HTMLFormElement>(null);
const [typingOffset, setTypingOffset] = useState(0);
const quickEmojis = useMemo(
() => visible.slice(0, process.env.QUICK_EMOJIS_MAX_COUNT),
[visible]
);
const handleChange = useCallback(
(event: ChangeEvent<HTMLTextAreaElement>) => {
const input = event.target.value;
const [openEmojiToken, closeEmojiToken] = locateEmojiTokens(input);
const emojiSegment = input.slice(openEmojiToken + 1, closeEmojiToken + 1);
updateDraft(input);
addQuery(
openEmojiToken === -1 || emojiSegment.includes(" ") ? "" : emojiSegment
);
setTypingOffset(
emojiSegment.length * process.env.APPROXIMATE_CHARACTER_WIDTH
);
},
[]
);
const handleSendMessage = useCallback(
(event?: FormEvent<HTMLFormElement>) => {
event?.preventDefault();
sendMessage();
},
[sendMessage]
);
const handleKeyUp = useCallback(
(event: KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter" && !event.shiftKey) {
handleSendMessage();
}
},
[handleSendMessage]
);
const handleInsertQuickEmoji = useCallback(
(emoji: string) => {
const [openEmojiToken, closeEmojiToken] = locateEmojiTokens(draft);
const toReplace = draft.slice(openEmojiToken, closeEmojiToken + 1);
updateDraft((prev) => prev.replace(toReplace, `:${emoji}: `));
addQuery("");
builtChatInput.current?.focus();
},
[draft]
);
const handleFocus = useCallback(() => {
builtChatInput.current?.scrollIntoView({ behavior: "smooth" });
onFocus();
}, [onFocus]);
// Listen for changes from the Emoji Modal and reflect them in draft
useEffect(() => {
const handleEmojiInsert = (event: CustomEvent<{ emoji: string }>) =>
updateDraft((prev) => `${prev} ${event.detail.emoji} `);
document.addEventListener("emojiInserted", handleEmojiInsert);
return () => {
document.removeEventListener("emojiInserted", handleEmojiInsert);
}
}, []);
useEffect(() => {
if (userToDm) {
builtChatInput.current?.focus();
}
}, [userToDm])
return (
<form ref={form} className="UserInput" onSubmit={handleSendMessage}>
{quickEmojis.length > 0 && (
<div
style={{
position: "absolute",
top: -254,
height: 250,
left: typingOffset,
display: "flex",
flexDirection: "column-reverse",
}}
>
<QuickEmojis
emojis={quickEmojis}
onSelectEmoji={handleInsertQuickEmoji}
/>
</div>
)}
<textarea
ref={builtChatInput}
id="builtChatInput"
className={cx("UserInput-input form-control")}
minLength={1}
maxLength={1000}
rows={1}
onChange={handleChange}
onKeyUp={handleKeyUp}
onFocus={handleFocus}
onBlur={onBlur}
placeholder="Message"
autoComplete="off"
value={draft}
/>
<i
role="button"
data-bs-toggle="modal"
data-bs-target="#emojiModal"
data-bs-placement="bottom"
title="Add Emoji"
onClick={() => {
const whatever = window as any;
whatever.loadEmojis("builtChatInput");
}}
className="UserInput-emoji fas fa-smile-beam"
/>
<button
className="btn btn-secondary"
disabled={draft.length === 0}
onClick={sendMessage}
>
<i className="UserInput-emoji fas fa-reply" />
</button>
</form>
);
}
function locateEmojiTokens(text: string) {
let openEmojiInputToken = -1;
let endEmojiInputToken = -1;
if (text.length <= 1) {
return [openEmojiInputToken, endEmojiInputToken];
}
for (let i = 0; i < text.length; i++) {
const character = text[i];
if (character === process.env.EMOJI_INPUT_TOKEN) {
if (openEmojiInputToken === -1) {
openEmojiInputToken = i;
} else {
openEmojiInputToken = -1;
}
}
}
if (openEmojiInputToken !== -1) {
endEmojiInputToken = openEmojiInputToken;
while (
endEmojiInputToken < text.length - 1 &&
text[endEmojiInputToken] !== " "
) {
endEmojiInputToken++;
}
}
return [openEmojiInputToken, endEmojiInputToken];
}

View File

@ -1,24 +0,0 @@
.UserList {
padding: 1rem;
flex: 1;
}
.UserList-heading {
display: flex;
align-items: center;
justify-content: space-between;
}
.UserList-heading h5 {
margin-right: 2rem;
}
.UserList ul {
max-height: calc(var(--vh, 1vh) * 50);
overflow: auto;
}
.UserList ul::-webkit-scrollbar {
display: none;
}

View File

@ -1,30 +0,0 @@
import React from "react";
import cx from "classnames";
import { useChat } from "../../hooks";
import "./UserList.css";
interface Props {
fluid?: boolean;
}
export function UserList({ fluid = false }: Props) {
const { online } = useChat();
return (
<div className="UserList">
<div className="UserList-heading">
<h5>Users Online</h5>
<div className="Chat-online">
<i className="far fa-user fa-sm" /> {online.length}
</div>
</div>
<ul className={cx({ fluid })}>
{online.map((user) => (
<li key={user}>
<a href={`/@${user}`}>@{user}</a>
</li>
))}
</ul>
</div>
);
}

View File

@ -1,9 +0,0 @@
.Username {
display: inline-flex;
align-items: center;
}
.Username > a {
font-weight: bold;
margin-left: 8px;
}

View File

@ -1,35 +0,0 @@
import React from "react";
import "./Username.css";
interface UsernameProps {
avatar: string;
color: string;
name: string;
hat?: string;
}
export function Username({ avatar, color, name, hat = "" }: UsernameProps) {
return (
<div className="Username">
<div className="profile-pic-20-wrapper">
<img alt={name} src={avatar} className="pp20" />
{hat && (
<img
className="avatar-hat profile-pic-20-hat hat"
loading="lazy"
src={hat}
/>
)}
</div>
<a
className="userlink"
style={{ color: `#${color}` }}
target="_blank"
href={`/@${name}`}
rel="nofollow noopener"
>
{name}
</a>
</div>
);
}

View File

@ -1,5 +0,0 @@
.UsersTyping {
height: 18px;
display: inline-block;
font-size: 10px;
}

View File

@ -1,32 +0,0 @@
import React from "react";
import { useChat } from "../../hooks";
import "./UsersTyping.css";
export function UsersTyping() {
const { typing } = useChat();
const [first, second, third, ...rest] = typing;
return (
<div className="UsersTyping">
{(() => {
if (rest.length > 0) {
return `${first}, ${second}, ${third} and ${rest.length} more are typing...`;
}
if (first && second && third) {
return `${first}, ${second} and ${third} are typing...`;
}
if (first && second) {
return `${first} and ${second} are typing...`;
}
if (first) {
return `${first} is typing...`;
}
return null;
})()}
</div>
);
}

View File

@ -1,8 +0,0 @@
export * from "./ChatHeading";
export * from "./ChatMessage";
export * from "./UserInput";
export * from "./UserList";
export * from "./Username";
export * from "./UsersTyping";
export * from "./QuotedMessage";
export * from "./QuotedMessageLink";

View File

@ -1,5 +0,0 @@
.EmojiDrawer-options {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@ -1,222 +0,0 @@
import React, {
useRef,
useMemo,
useCallback,
useEffect,
useState,
} from "react";
import { FixedSizeGrid, FixedSizeGrid as Grid } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import { useDrawer, useEmojis } from "../../hooks";
import { BaseDrawer } from "../../drawers";
import { EmojiGenres } from "./EmojiGenres";
import { EmojiMods } from "./EmojiMods";
import "./EmojiDrawer.css";
interface Props {
onSelectEmoji(emoji: string): void;
onClose?(): void;
}
enum EmojiDrawerTabs {
Favorites = "Favorites",
}
export function EmojiDrawer({ onSelectEmoji, onClose }: Props) {
const {
visible,
genres,
collections,
favorites,
mostRecentQuery,
addQuery,
updateVisible,
updateFavorites,
} = useEmojis();
const { hide } = useDrawer();
const [modSelection, setModSelection] = useState<EmojiModSelection>({
large: false,
mirror: false,
pat: false,
});
const [activeGenre, setActiveGenre] = useState<string>(
EmojiDrawerTabs.Favorites
);
const emojiGrid = useMemo(() => {
const grid = [];
let temp = [];
for (let i = 0; i < visible.length; i++) {
temp.push(visible[i]);
if (i % 7 === 0) {
grid.push([...temp]);
temp = [];
}
}
return grid;
}, [visible]);
// Refs
const gridRef = useRef<FixedSizeGrid<any>>();
const input = useRef<HTMLInputElement>(null);
// Callbacks
const handleEmojiGenreChange =
/*useCallback(*/
(genre: string) => {
setActiveGenre(genre);
if (genre === EmojiDrawerTabs.Favorites) {
updateVisible(favorites);
} else {
updateVisible(collections[genre] ?? []);
}
};
/*[favorites, collections]
);*/
const handleScrollToTop = useCallback(() => {
gridRef.current?.scrollToItem({
rowIndex: 0,
});
}, []);
const handleSelectEmoji = useCallback(
(emoji: string) => {
if (modSelection.large) {
emoji = `#${emoji}`;
}
if (modSelection.mirror) {
emoji = `!${emoji}`;
}
if (modSelection.pat) {
emoji = `${emoji}pat`;
}
onSelectEmoji(emoji);
updateFavorites((prev) => Array.from(new Set(prev.concat(emoji))));
},
[modSelection]
);
const handleClose = useCallback(() => {
hide();
onClose();
}, [onClose]);
// Effects
// When the user types more, scroll back up.
useEffect(() => {
handleScrollToTop();
}, [visible]);
// Enter and Esc finish the interaction.
useEffect(() => {
const handleKeyup = (event: KeyboardEvent) => {
if (["Escape", "Enter"].includes(event.key)) {
handleClose();
}
};
document.addEventListener("keyup", handleKeyup);
return () => {
document.removeEventListener("keyup", handleKeyup);
};
}, [handleClose]);
// Cell
const Cell = useCallback(
({ columnIndex, rowIndex, style }) => {
const emoji = emojiGrid[rowIndex]?.[columnIndex];
return emoji ? (
<img
role="button"
onClick={() => handleSelectEmoji(emoji)}
style={{
...style,
cursor: "pointer",
margin: "1rem",
}}
loading="lazy"
width={60}
src={`/e/${emoji}.webp`}
alt={emoji}
/>
) : null;
},
[emojiGrid, handleSelectEmoji]
);
return (
<BaseDrawer onClose={handleClose}>
<div className="EmojiDrawer-input">
<input
ref={input}
className="form-control"
type="text"
onChange={(e) => addQuery(e.target.value)}
autoFocus={true}
placeholder="Search for emojis..."
style={{
margin: "1rem",
flex: 1,
}}
/>
</div>
<div className="EmojiDrawer-options">
<EmojiMods
selection={modSelection}
onChangeSelection={setModSelection}
/>
<EmojiGenres
genres={genres}
activeGenre={activeGenre}
onGenreChange={handleEmojiGenreChange}
/>
</div>
{/* Results */}
{visible.length > 0 && (
<AutoSizer>
{({ width, height }) => (
<Grid
ref={gridRef}
columnCount={8}
columnWidth={64}
rowCount={visible.length}
rowHeight={64}
width={width}
height={height}
>
{Cell}
</Grid>
)}
</AutoSizer>
)}
{/* Searched, nothing found */}
{visible.length === 0 && mostRecentQuery !== "" && (
<div>Nothing found.</div>
)}
{/* Favorites */}
{visible.length === 0 &&
mostRecentQuery === "" &&
favorites.map((favorite) => (
<img
key={favorite}
role="button"
onClick={() => handleSelectEmoji(favorite)}
style={{
cursor: "pointer",
margin: "1rem",
}}
loading="lazy"
width={60}
src={`/e/${favorite}.webp`}
alt={favorite}
/>
))}
</BaseDrawer>
);
}

View File

@ -1,25 +0,0 @@
import React, { ChangeEvent, useCallback } from "react";
import "./EmojiGenres.css";
interface Props {
genres: string[];
activeGenre: string;
onGenreChange(genre: string): void;
}
export function EmojiGenres({ genres, activeGenre, onGenreChange }: Props) {
const handleSelect = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
onGenreChange(event.target.value);
}, [onGenreChange]);
return (
<select onChange={handleSelect} value={activeGenre}>
<option value="Favorites"> Favorites </option>
{genres.map((genre) => (
<option key={genre} value={genre}>
{genre}
</option>
))}
</select>
);
}

View File

@ -1,55 +0,0 @@
import React, { useCallback, useRef } from "react";
interface Props {
selection: EmojiModSelection;
onChangeSelection(selection: EmojiModSelection): void;
}
export function EmojiMods({ selection, onChangeSelection }: Props) {
const largeOption = useRef<HTMLInputElement>(null);
const mirrorOption = useRef<HTMLInputElement>(null);
const patOption = useRef<HTMLInputElement>(null);
const handleChangeSelection = useCallback(() => {
onChangeSelection({
large: largeOption.current.checked,
mirror: mirrorOption.current.checked,
pat: patOption.current.checked,
});
}, []);
return (
<div className="EmojiMods">
<input
type="checkbox"
id="largeOption"
ref={largeOption}
onChange={handleChangeSelection}
checked={selection.large}
style={{ marginLeft: "1rem", marginRight: "0.5rem" }}
></input>
<label htmlFor="largeOption" style={{ marginRight: "1rem" }}>
Large
</label>
<input
type="checkbox"
id="mirrorOption"
ref={mirrorOption}
onChange={handleChangeSelection}
checked={selection.mirror}
style={{ marginRight: "0.5rem" }}
></input>
<label htmlFor="mirrorOption" style={{ marginRight: "1rem" }}>
Mirror
</label>
<input
type="checkbox"
id="patOption"
ref={patOption}
onChange={handleChangeSelection}
checked={selection.pat}
style={{ marginRight: "0.5rem" }}
></input>
<label htmlFor="patOption">Pat</label>
</div>
);
}

View File

@ -1,49 +0,0 @@
import React from "react";
interface Props {
emojis: string[];
onSelectEmoji(emoji: string): void;
}
export function QuickEmojis({ emojis, onSelectEmoji }: Props) {
return (
<div
style={{
backgroundColor: "var(--gray-700)",
maxHeight: 250,
overflowY: "auto",
overflowX: "hidden",
borderRadius: "4px",
border: "1px solid rgba(255, 255, 255, 0.3)",
boxShadow: "0px 2px 5px rgb(0 0 0 / 20%)",
zIndex: 999,
}}
>
{emojis.map((emoji) => (
<div
key={emoji}
role="button"
onClick={() => onSelectEmoji(emoji)}
style={{
borderBottom: "1px solid #606060",
padding: "4px",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<img
src={`/e/${emoji}.webp`}
style={{
objectFit: "contain",
width: 30,
height: 30,
}}
/>
<span>{emoji}</span>
</div>
))}
</div>
);
}

View File

@ -1,4 +0,0 @@
export * from "./EmojiDrawer";
export * from "./EmojiGenres";
export * from "./EmojiMods";
export * from "./QuickEmojis";

View File

@ -1,2 +0,0 @@
export * from "./chat";
export * from "./emoji";

View File

@ -1,5 +0,0 @@
export * from "./useChat";
export * from "./useDrawer";
export * from "./useEmojis";
export * from "./useRootContext";
export * from "./useWindowFocus";

View File

@ -1,261 +0,0 @@
import React, {
createContext,
PropsWithChildren,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { io, Socket } from "socket.io-client";
import debounce from "lodash.debounce";
import { useRootContext } from "./useRootContext";
import { useWindowFocus } from "./useWindowFocus";
enum ChatHandlers {
CONNECT = "connect",
CATCHUP = "catchup",
ONLINE = "online",
TYPING = "typing",
DELETE = "delete",
SPEAK = "speak",
}
interface UserToDM {
id: string;
username: string;
}
interface ChatProviderContext {
online: string[];
typing: string[];
messages: IChatMessage[];
draft: string;
quote: null | IChatMessage;
messageLookup: Record<string, IChatMessage>;
userToDm: null | UserToDM;
updateDraft: React.Dispatch<React.SetStateAction<string>>;
sendMessage(): void;
quoteMessage(message: null | IChatMessage): void;
deleteMessage(withText: string): void;
updateUserToDm(userToDm: UserToDM): void;
}
const ChatContext = createContext<ChatProviderContext>({
online: [],
typing: [],
messages: [],
draft: "",
quote: null,
messageLookup: {},
userToDm: null,
updateDraft() {},
sendMessage() {},
quoteMessage() {},
deleteMessage() {},
updateUserToDm() {},
});
const MINIMUM_TYPING_UPDATE_INTERVAL = 250;
export const DIRECT_MESSAGE_ID = "DIRECT_MESSAGE";
export const OPTIMISTIC_MESSAGE_ID = "OPTIMISTIC";
export function ChatProvider({ children }: PropsWithChildren) {
const { username, id, siteName, hat, avatar, nameColor } = useRootContext();
const socket = useRef<null | Socket>(null);
const [online, setOnline] = useState<string[]>([]);
const [typing, setTyping] = useState<string[]>([]);
const [messages, setMessages] = useState<IChatMessage[]>([]);
const [draft, setDraft] = useState("");
const lastDraft = useRef("");
const [quote, setQuote] = useState<null | IChatMessage>(null);
const focused = useWindowFocus();
const [userToDm, setUserToDm] = useState<null | UserToDM>(null);
const [notifications, setNotifications] = useState<number>(0);
const [messageLookup, setMessageLookup] = useState({});
const addMessage = useCallback((message: IChatMessage) => {
if (message.id === OPTIMISTIC_MESSAGE_ID) {
setMessages((prev) => prev.concat(message));
} else {
// Are there any optimistic messages that have the same text?
setMessages((prev) => {
const matchingOptimisticMessage = prev.findIndex(
(prevMessage) =>
prevMessage.id === OPTIMISTIC_MESSAGE_ID &&
prevMessage.text.trim() === message.text.trim()
);
if (matchingOptimisticMessage === -1) {
return prev.slice(-99).concat(message);
} else {
const before = prev.slice(0, matchingOptimisticMessage);
const after = prev.slice(matchingOptimisticMessage + 1);
return [...before, message, ...after];
}
});
}
if (message.username !== username && !document.hasFocus()) {
setNotifications((prev) => prev + 1);
}
}, []);
const sendMessage = useCallback(() => {
if (userToDm) {
const directMessage = `<small class="text-primary"><em>(Sent to @${userToDm.username}):</em></small> ${draft}`;
addMessage({
id: DIRECT_MESSAGE_ID,
username,
user_id: id,
avatar,
hat,
namecolor: nameColor,
text: directMessage,
base_text_censored: directMessage,
text_censored: directMessage,
text_html: directMessage,
time: new Date().getTime() / 1000,
quotes: null,
dm: true,
});
} else {
addMessage({
id: OPTIMISTIC_MESSAGE_ID,
username,
user_id: id,
avatar,
hat,
namecolor: nameColor,
text: draft,
base_text_censored: draft,
text_censored: draft,
text_html: draft,
time: new Date().getTime() / 1000,
quotes: null,
dm: false,
});
}
socket.current?.emit(ChatHandlers.SPEAK, {
message: draft,
quotes: quote?.id ?? null,
recipient: userToDm?.id ?? "",
});
setQuote(null);
setDraft("");
setUserToDm(null);
}, [draft, quote, userToDm]);
const requestDeleteMessage = useCallback((withText: string) => {
socket.current?.emit(ChatHandlers.DELETE, withText);
}, []);
const deleteMessage = useCallback((withText: string) => {
setMessages((prev) =>
prev.filter((prevMessage) => prevMessage.text !== withText)
);
if (quote?.text === withText) {
setQuote(null);
}
}, []);
const quoteMessage = useCallback((message: IChatMessage) => {
setQuote(message);
try {
document.getElementById("builtChatInput").focus();
} catch (error) {}
}, []);
const context = useMemo<ChatProviderContext>(
() => ({
online,
typing,
messages,
draft,
quote,
messageLookup,
userToDm,
quoteMessage,
sendMessage,
deleteMessage: requestDeleteMessage,
updateDraft: setDraft,
updateUserToDm: setUserToDm,
}),
[
online,
typing,
messages,
draft,
quote,
messageLookup,
userToDm,
sendMessage,
deleteMessage,
quoteMessage,
]
);
useEffect(() => {
if (!socket.current) {
socket.current = io();
socket.current
.on(ChatHandlers.CATCHUP, setMessages)
.on(ChatHandlers.ONLINE, setOnline)
.on(ChatHandlers.TYPING, setTyping)
.on(ChatHandlers.SPEAK, addMessage)
.on(ChatHandlers.DELETE, deleteMessage);
}
});
const debouncedTypingUpdater = useMemo(
() =>
debounce(
() => socket.current?.emit(ChatHandlers.TYPING, lastDraft.current),
MINIMUM_TYPING_UPDATE_INTERVAL
),
[]
);
useEffect(() => {
lastDraft.current = draft;
debouncedTypingUpdater();
}, [draft]);
useEffect(() => {
if (focused || document.hasFocus()) {
setNotifications(0);
}
}, [focused]);
useEffect(() => {
setMessageLookup(
messages.reduce((prev, next) => {
prev[next.id] = next;
return prev;
}, {} as Record<string, IChatMessage>)
);
}, [messages]);
// Display e.g. [+2] Chat when notifications occur when you're away.
useEffect(() => {
const title = document.getElementsByTagName("title")[0];
const favicon = document.getElementById("favicon") as HTMLLinkElement;
const escape = (window as any).escapeHTML;
const alertedWhileAway = notifications > 0 && !focused;
const pathIcon = alertedWhileAway ? "alert" : "icon";
favicon.href = escape(`/assets/images/${siteName}/${pathIcon}.webp?v=3`);
title.innerHTML = alertedWhileAway ? `[+${notifications}] Chat` : "Chat";
}, [notifications, focused]);
return (
<ChatContext.Provider value={context}>{children}</ChatContext.Provider>
);
}
export function useChat() {
const value = useContext(ChatContext);
return value;
}

View File

@ -1,197 +0,0 @@
import React, {
ReactNode,
createContext,
PropsWithChildren,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useDrag } from "react-dnd";
interface DrawerConfig {
title?: string;
content: ReactNode;
actions?: Array<{ title: string; onClick(): void }>;
}
interface DrawerContextType {
config: DrawerConfig;
open: boolean;
coordinates: [number, number];
reveal(config: DrawerConfig): void;
hide(): void;
show(): void;
toggle(): void;
updateCoordinates(to: [number, number]): void;
}
const DRAWER_COORDINATES_STORAGE_KEY = "Drawer/Coordinates";
const DEFAULT_DRAWER_CONFIG = {
title: "",
content: null,
actions: [],
};
const DrawerContext = createContext<DrawerContextType>({
config: DEFAULT_DRAWER_CONFIG,
open: false,
coordinates: [0, 0] as [number, number],
reveal() {},
hide() {},
show() {},
toggle() {},
updateCoordinates() {},
});
export function useDrawer() {
const values = useContext(DrawerContext);
return values;
}
export function DrawerProvider({ children }: PropsWithChildren) {
const [open, setOpen] = useState(false);
const [config, setConfig] = useState<DrawerConfig>(DEFAULT_DRAWER_CONFIG);
const [coordinates, setCoordinates] = useState([0, 0] as [number, number]);
const reveal = useCallback((config: DrawerConfig) => {
setConfig(config);
show();
}, []);
const hide = useCallback(() => {
setOpen(false);
}, []);
const show = useCallback(() => {
setOpen(true);
}, []);
const toggle = useCallback(() => {
setOpen((prev) => !prev);
}, []);
const context = useMemo(
() => ({
config,
open,
coordinates,
reveal,
hide,
show,
toggle,
updateCoordinates: setCoordinates,
}),
[config, open, coordinates, reveal, hide, show, toggle]
);
// Load coordinates.
useEffect(() => {
const persisted = window.localStorage.getItem(
DRAWER_COORDINATES_STORAGE_KEY
);
if (persisted) {
setCoordinates(JSON.parse(persisted));
}
}, []);
// Persist coordinates.
useEffect(() => {
window.localStorage.setItem(
DRAWER_COORDINATES_STORAGE_KEY,
JSON.stringify(coordinates)
);
}, [coordinates]);
return (
<DrawerContext.Provider value={context}>{children}</DrawerContext.Provider>
);
}
export function Drawer() {
const {
config: { title = "", content, actions = [] },
open,
coordinates,
updateCoordinates,
hide,
} = useDrawer();
const [x, y] = coordinates;
const [{ isDragging }, dragRef] = useDrag({
type: "drawer",
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const lastMousePosition = useRef([0, 0]);
useEffect(() => {
const handleMouseMove = (event: MouseEvent) =>
(lastMousePosition.current = [event.clientX, event.clientY]);
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
useEffect(() => {
if (open) {
const chatWrapper = document.getElementById("chatWrapper");
const chatWrapperBox = chatWrapper.getBoundingClientRect();
updateCoordinates([chatWrapperBox.left, chatWrapperBox.top]);
}
}, [open]);
useEffect(() => {
if (isDragging) {
const onRelease = (event: DragEvent) => {
const drawer = document.getElementById("drawer");
const drawerBox = drawer.getBoundingClientRect();
const [mouseX, mouseY] = lastMousePosition.current;
const xDiff = mouseX - drawerBox.left;
const yDiff = mouseY - drawerBox.top;
updateCoordinates([event.clientX - xDiff, event.clientY - yDiff]);
};
document.addEventListener("drop", onRelease);
return () => {
document.removeEventListener("drop", onRelease);
};
}
}, [isDragging]);
return (
<div
id="drawer"
className="App-drawer"
ref={dragRef}
style={{
top: y,
left: x,
}}
>
<button
type="button"
className="App-drawer--close btn btn-default"
onClick={hide}
>
X
</button>
<div className="App-drawer--content">{content}</div>
{actions.map((action) => (
<button
key={action.title}
type="button"
onClick={action.onClick}
className="btn btn-secondary"
>
{action.title}
</button>
))}
</div>
);
}

View File

@ -1,230 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import debounce from "lodash.debounce";
const FAVORITES_STORAGE_KEY = "Emojis/Favorites";
const MINIMUM_SEARCH_INTERVAL = 350;
interface MarseyListEmoji {
author: string;
class: string;
count: number;
name: string;
tags: string[];
}
export function useEmojis() {
const emojiDictionary = useRef(new EmojiDictionary());
const [error, setError] = useState("");
const [emojis, setEmojis] = useState<MarseyListEmoji[]>([]);
const [genres, setGenres] = useState<string[]>([]);
const [collections, setCollections] = useState<Record<string, string[]>>({});
const [favorites, setFavorites] = useState<string[]>([]);
const [queries, setQueries] = useState<string[]>([]);
const [mostRecentQuery, setMostRecentQuery] = useState("");
const [visible, setVisible] = useState<string[]>([]);
const addQuery = useCallback(
(query: string) => setQueries((prev) => prev.concat(query)),
[]
);
const debouncedQueryAdder = useMemo(
() => debounce(addQuery, MINIMUM_SEARCH_INTERVAL),
[]
);
// Retrieve the list.
useEffect(() => {
fetch("/emojis")
.then((res) => res.json())
.then(setEmojis)
.catch(setError);
}, []);
// Load favorites.
useEffect(() => {
const persisted = window.localStorage.getItem(FAVORITES_STORAGE_KEY);
if (persisted) {
setFavorites(JSON.parse(persisted));
}
}, []);
// Persist favorites.
useEffect(() => {
window.localStorage.setItem(
FAVORITES_STORAGE_KEY,
JSON.stringify(Array.from(new Set(favorites)))
);
}, [favorites]);
// When emojis are received, update the dictionary.
useEffect(() => {
const dictionary = emojiDictionary.current;
const genreCollections: Record<string, string[]> = {};
for (const emoji of emojis) {
dictionary.updateTag(emoji.name, emoji.name);
if (typeof emoji.author !== "undefined" && emoji.author !== null) {
dictionary.updateTag(emoji.author.toLowerCase(), emoji.name);
}
if (emoji.tags instanceof Array) {
for (const tag of emoji.tags) {
dictionary.updateTag(tag, emoji.name);
}
}
dictionary.classes.add(emoji.class);
if (!genreCollections[emoji.class]) {
genreCollections[emoji.class] = [];
}
genreCollections[emoji.class].push(emoji.name);
}
setGenres(Array.from(dictionary.classes.values()) as string[]);
setCollections(genreCollections);
}, [emojis]);
// Process queries as they come in.
useEffect(() => {
if (queries.length > 0) {
const lastQuery = queries[queries.length - 1].toLowerCase();
setQueries([]);
setMostRecentQuery(lastQuery);
if (lastQuery.length === 0) {
return setVisible([]);
}
const results = emojiDictionary.current.completeSearch(lastQuery);
const nextVisible = Array.from(results.values()) as string[];
setVisible(nextVisible);
}
}, [queries]);
// Clean up any debounced calls before exit.
useEffect(() => {
return () => {
debouncedQueryAdder.cancel();
};
}, []);
return {
error,
ready: emojis.length > 0,
visible,
genres,
collections,
favorites,
mostRecentQuery,
addQuery,
updateVisible: setVisible,
updateFavorites: setFavorites
};
}
class EmojiDictionaryNode {
tag = "";
names = [];
constructor(tag: string, name: string) {
this.tag = tag;
this.names = [name];
}
}
class EmojiDictionary {
dictionary = [];
classes = new Set();
updateTag(tag: string, name: string) {
let low = 0;
let high = this.dictionary.length;
while (low < high) {
let mid = (low + high) >>> 1;
if (this.dictionary[mid].tag.length < tag.length) {
low = mid + 1;
} else {
high = mid;
}
}
let target = low;
if (
typeof this.dictionary[target] !== "undefined" &&
this.dictionary[target].tag === tag
) {
this.dictionary[target].names.push(name);
} else {
this.dictionary.splice(target, 0, new EmojiDictionaryNode(tag, name));
}
}
searchFor(query: string) {
query = query.toLowerCase();
const result = new Set();
if (this.dictionary.length === 0) {
return result;
}
let low = 0;
let high = this.dictionary.length;
while (low < high) {
let mid = (low + high) >>> 1;
if (this.dictionary[mid].tag.length < query.length) {
low = mid + 1;
} else {
high = mid;
}
}
let target = low;
for (
let i = target;
i >= 0 && this.dictionary[i].tag.startsWith(query);
i--
) {
for (const name of this.dictionary[i].names) {
result.add(name);
}
}
for (
let i = target + 1;
i < this.dictionary.length && this.dictionary[i].tag.startsWith(query);
i++
) {
for (const name of this.dictionary[i].names) {
result.add(name);
}
}
return result;
}
completeSearch(query: string) {
query = query.toLowerCase();
const result = new Set();
for (const { tag, names } of this.dictionary) {
if (tag.includes(query)) {
for (const name of names) {
result.add(name);
}
}
}
return result;
}
}

View File

@ -1,56 +0,0 @@
import { useEffect, useState } from "react";
export function useRootContext() {
const [
{
admin,
id,
username,
censored,
themeColor,
siteName,
nameColor,
avatar,
hat,
},
setContext,
] = useState({
id: "",
username: "",
admin: false,
censored: true,
themeColor: "#ff66ac",
siteName: "",
nameColor: "",
avatar: "",
hat: "",
});
useEffect(() => {
const root = document.getElementById("root");
setContext({
id: root.dataset.id,
username: root.dataset.username,
admin: root.dataset.admin === "True",
censored: root.dataset.censored === "True",
themeColor: root.dataset.themecolor,
siteName: root.dataset.sitename,
nameColor: root.dataset.namecolor,
avatar: root.dataset.avatar,
hat: root.dataset.hat,
});
}, []);
return {
id,
admin,
username,
censored,
themeColor,
siteName,
nameColor,
avatar,
hat,
};
}

View File

@ -1,19 +0,0 @@
import { useCallback, useEffect, useState } from "react";
export function useWindowFocus() {
const [focused, setFocused] = useState(true);
const onFocus = useCallback(() => setFocused(true), []);
const onBlur = useCallback(() => setFocused(false), []);
useEffect(() => {
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
return () => {
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
};
});
return focused;
}

View File

@ -1,7 +0,0 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
const root = createRoot(document.getElementById("root"))
root.render(<App />);

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react",
"lib": ["es2015", "dom", "ESNext"],
"noEmit": true
}
}

View File

@ -1,604 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/runtime@^7.0.0", "@babel/runtime@^7.9.2":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
dependencies:
regenerator-runtime "^0.13.4"
"@esbuild/android-arm@0.15.13":
version "0.15.13"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.13.tgz#ce11237a13ee76d5eae3908e47ba4ddd380af86a"
integrity sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==
"@esbuild/linux-loong64@0.15.13":
version "0.15.13"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz#64e8825bf0ce769dac94ee39d92ebe6272020dfc"
integrity sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==
"@react-dnd/asap@^5.0.1":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==
"@react-dnd/invariant@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df"
integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==
"@react-dnd/shallowequal@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4"
integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@types/humanize-duration@^3.27.1":
version "3.27.1"
resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7"
integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==
"@types/lodash.clonedeep@^4.5.7":
version "4.5.7"
resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz#0e119f582ed6f9e6b373c04a644651763214f197"
integrity sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==
dependencies:
"@types/lodash" "*"
"@types/lodash.debounce@^4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==
dependencies:
"@types/lodash" "*"
"@types/lodash.throttle@^4.1.7":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz#4ef379eb4f778068022310ef166625f420b6ba58"
integrity sha512-znwGDpjCHQ4FpLLx19w4OXDqq8+OvREa05H89obtSyXyOFKL3dDjCslsmfBz0T2FU8dmf5Wx1QvogbINiGIu9g==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.185"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.185.tgz#c9843f5a40703a8f5edfd53358a58ae729816908"
integrity sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==
"@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/react-dom@^18.0.7":
version "18.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.9.tgz#ffee5e4bfc2a2f8774b15496474f8e7fe8d0b504"
integrity sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==
dependencies:
"@types/react" "*"
"@types/react-virtualized-auto-sizer@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4"
integrity sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==
dependencies:
"@types/react" "*"
"@types/react-window@^1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==
dependencies:
"@types/react" "*"
"@types/react@*":
version "18.0.20"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.20.tgz#e4c36be3a55eb5b456ecf501bd4a00fd4fd0c9ab"
integrity sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^18.0.21":
version "18.0.25"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44"
integrity sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@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==
ansi-bold@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ansi-bold/-/ansi-bold-0.1.1.tgz#3e63950af5acc2ae2e670e6f67deb115d1a5f505"
integrity sha512-wWKwcViX1E28U6FohtWOP4sHFyArELHJ2p7+3BzbibqJiuISeskq6t7JnrLisUngMF5zMhgmXVw8Equjzz9OlA==
dependencies:
ansi-wrap "0.1.0"
ansi-wrap@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
classnames@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
commander@^2.15.1:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
csstype@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
dnd-core@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19"
integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==
dependencies:
"@react-dnd/asap" "^5.0.1"
"@react-dnd/invariant" "^4.0.1"
redux "^4.2.0"
dotenv@^16.0.3:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
engine.io-client@~6.2.3:
version "6.2.3"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.3.tgz#a8cbdab003162529db85e9de31575097f6d29458"
integrity sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.0.3"
ws "~8.2.3"
xmlhttprequest-ssl "~2.0.0"
engine.io-parser@~5.0.3:
version "5.0.4"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
es6-promise@^3.0.2:
version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==
esbuild-android-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz#5f25864055dbd62e250f360b38b4c382224063af"
integrity sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==
esbuild-android-arm64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz#d8820f999314efbe8e0f050653a99ff2da632b0f"
integrity sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==
esbuild-darwin-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz#99ae7fdaa43947b06cd9d1a1c3c2c9f245d81fd0"
integrity sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==
esbuild-darwin-arm64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz#bafa1814354ad1a47adcad73de416130ef7f55e3"
integrity sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==
esbuild-freebsd-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz#84ef85535c5cc38b627d1c5115623b088d1de161"
integrity sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==
esbuild-freebsd-arm64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz#033f21de434ec8e0c478054b119af8056763c2d8"
integrity sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==
esbuild-linux-32@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz#54290ea8035cba0faf1791ce9ae6693005512535"
integrity sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==
esbuild-linux-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz#4264249281ea388ead948614b57fb1ddf7779a2c"
integrity sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==
esbuild-linux-arm64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz#9323c333924f97a02bdd2ae8912b36298acb312d"
integrity sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==
esbuild-linux-arm@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz#b407f47b3ae721fe4e00e19e9f19289bef87a111"
integrity sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==
esbuild-linux-mips64le@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz#bdf905aae5c0bcaa8f83567fe4c4c1bdc1f14447"
integrity sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==
esbuild-linux-ppc64le@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz#2911eae1c90ff58a3bd3259cb557235df25aa3b4"
integrity sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==
esbuild-linux-riscv64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz#1837c660be12b1d20d2a29c7189ea703f93e9265"
integrity sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==
esbuild-linux-s390x@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz#d52880ece229d1bd10b2d936b792914ffb07c7fc"
integrity sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==
esbuild-netbsd-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz#de14da46f1d20352b43e15d97a80a8788275e6ed"
integrity sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==
esbuild-openbsd-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz#45e8a5fd74d92ad8f732c43582369c7990f5a0ac"
integrity sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==
esbuild-sunos-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz#f646ac3da7aac521ee0fdbc192750c87da697806"
integrity sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==
esbuild-windows-32@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz#fb4fe77c7591418880b3c9b5900adc4c094f2401"
integrity sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==
esbuild-windows-64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz#1fca8c654392c0c31bdaaed168becfea80e20660"
integrity sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==
esbuild-windows-arm64@0.15.13:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz#4ffd01b6b2888603f1584a2fe96b1f6a6f2b3dd8"
integrity sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==
esbuild@^0.15.11:
version "0.15.13"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.13.tgz#7293480038feb2bafa91d3f6a20edab3ba6c108a"
integrity sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==
optionalDependencies:
"@esbuild/android-arm" "0.15.13"
"@esbuild/linux-loong64" "0.15.13"
esbuild-android-64 "0.15.13"
esbuild-android-arm64 "0.15.13"
esbuild-darwin-64 "0.15.13"
esbuild-darwin-arm64 "0.15.13"
esbuild-freebsd-64 "0.15.13"
esbuild-freebsd-arm64 "0.15.13"
esbuild-linux-32 "0.15.13"
esbuild-linux-64 "0.15.13"
esbuild-linux-arm "0.15.13"
esbuild-linux-arm64 "0.15.13"
esbuild-linux-mips64le "0.15.13"
esbuild-linux-ppc64le "0.15.13"
esbuild-linux-riscv64 "0.15.13"
esbuild-linux-s390x "0.15.13"
esbuild-netbsd-64 "0.15.13"
esbuild-openbsd-64 "0.15.13"
esbuild-sunos-64 "0.15.13"
esbuild-windows-32 "0.15.13"
esbuild-windows-64 "0.15.13"
esbuild-windows-arm64 "0.15.13"
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fs-find-root@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fs-find-root/-/fs-find-root-2.0.0.tgz#71c23b384db6bcb1e8ec637cade707fda39c593b"
integrity sha512-LmgsxDwnxd+sfm3EZ66P8nSlUMm69hKz/LdXKChK2a+5xXnrAB7MPn2uo0VCyrgj8lZNqyj6EIdD516ILZAEBg==
dependencies:
es6-promise "^3.0.2"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
gaze@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
dependencies:
globule "^1.0.0"
glob@~7.1.1:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
globule@^1.0.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.4.tgz#7c11c43056055a75a6e68294453c17f2796170fb"
integrity sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==
dependencies:
glob "~7.1.1"
lodash "^4.17.21"
minimatch "~3.0.2"
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
humanize-duration@^3.27.3:
version "3.27.3"
resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.27.3.tgz#db654e72ebf5ccfe232c7f56bc58aa3a6fe4df88"
integrity sha512-iimHkHPfIAQ8zCDQLgn08pRqSVioyWvnGfaQ8gond2wf7Jq2jJ+24ykmnRyiz3fIldcn4oUuQXpjqKLhSVR7lw==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
"memoize-one@>=3.1.1 <6":
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
minimatch@^3.0.4:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@~3.0.2:
version "3.0.8"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1"
integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==
dependencies:
brace-expansion "^1.1.7"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
react-dnd-html5-backend@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6"
integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==
dependencies:
dnd-core "^16.0.1"
react-dnd@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37"
integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==
dependencies:
"@react-dnd/invariant" "^4.0.1"
"@react-dnd/shallowequal" "^4.0.1"
dnd-core "^16.0.1"
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-virtualized-auto-sizer@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz#bfb8414698ad1597912473de3e2e5f82180c1195"
integrity sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==
react-window@^1.8.8:
version "1.8.8"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.8.tgz#1b52919f009ddf91970cbdb2050a6c7be44df243"
integrity sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
redux@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
dependencies:
"@babel/runtime" "^7.9.2"
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
run-when-changed@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/run-when-changed/-/run-when-changed-2.1.0.tgz#2e76d6ff6014d38786a3a11b98e9291c7e934953"
integrity sha512-ge/wuPQAvQz0uDEOO8W2c3g4mqXDa1XXtnJmjP9+6Nqfb+XdUYkQX/KvKmSvJ4xG5weD3RGm0u2Q3UsGzAo5gw==
dependencies:
ansi-bold "^0.1.1"
commander "^2.15.1"
fs-find-root "^2.0.0"
gaze "^1.1.2"
minimatch "^3.0.4"
scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies:
loose-envify "^1.1.0"
socket.io-client@^4.5.3:
version "4.5.3"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.3.tgz#bed69209d001465b2fea650d2e95c1e82768ab5e"
integrity sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.2.3"
socket.io-parser "~4.2.0"
socket.io-parser@~4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
typescript@^4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
weak-key@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/weak-key/-/weak-key-1.0.2.tgz#dd5f66648ffb7e83810ea0553a948c60b2b50588"
integrity sha512-x9y9moPEcom985nUdHxM+YWbMcP3Ru+fmYqVNHSb6djJGg7H6Ru2ohuzaVIXx1JNyp8E7GO7GsBnehRntaBlsg==
dependencies:
core-js "^2.4.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@~8.2.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
xmlhttprequest-ssl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==