diff --git a/package.json b/package.json index 997bee52..21f3b6a7 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dependencies": { "fs-extra": "^10.0.0", "klaw": "^3.0.0", + "react-beautiful-dnd": "^13.1.0", "sirv-cli": "^1.0.14", "vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b" }, @@ -81,6 +82,7 @@ "@types/node": "^15.12.4", "@types/preact-i18n": "^2.3.0", "@types/prismjs": "^1.16.5", + "@types/react-beautiful-dnd": "^13.1.2", "@types/react-helmet": "^6.1.1", "@types/react-router-dom": "^5.1.7", "@types/react-scroll": "^1.8.2", diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index c6b07383..409f2439 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -48,11 +48,11 @@ export type UploadState = | { type: "none" } | { type: "attached"; files: File[] } | { - type: "uploading"; - files: File[]; - percent: number; - cancel: CancelTokenSource; - } + type: "uploading"; + files: File[]; + percent: number; + cancel: CancelTokenSource; + } | { type: "sending"; files: File[] } | { type: "failed"; files: File[]; error: string }; @@ -173,9 +173,9 @@ export default observer(({ channel }: Props) => { const text = action === "quote" ? `${content - .split("\n") - .map((x) => `> ${x}`) - .join("\n")}\n\n` + .split("\n") + .map((x) => `> ${x}`) + .join("\n")}\n\n` : `${content} `; if (!draft || draft.length === 0) { @@ -225,8 +225,8 @@ export default observer(({ channel }: Props) => { toReplace == "" ? msg.content.toString() + newText : msg.content - .toString() - .replace(new RegExp(toReplace, flags), newText); + .toString() + .replace(new RegExp(toReplace, flags), newText); if (newContent != msg.content) { if (newContent.length == 0) { @@ -305,10 +305,10 @@ export default observer(({ channel }: Props) => { files, percent: Math.round( (i * 100 + (100 * e.loaded) / e.total) / - Math.min( - files.length, - CAN_UPLOAD_AT_ONCE, - ), + Math.min( + files.length, + CAN_UPLOAD_AT_ONCE, + ), ), cancel, }), @@ -398,6 +398,7 @@ export default observer(({ channel }: Props) => { } } + // TODO: change to useDebounceCallback // eslint-disable-next-line const debouncedStopTyping = useCallback( debounce(stopTyping as (...args: unknown[]) => void, 1000), @@ -553,13 +554,13 @@ export default observer(({ channel }: Props) => { placeholder={ channel.channel_type === "DirectMessage" ? translate("app.main.channel.message_who", { - person: channel.recipient?.username, - }) + person: channel.recipient?.username, + }) : channel.channel_type === "SavedMessages" - ? translate("app.main.channel.message_saved") - : translate("app.main.channel.message_where", { - channel_name: channel.name ?? undefined, - }) + ? translate("app.main.channel.message_saved") + : translate("app.main.channel.message_where", { + channel_name: channel.name ?? undefined, + }) } disabled={ uploadState.type === "uploading" || diff --git a/src/components/ui/SaveStatus.tsx b/src/components/ui/SaveStatus.tsx new file mode 100644 index 00000000..9267b166 --- /dev/null +++ b/src/components/ui/SaveStatus.tsx @@ -0,0 +1,32 @@ +import { Check, CloudUpload } from "@styled-icons/boxicons-regular"; +import { Pencil } from "@styled-icons/boxicons-solid"; +import styled from "styled-components"; + +const StatusBase = styled.div` + gap: 4px; + padding: 4px; + display: flex; + align-items: center; + text-transform: capitalize; +`; + +export type EditStatus = "saved" | "editing" | "saving"; +interface Props { + status: EditStatus; +} + +export default function SaveStatus({ status }: Props) { + return ( + + {status === "saved" ? ( + + ) : status === "editing" ? ( + + ) : ( + + )} + {/* FIXME: add i18n */} + {status} + + ); +} diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 149e1043..252e44da 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -2,6 +2,7 @@ import { Prompt } from "react-router"; import { useHistory } from "react-router-dom"; import type { Attachment } from "revolt-api/types/Autumn"; import { Bot } from "revolt-api/types/Bots"; +import { TextChannel, VoiceChannel } from "revolt-api/types/Channels"; import type { EmbedImage } from "revolt-api/types/January"; import { Channel } from "revolt.js/dist/maps/Channels"; import { Message } from "revolt.js/dist/maps/Messages"; @@ -42,7 +43,12 @@ export type Screen = | { type: "leave_server"; target: Server } | { type: "delete_server"; target: Server } | { type: "delete_channel"; target: Channel } - | { type: "delete_bot"; target: string; name: string; cb: () => void } + | { + type: "delete_bot"; + target: string; + name: string; + cb?: () => void; + } | { type: "delete_message"; target: Message } | { type: "create_invite"; @@ -52,7 +58,11 @@ export type Screen = | { type: "ban_member"; target: Server; user: User } | { type: "unfriend_user"; target: User } | { type: "block_user"; target: User } - | { type: "create_channel"; target: Server } + | { + type: "create_channel"; + target: Server; + cb?: (channel: TextChannel | VoiceChannel) => void; + } | { type: "create_category"; target: Server } )) | ({ id: "special_input" } & ( diff --git a/src/context/intermediate/modals/Prompt.tsx b/src/context/intermediate/modals/Prompt.tsx index 79258ce5..f5ba994c 100644 --- a/src/context/intermediate/modals/Prompt.tsx +++ b/src/context/intermediate/modals/Prompt.tsx @@ -1,5 +1,6 @@ import { observer } from "mobx-react-lite"; import { useHistory } from "react-router-dom"; +import { TextChannel, VoiceChannel } from "revolt-api/types/Channels"; import { Channel } from "revolt.js/dist/maps/Channels"; import { Message as MessageI } from "revolt.js/dist/maps/Messages"; import { Server } from "revolt.js/dist/maps/Servers"; @@ -60,7 +61,7 @@ type SpecialProps = { onClose: () => void } & ( | { type: "leave_server"; target: Server } | { type: "delete_server"; target: Server } | { type: "delete_channel"; target: Channel } - | { type: "delete_bot"; target: string; name: string; cb: () => void } + | { type: "delete_bot"; target: string; name: string; cb?: () => void } | { type: "delete_message"; target: MessageI } | { type: "create_invite"; @@ -70,7 +71,11 @@ type SpecialProps = { onClose: () => void } & ( | { type: "ban_member"; target: Server; user: User } | { type: "unfriend_user"; target: User } | { type: "block_user"; target: User } - | { type: "create_channel"; target: Server } + | { + type: "create_channel"; + target: Server; + cb?: (channel: TextChannel | VoiceChannel) => void; + } | { type: "create_category"; target: Server } ); @@ -158,7 +163,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { break; case "delete_bot": client.bots.delete(props.target); - props.cb(); + props.cb?.(); break; } @@ -424,9 +429,14 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { nonce: ulid(), }); - history.push( - `/server/${props.target._id}/channel/${channel._id}`, - ); + if (props.cb) { + props.cb(channel); + } else { + history.push( + `/server/${props.target._id}/channel/${channel._id}`, + ); + } + onClose(); } catch (err) { setError(takeError(err)); @@ -472,7 +482,6 @@ export const SpecialPromptModal = observer((props: SpecialProps) => { } case "create_category": { const [name, setName] = useState(""); - const history = useHistory(); return ( void, duration: number) { // Store the timer variable. let timer: NodeJS.Timeout; @@ -13,3 +17,60 @@ export function debounce(cb: (...args: unknown[]) => void, duration: number) { }, duration); }; } + +export function useDebounceCallback( + cb: (...args: unknown[]) => void, + inputs: Inputs, + duration = 1000, +) { + // eslint-disable-next-line + return useCallback( + debounce(cb as (...args: unknown[]) => void, duration), + inputs, + ); +} + +export function useAutosaveCallback( + cb: (...args: unknown[]) => void, + inputs: Inputs, + duration = 1000, +) { + const ref = useRef(cb); + + // eslint-disable-next-line + const callback = useCallback( + debounce(() => ref.current(), duration), + [], + ); + + useEffect(() => { + ref.current = cb; + callback(); + // eslint-disable-next-line + }, [cb, callback, ...inputs]); +} + +export function useAutosave( + cb: () => void, + dependency: T, + initialValue: T, + onBeginChange?: () => void, + duration?: number, +) { + if (onBeginChange) { + // eslint-disable-next-line + useEffect( + () => { + !isEqual(dependency, initialValue) && onBeginChange(); + }, + // eslint-disable-next-line + [dependency], + ); + } + + return useAutosaveCallback( + () => !isEqual(dependency, initialValue) && cb(), + [dependency], + duration, + ); +} diff --git a/src/lib/dnd.ts b/src/lib/dnd.ts new file mode 100644 index 00000000..9f8f1eb2 --- /dev/null +++ b/src/lib/dnd.ts @@ -0,0 +1,56 @@ +import { + Draggable as rbdDraggable, + DraggableProps as rbdDraggableProps, + DraggableProvided as rbdDraggableProvided, + DraggableProvidedDraggableProps as rbdDraggableProvidedDraggableProps, + DraggableProvidedDragHandleProps as rbdDraggableProvidedDragHandleProps, + DraggableRubric, + DraggableStateSnapshot, + Droppable as rbdDroppable, + DroppableProps, + DroppableProvided, + DroppableStateSnapshot, +} from "react-beautiful-dnd"; + +export type DraggableProvidedDraggableProps = Omit< + rbdDraggableProvidedDraggableProps, + "style" | "onTransitionEnd" +> & { + style?: string; + onTransitionEnd?: JSX.TransitionEventHandler; +}; + +export type DraggableProvidedDragHandleProps = Omit< + rbdDraggableProvidedDragHandleProps, + "onDragStart" +> & { + onDragStart?: JSX.DragEventHandler; +}; + +export type DraggableProvided = rbdDraggableProvided & { + draggableProps: DraggableProvidedDraggableProps; + dragHandleProps?: DraggableProvidedDragHandleProps | undefined; +}; + +export type DraggableChildrenFn = ( + provided: DraggableProvided, + snapshot: DraggableStateSnapshot, + rubric: DraggableRubric, +) => JSX.Element; + +export type DraggableProps = Omit & { + children: DraggableChildrenFn; +}; + +export const Draggable = rbdDraggable as unknown as ( + props: DraggableProps, +) => JSX.Element; + +export const Droppable = rbdDroppable as unknown as ( + props: Omit & { + children( + provided: DroppableProvided, + snapshot: DroppableStateSnapshot, + ): JSX.Element; + }, +) => JSX.Element; diff --git a/src/pages/settings/ServerSettings.tsx b/src/pages/settings/ServerSettings.tsx index 639eae2e..ca31d122 100644 --- a/src/pages/settings/ServerSettings.tsx +++ b/src/pages/settings/ServerSettings.tsx @@ -50,6 +50,7 @@ export default observer(() => { title: ( ), + hideTitle: true, }, { id: "members", diff --git a/src/pages/settings/server/Categories.tsx b/src/pages/settings/server/Categories.tsx index 85884f37..f3dcd920 100644 --- a/src/pages/settings/server/Categories.tsx +++ b/src/pages/settings/server/Categories.tsx @@ -1,139 +1,460 @@ -import isEqual from "lodash.isequal"; +import { Plus, X } from "@styled-icons/boxicons-regular"; import { observer } from "mobx-react-lite"; +import { DragDropContext } from "react-beautiful-dnd"; +import { TextChannel, VoiceChannel } from "revolt-api/types/Channels"; import { Category } from "revolt-api/types/Servers"; import { Server } from "revolt.js/dist/maps/Servers"; +import styled, { css } from "styled-components"; import { ulid } from "ulid"; -import { useState } from "preact/hooks"; +import { Text } from "preact-i18n"; +import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; + +import { useAutosave } from "../../../lib/debounce"; +import { Draggable, Droppable } from "../../../lib/dnd"; +import { noop } from "../../../lib/js"; + +import { useIntermediate } from "../../../context/intermediate/Intermediate"; import ChannelIcon from "../../../components/common/ChannelIcon"; -import Button from "../../../components/ui/Button"; -import ComboBox from "../../../components/ui/ComboBox"; -import InputBox from "../../../components/ui/InputBox"; -import Tip from "../../../components/ui/Tip"; +import SaveStatus, { EditStatus } from "../../../components/ui/SaveStatus"; + +const KanbanEntry = styled.div` + padding: 2px 4px; + + > .inner { + display: flex; + align-items: center; + + gap: 4px; + height: 40px; + padding: 8px; + flex-shrink: 0; + font-size: 0.9em; + background: var(--primary-background); + + img { + flex-shrink: 0; + } + + span { + min-width: 0; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } +`; + +const KanbanList = styled.div<{ last: boolean }>` + ${(props) => + !props.last && + css` + padding-inline-end: 4px; + `} + + > .inner { + width: 180px; + display: flex; + flex-shrink: 0; + overflow-y: auto; + padding-bottom: 2px; + flex-direction: column; + background: var(--secondary-background); + + input { + width: 100%; + height: 100%; + border: none; + font-size: 1em; + text-align: center; + background: transparent; + color: var(--foreground); + } + + > [data-rbd-droppable-id] { + min-height: 24px; + } + } +`; + +const Row = styled.div` + gap: 2px; + margin: 4px; + display: flex; + + > :first-child { + flex-grow: 1; + } +`; + +const KanbanListHeader = styled.div` + height: 34px; + display: grid; + min-width: 34px; + place-items: center; + cursor: pointer !important; + transition: 0.2s ease background-color; + + &:hover { + background: var(--background); + } +`; + +const KanbanBoard = styled.div` + display: flex; + flex-direction: row; +`; + +const FullSize = styled.div` + flex-grow: 1; + min-height: 0; + + > * { + height: 100%; + overflow-x: scroll; + } +`; + +const Header = styled.div` + display: flex; + + h1 { + flex-grow: 1; + } +`; interface Props { server: Server; } -// ! FIXME: really bad code export const Categories = observer(({ server }: Props) => { - const channels = server.channels.filter((x) => typeof x !== "undefined"); + const [status, setStatus] = useState("saved"); + const [categories, setCategories] = useState( + server.categories ?? [], + ); - const [cats, setCats] = useState(server.categories ?? []); - const [name, setName] = useState(""); + useAutosave( + async () => { + setStatus("saving"); + await server.edit({ categories }); + setStatus("saved"); + }, + categories, + server.categories, + () => setStatus("editing"), + ); + + const defaultCategory = useMemo(() => { + return { + title: "Uncategorized", + channels: [...server.channels] + .filter((x) => x) + .map((x) => x!._id) + .filter( + (x) => !categories.find((cat) => cat.channels.includes(x)), + ), + id: "none", + }; + }, [categories, server.channels]); return ( -
- This section is under construction. -

- -

-

categories

- {cats.map((category) => ( -
- - setCats( - cats.map((y) => - y.id === category.id - ? { - ...y, - title: e.currentTarget.value, - } - : y, - ), - ) - } - contrast - /> - -
- ))} -

create new

-

- setName(e.currentTarget.value)} - contrast - /> - -

-

channels

- {channels.map((channel) => { - return ( -
-
- {" "} - {channel!.name} -
- - x.channels.includes(channel!._id), - )?.id ?? "none" - } - onChange={(e) => - setCats( - cats.map((x) => { - return { - ...x, - channels: [ - ...x.channels.filter( - (y) => y !== channel!._id, - ), - ...(e.currentTarget.value === - x.id - ? [channel!._id] - : []), - ], - }; - }), - ) - }> - - {cats.map((x) => ( - - ))} - -
- ); - })} -
+ <> +
+

+ +

+ +
+ { + const { destination, source, draggableId, type } = target; + + if ( + !destination || + (destination.droppableId === source.droppableId && + destination.index === source.index) + ) { + return; + } + + if (type === "column") { + if (destination.index === 0) return; + + // Remove from array. + const cat = categories.find( + (x) => x.id === draggableId, + ); + const arr = categories.filter( + (x) => x.id !== draggableId, + ); + + // Insert at new position. + arr.splice(destination.index - 1, 0, cat!); + setCategories(arr); + } else { + setCategories( + categories.map((category) => { + if (category.id === destination.droppableId) { + const channels = category.channels.filter( + (x) => x !== draggableId, + ); + + channels.splice( + destination.index, + 0, + draggableId, + ); + + return { + ...category, + channels, + }; + } else if (category.id === source.droppableId) { + return { + ...category, + channels: category.channels.filter( + (x) => x !== draggableId, + ), + }; + } + + return category; + }), + ); + } + }}> + + + {(provided) => ( +
+ + + {categories.map((category, index) => ( + { + setCategories( + categories.map((x) => + x.id === category.id + ? { + ...x, + title, + } + : x, + ), + ); + }} + deleteSelf={() => + setCategories( + categories.filter( + (x) => + x.id !== + category.id, + ), + ) + } + addChannel={(channel) => { + setCategories( + categories.map((x) => + x.id === category.id + ? { + ...x, + channels: [ + ...x.channels, + channel._id, + ], + } + : x, + ), + ); + }} + /> + ))} + +
+ + setCategories([ + ...categories, + { + id: ulid(), + title: "New Category", + channels: [], + }, + ]) + }> + + +
+
+ {provided.placeholder} +
+
+ )} +
+
+
+ ); }); + +function ListElement({ + category, + server, + index, + setTitle, + deleteSelf, + addChannel, + draggable, +}: { + category: Category; + server: Server; + index: number; + setTitle?: (title: string) => void; + deleteSelf?: () => void; + addChannel: (channel: TextChannel | VoiceChannel) => void; + draggable?: boolean; +}) { + const { openScreen } = useIntermediate(); + const [editing, setEditing] = useState(); + const startEditing = () => setTitle && setEditing(category.title); + + const save = useCallback(() => { + setEditing(undefined); + setTitle!(editing!); + }, [editing, setTitle]); + + useEffect(() => { + if (!editing) return; + + function onClick(ev: MouseEvent) { + if ((ev.target as HTMLElement)?.id !== category.id) { + save(); + } + } + + document.addEventListener("mousedown", onClick); + return () => document.removeEventListener("mousedown", onClick); + }, [editing, category.id, save]); + + return ( + + {(provided) => ( +
+ +
+ + + {editing ? ( + + setEditing( + e.currentTarget.value, + ) + } + onKeyDown={(e) => + e.key === "Enter" && save() + } + id={category.id} + /> + ) : ( + + {category.title} + + )} + + {deleteSelf && ( + + + + )} + + + {(provided) => ( +
+ {category.channels.map((x, index) => { + const channel = + server.client.channels.get(x); + if (!channel) return null; + + return ( + + {(provided) => ( +
+ +
+ + + { + channel.name + } + +
+
+
+ )} +
+ ); + })} + {provided.placeholder} +
+ )} +
+ + openScreen({ + id: "special_prompt", + type: "create_channel", + target: server, + cb: addChannel, + }) + }> + + +
+
+
+ )} +
+ ); +} diff --git a/yarn.lock b/yarn.lock index 61375864..725fac8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1398,6 +1398,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/react-beautiful-dnd@^13.1.2": + version "13.1.2" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130" + integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg== + dependencies: + "@types/react" "*" + "@types/react-helmet@^6.1.1": version "6.1.2" resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.2.tgz#e9d7d16b29e4ec5716711c52c35c3cec45819eac" @@ -1981,6 +1988,13 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -3069,6 +3083,11 @@ mdurl@^1.0.1: sdp-transform "^2.14.1" supports-color "^8.1.1" +memoize-one@^5.1.1: + 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== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -3365,6 +3384,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -3372,6 +3396,19 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +react-beautiful-dnd@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" + integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-device-detect@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.17.0.tgz#a00b4fd6880cebfab3fd8a42a79dc0290cdddca9" @@ -3409,6 +3446,18 @@ react-overlapping-panels@1.2.2: resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.2.2.tgz#16b60ed60045a7fa40bcf321de113c655f6e0acd" integrity sha512-jZ8ZT4tnqM2YQF91Ct+9dLk7rSjnNiudxzgKlsaVfgwEjdBAWtE8nWJX9d2jDZZ9qimWgg43u5+SF6U+ELjyKQ== +react-redux@^7.2.0: + version "7.2.5" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.5.tgz#213c1b05aa1187d9c940ddfc0b29450957f6a3b8" + integrity sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" + react-redux@^7.2.4: version "7.2.4" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" @@ -3484,7 +3533,7 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redux@^4.0.0, redux@^4.1.0: +redux@^4.0.0, redux@^4.0.4, redux@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47" integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw== @@ -3963,7 +4012,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -tiny-invariant@^1.0.2: +tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== @@ -4115,6 +4164,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-memo-one@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + use-resize-observer@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-7.1.0.tgz#709ea7540fbe0a60ceae41ee2bef933d7782e4d4"