Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
......@@ -16,6 +16,10 @@
background: var(--scrollbar-thumb);
}
::-webkit-scrollbar-corner {
background: transparent;
}
::selection {
background: var(--accent);
color: var(--foreground);
......@@ -44,3 +48,7 @@ hr {
height: 1px;
flex-grow: 1;
}
foreignObject > svg {
vertical-align: top !important;
}
......@@ -7,13 +7,20 @@
--font: "Open Sans";
--codeblock-font: "Fira Code";
--sidebar-active: var(--secondary-background);
/**
* Native
*/
--titlebar-height: 29px;
--titlebar-action-padding: 8px;
--titlebar-logo-color: var(--secondary-foreground);
/**
* Layout
*/
--app-height: 100vh;
--border-radius: 6px;
--input-border-width: 2px;
--textarea-padding: 16px;
--textarea-line-height: 20px;
......@@ -29,6 +36,13 @@
/**
* Experimental
*/
--background-rgb: (25,25,25); //THIS IS SO THAT WE CAN HAVE CUSTOM BACKGROUNDS FOR THE CLIENT, CONVERTS THE HEX TO AN RGB VALUE FROM --background
--background-rgba: rgba(var(--background-rgb), .8); //make the opacity also customizable
--background-rgb: (
25,
25,
25
); //THIS IS SO THAT WE CAN HAVE CUSTOM BACKGROUNDS FOR THE CLIENT, CONVERTS THE HEX TO AN RGB VALUE FROM --background
--background-rgba: rgba(
var(--background-rgb),
0.8
); //make the opacity also customizable
}
/// <reference lib="webworker" />
import { IDBPDatabase, openDB } from "idb";
import { getItem } from "localforage";
import { Channel, Message, User } from "revolt.js";
import { Server } from "revolt.js/dist/api/objects";
import { precacheAndRoute } from "workbox-precaching";
import type { State } from "./redux";
import {
getNotificationState,
shouldNotify,
} from "./redux/reducers/notifications";
declare let self: ServiceWorkerGlobalScope;
self.addEventListener("message", (event) => {
......@@ -19,130 +9,10 @@ self.addEventListener("message", (event) => {
precacheAndRoute(self.__WB_MANIFEST);
// ulid decodeTime(id: string)
// since crypto is not available in sw.js
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
const ENCODING_LEN = ENCODING.length;
const TIME_LEN = 10;
function decodeTime(id: string) {
const time = id
.substr(0, TIME_LEN)
.split("")
.reverse()
.reduce((carry, char, index) => {
const encodingIndex = ENCODING.indexOf(char);
if (encodingIndex === -1) throw `invalid character found: ${char}`;
return (carry += encodingIndex * Math.pow(ENCODING_LEN, index));
}, 0);
return time;
}
self.addEventListener("push", (event) => {
async function process() {
if (event.data === null) return;
const data: Message = event.data.json();
const item = await localStorage.getItem("state");
if (!item) return;
const state: State = JSON.parse(item);
const autumn_url = state.config.features.autumn.url;
const user_id = state.auth.active!;
let db: IDBPDatabase;
try {
// Match RevoltClient.tsx#L55
db = await openDB("state", 3, {
upgrade(db) {
for (const store of [
"channels",
"servers",
"users",
"members",
]) {
db.createObjectStore(store, {
keyPath: "_id",
});
}
},
});
} catch (err) {
console.error(
"Failed to open IndexedDB store, continuing without.",
);
return;
}
async function get<T>(
store: string,
key: string,
): Promise<T | undefined> {
try {
return await db.get(store, key);
} catch (err) {
return undefined;
}
}
const channel = await get<Channel>("channels", data.channel);
const user = await get<User>("users", data.author);
if (channel) {
const notifs = getNotificationState(state.notifications, channel);
if (!shouldNotify(notifs, data, user_id)) return;
}
let title = `@${data.author}`;
const username = user?.username ?? data.author;
let image;
if (data.attachments) {
const attachment = data.attachments[0];
if (attachment.metadata.type === "Image") {
image = `${autumn_url}/${attachment.tag}/${attachment._id}`;
}
}
switch (channel?.channel_type) {
case "SavedMessages":
break;
case "DirectMessage":
title = `@${username}`;
break;
case "Group":
if (user?._id === "00000000000000000000000000") {
title = channel.name;
} else {
title = `@${user?.username} - ${channel.name}`;
}
break;
case "TextChannel":
{
const server = await get<Server>("servers", channel.server);
title = `@${user?.username} (#${channel.name}, ${server?.name})`;
}
break;
}
await self.registration.showNotification(title, {
icon: user?.avatar
? `${autumn_url}/${user.avatar.tag}/${user.avatar._id}`
: `https://api.revolt.chat/users/${data.author}/default_avatar`,
image,
body:
typeof data.content === "string"
? data.content
: JSON.stringify(data.content),
timestamp: decodeTime(data._id),
tag: data.channel,
badge: "https://app.revolt.chat/assets/icons/mono-48x48.png",
data:
channel?.channel_type === "TextChannel"
? `/server/${channel.server}/channel/${channel._id}`
: `/channel/${data.channel}`,
});
// Need to write notification generator on server.
}
event.waitUntil(process());
......
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"types": [
"vite-plugin-pwa/client"
]
},
"include": ["src", "ui/ui.tsx"]
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"types": ["vite-plugin-pwa/client"],
"experimentalDecorators": true
},
"include": ["src", "ui/ui.tsx"]
}
import { useState } from 'preact/hooks';
import styled from 'styled-components';
import '../src/styles/index.scss'
import { render } from 'preact'
import styled from "styled-components";
import Theme from '../src/context/Theme';
import "../src/styles/index.scss";
import { render } from "preact";
import { useState } from "preact/hooks";
import Theme from "../src/context/Theme";
import Banner from "../src/components/ui/Banner";
import Button from "../src/components/ui/Button";
import Checkbox from "../src/components/ui/Checkbox";
import ColourSwatches from "../src/components/ui/ColourSwatches";
import ComboBox from "../src/components/ui/ComboBox";
import InputBox from "../src/components/ui/InputBox";
import Overline from "../src/components/ui/Overline";
import Radio from "../src/components/ui/Radio";
import Tip from "../src/components/ui/Tip";
export const UIDemo = styled.div`
gap: 12px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: flex-start;
`;
import Button from '../src/components/ui/Button';
import Banner from '../src/components/ui/Banner';
import Checkbox from '../src/components/ui/Checkbox';
import ComboBox from '../src/components/ui/ComboBox';
import InputBox from '../src/components/ui/InputBox';
import ColourSwatches from '../src/components/ui/ColourSwatches';
import Tip from '../src/components/ui/Tip';
import Radio from '../src/components/ui/Radio';
import Overline from '../src/components/ui/Overline';
export function UI() {
let [checked, setChecked] = useState(false);
let [colour, setColour] = useState('#FD6671');
let [selected, setSelected] = useState<'a' | 'b' | 'c'>('a');
let [checked, setChecked] = useState(false);
let [colour, setColour] = useState("#FD6671");
let [selected, setSelected] = useState<"a" | "b" | "c">("a");
return (
<>
<Button>Button (normal)</Button>
<Button contrast>Button (contrast)</Button>
<Button error>Button (error)</Button>
<Button contrast error>Button (contrast + error)</Button>
<Button contrast error>
Button (contrast + error)
</Button>
<Banner>I am a banner!</Banner>
<Checkbox checked={checked} onChange={setChecked} description="ok gamer">Do you want thing??</Checkbox>
<Checkbox
checked={checked}
onChange={setChecked}
description="ok gamer">
Do you want thing??
</Checkbox>
<ComboBox>
<option>Select an option.</option>
<option>1</option>
......@@ -46,24 +54,35 @@ export function UI() {
<InputBox placeholder="Contrast input box..." contrast />
<InputBox value="Input box with value" />
<InputBox value="Contrast with value" contrast />
<ColourSwatches value={colour} onChange={v => setColour(v)} />
<Tip>I am a tip! I provide valuable information.</Tip>
<Radio checked={selected === 'a'} onSelect={() => setSelected('a')}>First option</Radio>
<Radio checked={selected === 'b'} onSelect={() => setSelected('b')}>Second option</Radio>
<Radio checked={selected === 'c'} onSelect={() => setSelected('c')}>Last option</Radio>
<ColourSwatches value={colour} onChange={(v) => setColour(v)} />
<Tip hideSeparator>I am a tip! I provide valuable information.</Tip>
<Radio checked={selected === "a"} onSelect={() => setSelected("a")}>
First option
</Radio>
<Radio checked={selected === "b"} onSelect={() => setSelected("b")}>
Second option
</Radio>
<Radio checked={selected === "c"} onSelect={() => setSelected("c")}>
Last option
</Radio>
<Overline>Normal overline</Overline>
<Overline type="subtle">Subtle overline</Overline>
<Overline type="error">Error overline</Overline>
<Overline error="with error">Normal overline</Overline>
<Overline type="subtle" error="with error">Subtle overline</Overline>
<Overline type="subtle" error="with error">
Subtle overline
</Overline>
</>
)
);
}
render(<>
<Theme>
<UIDemo>
<UI />
</UIDemo>
</Theme>
</>, document.getElementById('app')!)
render(
<>
<Theme>
<UIDemo>
<UI />
</UIDemo>
</Theme>
</>,
document.getElementById("app")!,
);
import { resolve } from 'path'
import { readFileSync } from 'fs'
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
import { VitePWA } from 'vite-plugin-pwa'
import replace from '@rollup/plugin-replace'
import replace from "@rollup/plugin-replace";
import { readFileSync } from "fs";
import { resolve } from "path";
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import preact from "@preact/preset-vite";
function getGitRevision() {
try {
const rev = readFileSync('.git/HEAD').toString().trim();
if (rev.indexOf(':') === -1) {
return rev;
} else {
return readFileSync('.git/' + rev.substring(5)).toString().trim();
try {
const rev = readFileSync(".git/HEAD").toString().trim();
if (rev.indexOf(":") === -1) {
return rev;
} else {
return readFileSync(".git/" + rev.substring(5))
.toString()
.trim();
}
} catch (err) {
console.error("Failed to get Git revision.");
return "?";
}
} catch (err) {
console.error('Failed to get Git revision.');
return '?';
}
}
function getGitBranch() {
try {
const rev = readFileSync('.git/HEAD').toString().trim();
if (rev.indexOf(':') === -1) {
return 'DETACHED';
} else {
return rev.split('/').pop();
try {
const rev = readFileSync(".git/HEAD").toString().trim();
if (rev.indexOf(":") === -1) {
return "DETACHED";
} else {
return rev.split("/").pop();
}
} catch (err) {
console.error("Failed to get Git branch.");
return "?";
}
} catch (err) {
console.error('Failed to get Git branch.');
return '?';
}
}
function getVersion() {
return readFileSync('VERSION').toString();
return readFileSync("VERSION").toString();
}
const branch = getGitBranch();
const isNightly = false;//branch !== 'production';
const iconPrefix = isNightly ? 'nightly-' : '';
const isNightly = false; //branch !== 'production';
const iconPrefix = isNightly ? "nightly-" : "";
export default defineConfig({
plugins: [
preact(),
VitePWA({
srcDir: 'src',
filename: 'sw.ts',
strategies: 'injectManifest',
manifest: {
name: isNightly ? "Revolt Nightly" : "Revolt",
short_name: "Revolt",
description: isNightly ? "Early preview builds of Revolt." : "User-first, privacy-focused chat platform.",
categories: ["messaging"],
start_url: "/",
orientation: "any",
display: "standalone",
background_color: "#101823",
theme_color: "#101823",
icons: [
{
"src": `/assets/icons/${iconPrefix}android-chrome-192x192.png`,
"type": "image/png",
"sizes": "192x192"
},
{
"src": `/assets/icons/${iconPrefix}android-chrome-512x512.png`,
"type": "image/png",
"sizes": "512x512"
plugins: [
preact(),
VitePWA({
srcDir: "src",
filename: "sw.ts",
strategies: "injectManifest",
manifest: {
name: isNightly ? "Revolt Nightly" : "Revolt",
short_name: "Revolt",
description: isNightly
? "Early preview builds of Revolt."
: "User-first, privacy-focused chat platform.",
categories: ["messaging"],
start_url: "/",
orientation: "portrait",
display: "standalone",
background_color: "#101823",
theme_color: "#101823",
icons: [
{
src: `/assets/icons/${iconPrefix}android-chrome-192x192.png`,
type: "image/png",
sizes: "192x192",
},
{
src: `/assets/icons/${iconPrefix}android-chrome-512x512.png`,
type: "image/png",
sizes: "512x512",
},
{
src: `/assets/icons/monochrome.svg`,
type: "image/svg+xml",
sizes: "48x48 72x72 96x96 128x128 256x256",
purpose: "monochrome",
},
{
src: `/assets/icons/masking-512x512.png`,
type: "image/png",
sizes: "512x512",
purpose: "maskable",
},
],
},
{
"src": `/assets/icons/monochrome.svg`,
"type": "image/svg+xml",
"sizes": "48x48 72x72 96x96 128x128 256x256",
"purpose": "monochrome"
}),
replace({
__GIT_REVISION__: getGitRevision(),
__GIT_BRANCH__: getGitBranch(),
__APP_VERSION__: getVersion(),
preventAssignment: true,
}),
],
build: {
sourcemap: true,
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
ui: resolve(__dirname, "ui/index.html"),
},
{
"src": `/assets/icons/masking-512x512.png`,
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
]
}
}),
replace({
__GIT_REVISION__: getGitRevision(),
__GIT_BRANCH__: getGitBranch(),
__APP_VERSION__: getVersion(),
preventAssignment: true
})
],
build: {
sourcemap: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
ui: resolve(__dirname, 'ui/index.html')
}
}
}
})
},
},
});
......@@ -879,6 +879,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.14.8":
version "7.14.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.12.13", "@babel/template@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
......@@ -1114,14 +1121,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
"@insertish/mutable@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@insertish/mutable/-/mutable-1.1.0.tgz#06f95f855691ccb69ee3c339887a80bcd1498116"
integrity sha512-NH7aCGFAKRE1gFprrW/HsJoWCWQy18TZBarxLdeLVWdLFvkb2lD6Z5B70oOoUHFNpykiTC8IcRonsd9Xn13n8Q==
dependencies:
eventemitter3 "^4.0.7"
lodash.isequal "^4.5.0"
"@mdn/browser-compat-data@^2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-2.0.7.tgz#72ec37b9c1e00ce0b4e0309d753be18e2da12ee3"
......@@ -1252,12 +1251,12 @@
"@babel/runtime" "^7.14.0"
"@styled-icons/styled-icon" "^10.6.3"
"@styled-icons/boxicons-solid@^10.34.0":
version "10.34.0"
resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-solid/-/boxicons-solid-10.34.0.tgz#4f31b873a1d52c85230f86eb7106c04f416a12c9"
integrity sha512-0DTsuysRgIO/XoSq5sFPeknnidLzhTEmaG5uJuLmCPBw78VjxNt6DQFRcn8ytn+ba5Qhu+Ps3Km8nbY7Zf14/g==
"@styled-icons/boxicons-solid@^10.37.0":
version "10.37.0"
resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-solid/-/boxicons-solid-10.37.0.tgz#49e3ec72b560967f1ba12c433f28270d8a74e9b9"
integrity sha512-u6/urwIWesGArSHW98TFHnzMduInQYkhWE1LdNCWkmzs0CvQ0Xmmvnl1Lz9LnAHatQR+UFpKLz2y08Fhu7zBgQ==
dependencies:
"@babel/runtime" "^7.14.0"
"@babel/runtime" "^7.14.8"
"@styled-icons/styled-icon" "^10.6.3"
"@styled-icons/simple-icons@^10.33.0":
......@@ -2614,11 +2613,6 @@ hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-
dependencies:
react-is "^16.7.0"
idb@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.2.tgz#82ef5c951b8e1f47875d36ccafa4bedafc62f2f1"
integrity sha512-1DNDVu3yDhAZkFDlJf0t7r+GLZ248F5pTAtA7V0oVG3yjmV125qZOx3g0XpAEkGZVYQiFDAsSOnGet2bhugc3w==
ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
......@@ -3077,6 +3071,16 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mobx-react-lite@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz#331d7365a6b053378dfe9c087315b4e41c5df69f"
integrity sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==
mobx@^6.3.2:
version "6.3.2"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.2.tgz#125590961f702a572c139ab69392bea416d2e51b"
integrity sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
......@@ -3288,7 +3292,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.4.6, preact@^10.5.13:
preact@^10.0.0, preact@^10.4.6, preact@^10.5.14:
version "10.5.14"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.14.tgz#0b14a2eefba3c10a57116b90d1a65f5f00cd2701"
integrity sha512-KojoltCrshZ099ksUZ2OQKfbH66uquFoxHSbnwKbTJHeQNvx42EmC7wQVWNuDt6vC5s3nudRHFtKbpY4ijKlaQ==
......@@ -3563,17 +3567,23 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
revolt.js@4.3.3-alpha.14:
version "4.3.3-alpha.14"
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-4.3.3-alpha.14.tgz#f4912ee25725de6e43ba1d4fd8ab84c711678271"
integrity sha512-4p3DhEu+GUKZxczCPXR2JM04fzGlFfZdwHYjgkgU48NgPXgzxQrSv4x0FjpyIIv3xNpuO59z35mYRMLxAnBXsQ==
revolt-api@0.5.1-alpha.10-patch.0:
version "0.5.1-alpha.10-patch.0"
resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.1-alpha.10-patch.0.tgz#97d31bec7dfa4573567097443acb059c4feaac20"
integrity sha512-UyM890HkGlYNQOxpHuEpUsJHLt8Ujnjg9/zPEDGpbvS4iy0jmHX23Hh8tOCfb/ewxbNrtT3G1HpSWKOneW/vYg==
revolt.js@5.0.0-alpha.18:
version "5.0.0-alpha.18"
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.0.0-alpha.18.tgz#fffd63a4f4f93a4a6422de6a68c1ba3f3f9b55e5"
integrity sha512-NVd00P4CYLVJf1AuYwo65mPeLaST/RdU7dMLFwCZPvQAHvyPwTfLekCc9dfKPT4BkS2sjF8Vxi2xUFpMRMZYfw==
dependencies:
"@insertish/mutable" "1.1.0"
axios "^0.19.2"
eventemitter3 "^4.0.7"
exponential-backoff "^3.1.0"
isomorphic-ws "^4.0.1"
lodash.defaultsdeep "^4.6.1"
lodash.isequal "^4.5.0"
mobx "^6.3.2"
tsc-watch "^4.1.0"
ulid "^2.3.0"
ws "^7.2.1"
......