From 0a0c00fe58c99a7306e9daba05389672aceba2aa Mon Sep 17 00:00:00 2001 From: Paul <paulmakles@gmail.com> Date: Sat, 19 Jun 2021 20:00:30 +0100 Subject: [PATCH] Port friends menu over. --- package.json | 4 +- src/components/ui/Header.tsx | 3 +- src/components/ui/IconButton.tsx | 43 +++++++++++ src/components/ui/Modal.tsx | 29 ++++++- src/components/ui/Overline.tsx | 7 +- src/context/intermediate/Intermediate.tsx | 2 +- src/context/intermediate/Modals.tsx | 8 +- src/context/intermediate/Popovers.tsx | 6 ++ src/context/intermediate/modals/Input.tsx | 17 +++- src/context/intermediate/modals/Prompt.tsx | 11 ++- src/lib/stopPropagation.ts | 4 + src/pages/App.tsx | 86 +++++++++++++++++++-- src/pages/friends/Friend.module.scss | 71 +++++++++++++++++ src/pages/friends/Friend.tsx | 90 ++++++++++++++++++++++ src/pages/friends/Friends.tsx | 85 ++++++++++++++++++++ src/styles/_elements.scss | 5 ++ src/types/Preact.ts | 2 +- yarn.lock | 9 ++- 18 files changed, 452 insertions(+), 30 deletions(-) create mode 100644 src/components/ui/IconButton.tsx create mode 100644 src/lib/stopPropagation.ts create mode 100644 src/pages/friends/Friend.module.scss create mode 100644 src/pages/friends/Friend.tsx create mode 100644 src/pages/friends/Friends.tsx diff --git a/package.json b/package.json index e2c051e..8862bde 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "build": "rimraf build && tsc && vite build", "preview": "vite preview", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", - "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'" + "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", + "typecheck": "tsc --noEmit" }, "eslintConfig": { "parser": "@typescript-eslint/parser", @@ -54,6 +55,7 @@ "markdown-it-emoji": "^2.0.0", "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", + "preact-context-menu": "^0.1.5", "preact-i18n": "^2.4.0-preactx", "prettier": "^2.3.1", "prismjs": "^1.23.0", diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index 6a97dd3..cb17b8c 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -17,7 +17,7 @@ export default styled.div<Props>` flex-shrink: 0; align-items: center; - background-color: var(--primary-background); + background-color: var(--primary-header); background-size: cover !important; background-position: center !important; @@ -27,6 +27,7 @@ export default styled.div<Props>` ` } ${ props => props.placement === 'secondary' && css` + background-color: var(--secondary-header); padding: 14px; ` } `; diff --git a/src/components/ui/IconButton.tsx b/src/components/ui/IconButton.tsx new file mode 100644 index 0000000..497f287 --- /dev/null +++ b/src/components/ui/IconButton.tsx @@ -0,0 +1,43 @@ +import styled, { css } from "styled-components"; + +interface Props { + type?: 'default' | 'circle' +} + +const normal = `var(--secondary-foreground)`; +const hover = `var(--foreground)`; + +export default styled.div<Props>` + z-index: 1; + display: grid; + cursor: pointer; + place-items: center; + + fill: ${normal}; + color: ${normal}; + stroke: ${normal}; + + a { + color: ${normal}; + } + + &:hover { + fill: ${hover}; + color: ${hover}; + stroke: ${hover}; + + a { + color: ${hover}; + } + } + + ${ props => props.type === 'circle' && css` + padding: 4px; + border-radius: 50%; + background-color: var(--secondary-header); + + &:hover { + background-color: var(--primary-header); + } + ` } +`; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 65a9396..7d80cb8 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -2,7 +2,7 @@ import Button from "./Button"; import classNames from "classnames"; import { Children } from "../../types/Preact"; import { createPortal, useEffect } from "preact/compat"; -import styled, { keyframes } from "styled-components"; +import styled, { css, keyframes } from "styled-components"; const open = keyframes` 0% {opacity: 0;} @@ -48,6 +48,26 @@ const ModalContainer = styled.div` `; const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border']?: boolean }>` + border-radius: 8px; + text-overflow: ellipsis; + + h3 { + margin-top: 0; + } + + ${ props => !props.noBackground && css` + padding: 1.5em; + background: var(--secondary-header); + ` } + + ${ props => props.attachment && css` + border-radius: 8px 8px 0 0; + ` } + + ${ props => props.border && css` + border-radius: 10px; + border: 2px solid var(--secondary-background); + ` } `; const ModalActions = styled.div` @@ -64,7 +84,8 @@ export interface Action { text: Children; onClick: () => void; confirmation?: boolean; - style?: 'default' | 'contrast' | 'error' | 'contrast-error'; + contrast?: boolean; + error?: boolean; } interface Props { @@ -123,7 +144,9 @@ export default function Modal(props: Props) { {props.actions && ( <ModalActions> {props.actions.map(x => ( - <Button style={x.style ?? "contrast"} + <Button + contrast={x.contrast ?? true} + error={x.error ?? false} onClick={x.onClick} disabled={props.disabled}> {x.text} diff --git a/src/components/ui/Overline.tsx b/src/components/ui/Overline.tsx index ee43499..8623369 100644 --- a/src/components/ui/Overline.tsx +++ b/src/components/ui/Overline.tsx @@ -1,9 +1,10 @@ import styled, { css } from "styled-components"; import { Children } from "../../types/Preact"; +import { Text } from 'preact-i18n'; interface Props { + error?: string; block?: boolean; - error?: Children; children?: Children; type?: "default" | "subtle" | "error"; } @@ -45,7 +46,9 @@ export default function Overline(props: Props) { <OverlineBase {...props}> {props.children} {props.children && props.error && <> · </>} - {props.error && <Overline type="error">{props.error}</Overline>} + {props.error && <Overline type="error"> + <Text id={`error.${props.error}`}>{props.error}</Text> + </Overline>} </OverlineBase> ); } diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index a623d9f..1c6bc8e 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -27,7 +27,7 @@ export type Screen = { type: "ban_member", target: Servers.Server, user: string } )) | ({ id: "special_input" } & ( - { type: "create_group" | "create_server" | "set_custom_status" } | + { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } | { type: "create_channel", server: string } )) | { diff --git a/src/context/intermediate/Modals.tsx b/src/context/intermediate/Modals.tsx index a70c137..b603559 100644 --- a/src/context/intermediate/Modals.tsx +++ b/src/context/intermediate/Modals.tsx @@ -1,12 +1,12 @@ import { Screen } from "./Intermediate"; import { ErrorModal } from "./modals/Error"; +import { InputModal } from "./modals/Input"; +import { PromptModal } from "./modals/Prompt"; import { SignedOutModal } from "./modals/SignedOut"; import { ClipboardModal } from "./modals/Clipboard"; import { OnboardingModal } from "./modals/Onboarding"; import { ModifyAccountModal } from "./modals/ModifyAccount"; -import { InputModal, SpecialInputModal } from "./modals/Input"; -import { PromptModal, SpecialPromptModal } from "./modals/Prompt"; export interface Props { screen: Screen; @@ -19,12 +19,8 @@ export default function Modals({ screen, openScreen }: Props) { switch (screen.id) { case "_prompt": return <PromptModal onClose={onClose} {...screen} />; - case "special_prompt": - return <SpecialPromptModal onClose={onClose} {...screen} />; case "_input": return <InputModal onClose={onClose} {...screen} />; - case "special_input": - return <SpecialInputModal onClose={onClose} {...screen} />; case "error": return <ErrorModal onClose={onClose} {...screen} />; case "signed_out": diff --git a/src/context/intermediate/Popovers.tsx b/src/context/intermediate/Popovers.tsx index 565d762..f91b02d 100644 --- a/src/context/intermediate/Popovers.tsx +++ b/src/context/intermediate/Popovers.tsx @@ -2,6 +2,8 @@ import { IntermediateContext, useIntermediate } from "./Intermediate"; import { useContext } from "preact/hooks"; import { UserPicker } from "./popovers/UserPicker"; +import { SpecialInputModal } from "./modals/Input"; +import { SpecialPromptModal } from "./modals/Prompt"; import { UserProfile } from "./popovers/UserProfile"; import { ImageViewer } from "./popovers/ImageViewer"; import { ChannelInfo } from "./popovers/ChannelInfo"; @@ -21,6 +23,10 @@ export default function Popovers() { return <ImageViewer {...screen} onClose={onClose} />; case "channel_info": return <ChannelInfo {...screen} onClose={onClose} />; + case "special_prompt": + return <SpecialPromptModal onClose={onClose} {...screen} />; + case "special_input": + return <SpecialInputModal onClose={onClose} {...screen} />; } return null; diff --git a/src/context/intermediate/modals/Input.tsx b/src/context/intermediate/modals/Input.tsx index 4aa68f9..3d28785 100644 --- a/src/context/intermediate/modals/Input.tsx +++ b/src/context/intermediate/modals/Input.tsx @@ -12,7 +12,7 @@ import { AppContext } from "../../revoltjs/RevoltClient"; interface Props { onClose: () => void; question: Children; - field: Children; + field?: Children; defaultValue?: string; callback: (value: string) => Promise<void>; } @@ -53,9 +53,9 @@ export function InputModal({ ]} onClose={onClose} > - <Overline error={error} block> + { field ? <Overline error={error} block> {field} - </Overline> + </Overline> : (error && <Overline error={error} type="error" block />) } <InputBox value={value} onChange={e => setValue(e.currentTarget.value)} @@ -65,7 +65,7 @@ export function InputModal({ } type SpecialProps = { onClose: () => void } & ( - { type: "create_group" | "create_server" | "set_custom_status" } | + { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } | { type: "create_channel", server: string } ) @@ -144,6 +144,15 @@ export function SpecialInputModal(props: SpecialProps) { } />; } + case "add_friend": { + return <InputModal + onClose={onClose} + question={"Add Friend"} + callback={username => + client.users.addFriend(username) + } + />; + } default: return null; } } diff --git a/src/context/intermediate/modals/Prompt.tsx b/src/context/intermediate/modals/Prompt.tsx index c5aedfa..645d395 100644 --- a/src/context/intermediate/modals/Prompt.tsx +++ b/src/context/intermediate/modals/Prompt.tsx @@ -1,7 +1,7 @@ import { Text } from "preact-i18n"; import styles from './Prompt.module.scss'; import { Children } from "../../../types/Preact"; -import { IntermediateContext, useIntermediate } from "../Intermediate"; +import { useIntermediate } from "../Intermediate"; import InputBox from "../../../components/ui/InputBox"; import Overline from "../../../components/ui/Overline"; import UserIcon from "../../../components/common/UserIcon"; @@ -82,7 +82,8 @@ export function SpecialPromptModal(props: SpecialProps) { actions={[ { confirmation: true, - style: 'contrast-error', + contrast: true, + error: true, text: <Text id="app.special.modals.actions.delete" />, onClick: async () => { setProcessing(true); @@ -162,7 +163,8 @@ export function SpecialPromptModal(props: SpecialProps) { actions={[ { text: <Text id="app.special.modals.actions.kick" />, - style: 'contrast-error', + contrast: true, + error: true, confirmation: true, onClick: async () => { setProcessing(true); @@ -200,7 +202,8 @@ export function SpecialPromptModal(props: SpecialProps) { actions={[ { text: <Text id="app.special.modals.actions.ban" />, - style: 'contrast-error', + contrast: true, + error: true, confirmation: true, onClick: async () => { setProcessing(true); diff --git a/src/lib/stopPropagation.ts b/src/lib/stopPropagation.ts new file mode 100644 index 0000000..98c9fa6 --- /dev/null +++ b/src/lib/stopPropagation.ts @@ -0,0 +1,4 @@ +export const stopPropagation = (ev: JSX.TargetedMouseEvent<HTMLDivElement>, _consume?: any) => { + ev.preventDefault(); + ev.stopPropagation(); +}; diff --git a/src/pages/App.tsx b/src/pages/App.tsx index ebe37fe..4648649 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -1,12 +1,22 @@ import { Docked, OverlappingPanels } from "react-overlapping-panels"; import { isTouchscreenDevice } from "../lib/isTouchscreenDevice"; +import Popovers from "../context/intermediate/Popovers"; import { Switch, Route } from "react-router-dom"; +import styled from "styled-components"; import LeftSidebar from "../components/navigation/LeftSidebar"; import RightSidebar from "../components/navigation/RightSidebar"; import Home from './home/Home'; -import Popovers from "../context/intermediate/Popovers"; +import Friends from "./friends/Friends"; + +const Routes = styled.div` + min-width: 0; + display: flex; + overflow: hidden; + flex-direction: column; + background: var(--primary-background); +`; export default function App() { return ( @@ -16,12 +26,76 @@ export default function App() { leftPanel={{ width: 292, component: <LeftSidebar /> }} rightPanel={{ width: 240, component: <RightSidebar /> }} docked={isTouchscreenDevice ? Docked.None : Docked.Left}> - <Switch> - <Route path="/"> - <Home /> - </Route> - </Switch> + <Routes> + <Switch> + <Route path="/friends"> + <Friends /> + </Route> + + <Route path="/"> + <Home /> + </Route> + </Switch> + </Routes> <Popovers /> </OverlappingPanels> ); }; + +/** + * + * <Route path="/channel/:channel/message/:message"> + <ChannelWrapper /> + </Route> + <Route path="/server/:server/channel/:channel/settings/:page"> + <ChannelSettings key="channel_settings" /> + </Route> + <Route path="/server/:server/channel/:channel/settings"> + <ChannelSettings key="channel_settings" /> + </Route> + <Route path="/server/:server/settings/:page"> + <ServerSettings key="channel_settings" /> + </Route> + <Route path="/server/:server/settings"> + <ServerSettings key="channel_settings" /> + </Route> + <Route path="/channel/:channel/settings/:page"> + <ChannelSettings key="channel_settings" /> + </Route> + <Route path="/channel/:channel/settings"> + <ChannelSettings key="channel_settings" /> + </Route> + + <Route path="/settings/:page"> + <Settings key="settings" /> + </Route> + <Route path="/settings"> + <Settings key="settings" /> + </Route> + + <Route path="/server/:server/channel/:channel"> + <ChannelWrapper /> + </Route> + <Route path="/server/:server" /> + <Route path="/channel/:channel"> + <ChannelWrapper /> + </Route> + + <Route path="/friends"> + <Friends /> + </Route> + <Route path="/dev"> + <Developer /> + </Route> + + <Route path="/open/:id"> + <Open /> + </Route> + {/*<Route path="/invite/:code"> + <OpenInvite /> + </Route> + + <Route path="/"> + <Home /> + </Route> + */ diff --git a/src/pages/friends/Friend.module.scss b/src/pages/friends/Friend.module.scss new file mode 100644 index 0000000..b9e90c0 --- /dev/null +++ b/src/pages/friends/Friend.module.scss @@ -0,0 +1,71 @@ +.list { + padding: 16px; + user-select: none; + overflow-y: scroll; + + &[data-empty="true"] { + img { + height: 120px; + border-radius: 8px; + } + + gap: 16px; + height: 100%; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + } +} + +.friend { + padding: 10px; + display: flex; + border-radius: 5px; + align-items: center; + flex-direction: row; + cursor: pointer; + + &:hover { + background: var(--secondary-background); + + :global(.button) { + background-color: var(--primary-background); + } + } + + .name { + flex-grow: 1; + margin: 0 12px; + font-size: 16px; + + display: flex; + flex-direction: column; + justify-content: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .subtext { + font-size: 12px; + color: var(--tertiary-foreground); + } + } + + .actions { + display: flex; + gap: 12px; + + > div { + height: 32px; + width: 32px; + } + } +} + +//! FIXME: Move this to the Header component, do this: +// 1. Check if header has topic, if yes, flex-grow: 0 on the title. +// 2. If header has no topic (example: friends page), flex-grow 1 on the header title. +.title { + flex-grow: 1; +} diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx new file mode 100644 index 0000000..d27ee85 --- /dev/null +++ b/src/pages/friends/Friend.tsx @@ -0,0 +1,90 @@ +import { Text } from "preact-i18n"; +import { Link } from "react-router-dom"; +import styles from "./Friend.module.scss"; +import { useContext } from "preact/hooks"; +import { Children } from "../../types/Preact"; +import { X, Plus, Mail } from "@styled-icons/feather"; +import UserIcon from "../../components/common/UserIcon"; +import IconButton from "../../components/ui/IconButton"; +import { attachContextMenu } from "preact-context-menu"; +import { User, Users } from "revolt.js/dist/api/objects"; +import UserStatus from '../../components/common/UserStatus'; +import { stopPropagation } from "../../lib/stopPropagation"; +import { AppContext } from "../../context/revoltjs/RevoltClient"; +import { useIntermediate } from "../../context/intermediate/Intermediate"; + +interface Props { + user: User; +} + +export function Friend({ user }: Props) { + const client = useContext(AppContext); + const { openScreen } = useIntermediate(); + + const actions: Children[] = []; + let subtext: Children = null; + + if (user.relationship === Users.Relationship.Friend) { + subtext = <UserStatus user={user} /> + actions.push( + <IconButton type="circle" + onClick={stopPropagation}> + <Link to={'/open/' + user._id}> + <Mail size={20} /> + </Link> + </IconButton> + ); + } + + if (user.relationship === Users.Relationship.Incoming) { + actions.push( + <IconButton type="circle" + onClick={ev => stopPropagation(ev, client.users.addFriend(user.username))}> + <Plus size={24} /> + </IconButton> + ); + + subtext = <Text id="app.special.friends.incoming" />; + } + + if (user.relationship === Users.Relationship.Outgoing) { + subtext = <Text id="app.special.friends.outgoing" />; + } + + if ( + user.relationship === Users.Relationship.Friend || + user.relationship === Users.Relationship.Outgoing || + user.relationship === Users.Relationship.Incoming + ) { + actions.push( + <IconButton type="circle" + onClick={ev => stopPropagation(ev, client.users.removeFriend(user._id))}> + <X size={24} /> + </IconButton> + ); + } + + if (user.relationship === Users.Relationship.Blocked) { + actions.push( + <IconButton type="circle" + onClick={ev => stopPropagation(ev, client.users.unblockUser(user._id))}> + <X size={24} /> + </IconButton> + ); + } + + return ( + <div className={styles.friend} + onClick={() => openScreen({ id: 'profile', user_id: user._id })} + onContextMenu={attachContextMenu('Menu', { user: user._id })}> + <UserIcon target={user} size={32} status /> + <div className={styles.name}> + <span>@{user.username}</span> + {subtext && ( + <span className={styles.subtext}>{subtext}</span> + )} + </div> + <div className={styles.actions}>{actions}</div> + </div> + ); +} diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx new file mode 100644 index 0000000..9460f8e --- /dev/null +++ b/src/pages/friends/Friends.tsx @@ -0,0 +1,85 @@ +import styles from "./Friend.module.scss"; +import { UserPlus } from "@styled-icons/feather"; + +import { Friend } from "./Friend"; +import { Text } from "preact-i18n"; +import Header from "../../components/ui/Header"; +import Overline from "../../components/ui/Overline"; +import IconButton from "../../components/ui/IconButton"; +import { useUsers } from "../../context/revoltjs/hooks"; +import { User, Users } from "revolt.js/dist/api/objects"; +import { useIntermediate } from "../../context/intermediate/Intermediate"; + +export default function Friends() { + const { openScreen } = useIntermediate(); + + const users = useUsers() as User[]; + users.sort((a, b) => a.username.localeCompare(b.username)); + + const pending = users.filter( + x => + x.relationship === Users.Relationship.Incoming || + x.relationship === Users.Relationship.Outgoing + ); + const friends = users.filter( + x => x.relationship === Users.Relationship.Friend + ); + const blocked = users.filter( + x => x.relationship === Users.Relationship.Blocked + ); + + return ( + <> + <Header placement="primary"> + <div className={styles.title}> + <Text id="app.navigation.tabs.friends" /> + </div> + <div className="actions"> + <IconButton onClick={() => openScreen({ id: 'special_input', type: 'add_friend' })}> + <UserPlus size={24} /> + </IconButton> + </div> + </Header> + <div + className={styles.list} + data-empty={ + pending.length + friends.length + blocked.length === 0 + } + > + {pending.length + friends.length + blocked.length === 0 && ( + <> + <img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" /> + <Text id="app.special.friends.nobody" /> + </> + )} + {pending.length > 0 && ( + <Overline type="subtle"> + <Text id="app.special.friends.pending" /> –{" "} + {pending.length} + </Overline> + )} + {pending.map(y => ( + <Friend key={y._id} user={y} /> + ))} + {friends.length > 0 && ( + <Overline type="subtle"> + <Text id="app.navigation.tabs.friends" /> –{" "} + {friends.length} + </Overline> + )} + {friends.map(y => ( + <Friend key={y._id} user={y} /> + ))} + {blocked.length > 0 && ( + <Overline type="subtle"> + <Text id="app.special.friends.blocked" /> –{" "} + {blocked.length} + </Overline> + )} + {blocked.map(y => ( + <Friend key={y._id} user={y} /> + ))} + </div> + </> + ); +} diff --git a/src/styles/_elements.scss b/src/styles/_elements.scss index d0e6524..f019eff 100644 --- a/src/styles/_elements.scss +++ b/src/styles/_elements.scss @@ -1,3 +1,8 @@ +:disabled { + opacity: 0.5; + pointer-events: none; +} + ::-webkit-scrollbar { width: 3px; height: 3px; diff --git a/src/types/Preact.ts b/src/types/Preact.ts index 80ac489..dbbea16 100644 --- a/src/types/Preact.ts +++ b/src/types/Preact.ts @@ -1,4 +1,4 @@ import { VNode } from "preact"; -export type Child = VNode | string | false | undefined; +export type Child = VNode | string | number | boolean | undefined | null; export type Children = Child | Child[] | Children[]; diff --git a/yarn.lock b/yarn.lock index 16f513a..effb671 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3038,6 +3038,13 @@ postcss@^8.3.0: nanoid "^3.1.23" source-map-js "^0.6.2" +preact-context-menu@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/preact-context-menu/-/preact-context-menu-0.1.5.tgz#51d13b0eceed8bd53493f2bdbce36e7c74ff8575" + integrity sha512-cQxcOf4w8MZAQtLpQeX80TQ2TzSKD/z6rnV+654xiHzqQp2pSV+qE0IEZLkUNz88ZmtpIy6GnB/6pCnCGjS4Ww== + dependencies: + preact "^10.4.6" + preact-i18n@^2.4.0-preactx: version "2.4.0-preactx" resolved "https://registry.yarnpkg.com/preact-i18n/-/preact-i18n-2.4.0-preactx.tgz#fbcb2e3ae22744c7fef5a102db2ef7506057d082" @@ -3051,7 +3058,7 @@ preact-markup@^2.0.0: resolved "https://registry.yarnpkg.com/preact-markup/-/preact-markup-2.1.1.tgz#0451e7eed1dac732d7194c34a7f16ff45a2cfdd7" integrity sha512-8JL2p36mzK8XkspOyhBxUSPjYwMxDM0L5BWBZWxsZMVW8WsGQrYQDgVuDKkRspt2hwrle+Cxr/053hpc9BJwfw== -preact@^10.0.0, preact@^10.5.13: +preact@^10.0.0, preact@^10.4.6, preact@^10.5.13: version "10.5.13" resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.13.tgz#85f6c9197ecd736ce8e3bec044d08fd1330fa019" integrity sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ== -- GitLab