not so initial commit

This commit is contained in:
2026-01-11 03:50:03 +01:00
commit a2016e2b0e
41 changed files with 54638 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
db
node_modules

36
build.ts Normal file
View File

@@ -0,0 +1,36 @@
import { readdirSync, copyFileSync, mkdirSync, watch } from "fs";
import { join } from "path";
function copyAllFiles(srcDir: string, destDir: string) {
const entries = readdirSync(srcDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(srcDir, entry.name);
const destPath = join(destDir, entry.name);
if (entry.isDirectory()) {
mkdirSync(destPath, { recursive: true });
copyAllFiles(srcPath, destPath); // recursive
} else if (entry.isFile()) {
copyFileSync(srcPath, destPath);
}
}
}
function build() {
Bun.build({
entrypoints: ["src/index.tsx"],
minify: false,
sourcemap: true,
outdir: "dist/",
})
copyAllFiles("./public", "./dist");
}
const watcher = watch(import.meta.dir + "\\src", {recursive: true}, (event, filename) => {
console.log(`detected ${event} in ${filename}`);
build();
});
console.log("initial build");
build();

61
bun.lock Normal file
View File

@@ -0,0 +1,61 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"react": "^19.2.3",
"react-dom": "^19.2.3",
"slate": "^0.120.0",
"slate-react": "^0.120.0",
},
"devDependencies": {
"@types/bun": "^1.3.5",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
},
},
},
"packages": {
"@juggle/resize-observer": ["@juggle/resize-observer@3.4.0", "", {}, "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="],
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"direction": ["direction@1.0.4", "", { "bin": { "direction": "cli.js" } }, "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ=="],
"is-hotkey": ["is-hotkey@0.2.0", "", {}, "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="],
"is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
"slate": ["slate@0.120.0", "", {}, "sha512-CXK/DADGgMZb4z9RTtXylzIDOxvmNJEF9bXV2bAGkLWhQ3rm7GORY9q0H/W41YJvAGZsLbH7nnrhMYr550hWDQ=="],
"slate-dom": ["slate-dom@0.119.0", "", { "dependencies": { "@juggle/resize-observer": "^3.4.0", "direction": "^1.0.4", "is-hotkey": "^0.2.0", "is-plain-object": "^5.0.0", "lodash": "^4.17.21", "scroll-into-view-if-needed": "^3.1.0", "tiny-invariant": "1.3.1" }, "peerDependencies": { "slate": ">=0.99.0" } }, "sha512-foc8a2NkE+1SldDIYaoqjhVKupt8RSuvHI868rfYOcypD4we5TT7qunjRKJ852EIRh/Ql8sSTepXgXKOUJnt1w=="],
"slate-react": ["slate-react@0.120.0", "", { "dependencies": { "@juggle/resize-observer": "^3.4.0", "direction": "^1.0.4", "is-hotkey": "^0.2.0", "lodash": "^4.17.21", "scroll-into-view-if-needed": "^3.1.0", "tiny-invariant": "1.3.1" }, "peerDependencies": { "react": ">=18.2.0", "react-dom": ">=18.2.0", "slate": ">=0.114.0", "slate-dom": ">=0.119.0" } }, "sha512-CMEJzozriddBjVmbxNvc2erCkXUuEkgdXIdM+jEMvxWMb51z0zhIVzgoxbGprVpzwBXY8Kv7aZOUDVMomzWH/g=="],
"tiny-invariant": ["tiny-invariant@1.3.1", "", {}, "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

209
dist/index.css vendored Normal file
View File

@@ -0,0 +1,209 @@
/* src/style.css */
:root {
--brand-color: #a200ff;
}
* {
margin: 0;
}
html {
color: #fff;
background-color: #333;
font-family: Arial, Helvetica, sans-serif;
}
.mentions {
color: #fff;
display: flex;
background-color: #ff1717;
border-radius: 50%;
justify-content: center;
align-items: center;
width: 15px;
height: 15px;
padding: 2px;
font-size: 14px;
}
.channel-bar {
position: absolute;
width: 200px;
padding: 5px;
right: calc(100% - 250px);
}
.pillWrapper {
contain: layout size;
display: flex;
overflow: hidden;
position: relative;
justify-content: flex-start;
align-items: center;
width: 8px;
height: 100%;
}
.pill {
opacity: 0;
background-color: #fff;
border-radius: 0 4px 4px 0;
width: 8px;
height: 0;
transition: height .2s, translate .3s, opacity .1s;
}
.pill.unread {
opacity: 1;
height: 8px;
}
.pill.hover {
opacity: 1;
height: 20px;
}
.pill.open {
opacity: 1;
height: 40px;
}
.channel .mentions {
position: relative;
bottom: 27px;
left: 85%;
}
.channel .pillWrapper {
position: relative;
height: 17px;
top: 9px;
right: 2px;
}
.channel .channelname {
position: relative;
top: -10px;
left: 10px;
}
.server-bar {
position: absolute;
width: 50px;
left: 0;
}
.guild {
width: 40px;
height: 40px;
font-size: 12px;
}
.guild .guildname {
position: relative;
top: -20px;
right: -10px;
}
.guild .pillWrapper {
position: relative;
top: 9px;
right: 2px;
}
.messages {
overflow: hidden scroll;
position: absolute;
width: calc(100% - 250px);
height: calc(100% - 44px);
padding: 5px;
right: 0;
}
.message {
position: relative;
margin-bottom: 2px;
margin-left: 50px;
}
.message.bottom {
margin-bottom: 15px;
}
.message.sending {
color: #777;
}
.message.mentionsyou {
background-color: #b87d00;
}
.message .messagePfp {
position: absolute;
cursor: pointer;
border-radius: 100%;
left: -50px;
}
.message .messagePfp:active {
top: 1px;
}
.message .messageAuthor span {
cursor: pointer;
max-width: -moz-fit-content;
max-width: fit-content;
font-weight: 600;
}
.message .messageAuthor span:hover {
text-decoration: underline;
}
.message .messageAuthor .messageTimestamp {
font-weight: initial;
color: #777;
display: inline;
font-size: 10px;
}
.message .messageContent {
position: relative;
top: -1px;
}
.message .mention {
display: inline;
cursor: pointer;
background-color: #0099ff54;
border-radius: 3px;
padding: 2px;
}
.message .mention:hover {
background-color: #0099ff79;
}
.message .badge {
background-color: var(--badge-color);
display: inline;
border-radius: 10%;
max-width: -moz-fit-content;
max-width: fit-content;
padding: 2px;
font-size: 8pt;
}
.textbox {
position: absolute;
background-color: #222;
width: calc(100% - 250px);
padding: 3px;
bottom: 5px;
right: 5px;
}
.editor:empty:before {
content: attr(data-text);
color: gray;
}

10
dist/index.html vendored Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>dicor</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<script src="index.js"></script>
</body>
</html>

17595
dist/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

27
dist/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

7
dist/pfps/dicor.htm vendored Normal file
View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html><head>
<meta http-equiv="content-type" content="text/html; charset=windows-1252">
<title>dicor</title>
<link rel="stylesheet" type="text/css" href="dicor_files/index.css">
</head>
<body><script src="dicor_files/index.js"></script><div><div class="server-bar"><div class="guild"><div class="pillWrapper"><div class="pill unread"></div></div><div class="guildname">meow</div></div><div class="guild"><div class="pillWrapper"><div class="pill unread"></div></div><div class="guildname">mrrow</div></div><div class="guild"><div class="pillWrapper"><div class="pill unread"></div></div><div class="guildname">mrrp</div></div></div><div class="channel-bar"><h2>undefined</h2><div class="channel"><div class="pillWrapper"><div class="pill unread"></div></div><span class="channelname">meow</span></div></div><div class="messages"><div class="message bottom "><img class="messagePfp" width="48" height="48" src="dicor_files/pfp.png"><div class="messageAuthor"><span>Niko (defautluser0, 4160)</span> <div class="badge" style="--badge-color: #a200ff;">DEV</div> <div class="messageTimestamp">Today at 22:09</div></div><div class="messageContent"><span>buh</span></div></div><div class="message bottom "><img class="messagePfp" width="48" height="48" src="dicor_files/pfp2.png"><div class="messageAuthor"><span>Snow32 (snow32a, 240389667688512)</span> <div class="messageTimestamp">Today at 22:09</div></div><div class="messageContent"><span>g</span></div></div><div class="message "><img class="messagePfp" width="48" height="48" src="dicor_files/pfp.png"><div class="messageAuthor"><span>Niko (defautluser0, 4160)</span> <div class="badge" style="--badge-color: #a200ff;">DEV</div> <div class="messageTimestamp">Today at 22:10</div></div><div class="messageContent"><span>afsg</span></div></div><div class="message inline "><div class="messageContent"><span>sagdf</span></div></div><div class="message inline "><div class="messageContent"><span>a</span></div></div><div class="message inline "><div class="messageContent"><span>sg</span></div></div><div class="message inline "><div class="messageContent"><span>adsg</span></div></div><div class="message inline "><div class="messageContent"><span>asd</span></div></div><div class="message inline bottom "><div class="messageContent"><span>ga</span></div></div><div></div></div><div class="textbox"><div class="editor" contenteditable="true" data-text="Type here..."></div></div></div></body></html>

209
dist/pfps/dicor_files/index.css vendored Normal file
View File

@@ -0,0 +1,209 @@
/* src/style.css */
:root {
--brand-color: #a200ff;
}
* {
margin: 0;
}
html {
color: #fff;
background-color: #333;
font-family: Arial, Helvetica, sans-serif;
}
.mentions {
color: #fff;
display: flex;
background-color: #ff1717;
border-radius: 50%;
justify-content: center;
align-items: center;
width: 15px;
height: 15px;
padding: 2px;
font-size: 14px;
}
.channel-bar {
position: absolute;
width: 200px;
padding: 5px;
right: calc(100% - 250px);
}
.pillWrapper {
contain: layout size;
display: flex;
overflow: hidden;
position: relative;
justify-content: flex-start;
align-items: center;
width: 8px;
height: 100%;
}
.pill {
opacity: 0;
background-color: #fff;
border-radius: 0 4px 4px 0;
width: 8px;
height: 0;
transition: height .2s, translate .3s, opacity .1s;
}
.pill.unread {
opacity: 1;
height: 8px;
}
.pill.hover {
opacity: 1;
height: 20px;
}
.pill.open {
opacity: 1;
height: 40px;
}
.channel .mentions {
position: relative;
bottom: 27px;
left: 85%;
}
.channel .pillWrapper {
position: relative;
height: 17px;
top: 9px;
right: 2px;
}
.channel .channelname {
position: relative;
top: -10px;
left: 10px;
}
.server-bar {
position: absolute;
width: 50px;
left: 0;
}
.guild {
width: 40px;
height: 40px;
font-size: 12px;
}
.guild .guildname {
position: relative;
top: -20px;
right: -10px;
}
.guild .pillWrapper {
position: relative;
top: 9px;
right: 2px;
}
.messages {
overflow: hidden scroll;
position: absolute;
width: calc(100% - 250px);
height: calc(100% - 44px);
padding: 5px;
right: 0;
}
.message {
position: relative;
margin-bottom: 2px;
margin-left: 50px;
}
.message.bottom {
margin-bottom: 15px;
}
.message.sending {
color: #777;
}
.message.mentionsyou {
background-color: #b87d00;
}
.message .messagePfp {
position: absolute;
cursor: pointer;
border-radius: 100%;
left: -50px;
}
.message .messagePfp:active {
top: 1px;
}
.message .messageAuthor span {
cursor: pointer;
max-width: -moz-fit-content;
max-width: fit-content;
font-weight: 600;
}
.message .messageAuthor span:hover {
text-decoration: underline;
}
.message .messageAuthor .messageTimestamp {
font-weight: initial;
color: #777;
display: inline;
font-size: 10px;
}
.message .messageContent {
position: relative;
top: -1px;
}
.message .mention {
display: inline;
cursor: pointer;
background-color: #0099ff54;
border-radius: 3px;
padding: 2px;
}
.message .mention:hover {
background-color: #0099ff79;
}
.message .badge {
background-color: var(--badge-color);
display: inline;
border-radius: 10%;
max-width: -moz-fit-content;
max-width: fit-content;
padding: 2px;
font-size: 8pt;
}
.textbox {
position: absolute;
background-color: #222;
width: calc(100% - 250px);
padding: 3px;
bottom: 5px;
right: 5px;
}
.editor:empty:before {
content: attr(data-text);
color: gray;
}

17584
dist/pfps/dicor_files/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
dist/pfps/dicor_files/pfp.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
dist/pfps/dicor_files/pfp2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
dist/pfps/pfp.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
dist/pfps/pfp2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

13
package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"dependencies": {
"react": "^19.2.3",
"react-dom": "^19.2.3",
"slate": "^0.120.0",
"slate-react": "^0.120.0"
},
"devDependencies": {
"@types/bun": "^1.3.5",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3"
}
}

10
public/index.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>dicor</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<script src="index.js"></script>
</body>
</html>

7
public/pfps/dicor.htm Normal file
View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html><head>
<meta http-equiv="content-type" content="text/html; charset=windows-1252">
<title>dicor</title>
<link rel="stylesheet" type="text/css" href="dicor_files/index.css">
</head>
<body><script src="dicor_files/index.js"></script><div><div class="server-bar"><div class="guild"><div class="pillWrapper"><div class="pill unread"></div></div><div class="guildname">meow</div></div><div class="guild"><div class="pillWrapper"><div class="pill unread"></div></div><div class="guildname">mrrow</div></div><div class="guild"><div class="pillWrapper"><div class="pill unread"></div></div><div class="guildname">mrrp</div></div></div><div class="channel-bar"><h2>undefined</h2><div class="channel"><div class="pillWrapper"><div class="pill unread"></div></div><span class="channelname">meow</span></div></div><div class="messages"><div class="message bottom "><img class="messagePfp" width="48" height="48" src="dicor_files/pfp.png"><div class="messageAuthor"><span>Niko (defautluser0, 4160)</span> <div class="badge" style="--badge-color: #a200ff;">DEV</div> <div class="messageTimestamp">Today at 22:09</div></div><div class="messageContent"><span>buh</span></div></div><div class="message bottom "><img class="messagePfp" width="48" height="48" src="dicor_files/pfp2.png"><div class="messageAuthor"><span>Snow32 (snow32a, 240389667688512)</span> <div class="messageTimestamp">Today at 22:09</div></div><div class="messageContent"><span>g</span></div></div><div class="message "><img class="messagePfp" width="48" height="48" src="dicor_files/pfp.png"><div class="messageAuthor"><span>Niko (defautluser0, 4160)</span> <div class="badge" style="--badge-color: #a200ff;">DEV</div> <div class="messageTimestamp">Today at 22:10</div></div><div class="messageContent"><span>afsg</span></div></div><div class="message inline "><div class="messageContent"><span>sagdf</span></div></div><div class="message inline "><div class="messageContent"><span>a</span></div></div><div class="message inline "><div class="messageContent"><span>sg</span></div></div><div class="message inline "><div class="messageContent"><span>adsg</span></div></div><div class="message inline "><div class="messageContent"><span>asd</span></div></div><div class="message inline bottom "><div class="messageContent"><span>ga</span></div></div><div></div></div><div class="textbox"><div class="editor" contenteditable="true" data-text="Type here..."></div></div></div></body></html>

View File

@@ -0,0 +1,209 @@
/* src/style.css */
:root {
--brand-color: #a200ff;
}
* {
margin: 0;
}
html {
color: #fff;
background-color: #333;
font-family: Arial, Helvetica, sans-serif;
}
.mentions {
color: #fff;
display: flex;
background-color: #ff1717;
border-radius: 50%;
justify-content: center;
align-items: center;
width: 15px;
height: 15px;
padding: 2px;
font-size: 14px;
}
.channel-bar {
position: absolute;
width: 200px;
padding: 5px;
right: calc(100% - 250px);
}
.pillWrapper {
contain: layout size;
display: flex;
overflow: hidden;
position: relative;
justify-content: flex-start;
align-items: center;
width: 8px;
height: 100%;
}
.pill {
opacity: 0;
background-color: #fff;
border-radius: 0 4px 4px 0;
width: 8px;
height: 0;
transition: height .2s, translate .3s, opacity .1s;
}
.pill.unread {
opacity: 1;
height: 8px;
}
.pill.hover {
opacity: 1;
height: 20px;
}
.pill.open {
opacity: 1;
height: 40px;
}
.channel .mentions {
position: relative;
bottom: 27px;
left: 85%;
}
.channel .pillWrapper {
position: relative;
height: 17px;
top: 9px;
right: 2px;
}
.channel .channelname {
position: relative;
top: -10px;
left: 10px;
}
.server-bar {
position: absolute;
width: 50px;
left: 0;
}
.guild {
width: 40px;
height: 40px;
font-size: 12px;
}
.guild .guildname {
position: relative;
top: -20px;
right: -10px;
}
.guild .pillWrapper {
position: relative;
top: 9px;
right: 2px;
}
.messages {
overflow: hidden scroll;
position: absolute;
width: calc(100% - 250px);
height: calc(100% - 44px);
padding: 5px;
right: 0;
}
.message {
position: relative;
margin-bottom: 2px;
margin-left: 50px;
}
.message.bottom {
margin-bottom: 15px;
}
.message.sending {
color: #777;
}
.message.mentionsyou {
background-color: #b87d00;
}
.message .messagePfp {
position: absolute;
cursor: pointer;
border-radius: 100%;
left: -50px;
}
.message .messagePfp:active {
top: 1px;
}
.message .messageAuthor span {
cursor: pointer;
max-width: -moz-fit-content;
max-width: fit-content;
font-weight: 600;
}
.message .messageAuthor span:hover {
text-decoration: underline;
}
.message .messageAuthor .messageTimestamp {
font-weight: initial;
color: #777;
display: inline;
font-size: 10px;
}
.message .messageContent {
position: relative;
top: -1px;
}
.message .mention {
display: inline;
cursor: pointer;
background-color: #0099ff54;
border-radius: 3px;
padding: 2px;
}
.message .mention:hover {
background-color: #0099ff79;
}
.message .badge {
background-color: var(--badge-color);
display: inline;
border-radius: 10%;
max-width: -moz-fit-content;
max-width: fit-content;
padding: 2px;
font-size: 8pt;
}
.textbox {
position: absolute;
background-color: #222;
width: calc(100% - 250px);
padding: 3px;
bottom: 5px;
right: 5px;
}
.editor:empty:before {
content: attr(data-text);
color: gray;
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
public/pfps/pfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/pfps/pfp2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

228
serve.ts Normal file
View File

@@ -0,0 +1,228 @@
import { SHA512_256, type ServerWebSocket } from "bun";
import { SnowflakeGenerator } from "./snowflake.ts";
function getContentType(path: string): string {
if (path.endsWith(".html")) return "text/html";
if (path.endsWith(".css")) return "text/css";
if (path.endsWith(".js")) return "application/javascript";
if (path.endsWith(".json")) return "application/json";
if (path.endsWith(".png")) return "image/png";
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
if (path.endsWith(".svg")) return "image/svg+xml";
if (path.endsWith(".ico")) return "image/x-icon";
return "application/octet-stream";
}
enum OpcodesClientbound {
authorizeLogin,
initMessages,
updateMessages,
registerSuccess,
error,
users,
keepAlive = 9,
}
enum OpcodesServerbound {
identify,
sendMessage,
register,
getUsers,
keepAlive = 9,
}
interface user {
name: string;
username: string;
pfp: string;
id: number;
token: string;
password: string;
dev: boolean;
}
interface message {
id: string;
author: user;
content: string;
timestamp: number;
editedTimestamp?: number;
replyTo?: string;
hasSent: true; // client sends false but server should always say true
}
const messages: message[] = await Bun.file("./db/messages.json").json();
const users: user[] = await Bun.file("./db/users.json").json();
const generator = new SnowflakeGenerator();
function sendError(ws: ServerWebSocket, reason: string) {
ws.send(
JSON.stringify({
op: OpcodesClientbound.error,
data: {
reason,
},
})
);
ws.close();
}
const server = Bun.serve({
port: 80,
websocket: {
open(ws) {
console.log("connected to ws");
ws.subscribe("messages");
// @ts-expect-error
ws.interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
op: OpcodesClientbound.keepAlive,
data: null,
})
);
console.log("sending keeaplivve");
} else {
// @ts-expect-error
clearInterval(ws.interval);
}
// @ts-expect-error guhh
clearTimeout(ws.pingTimeout);
// @ts-expect-error guhh
ws.pingTimeout = setTimeout(() => {
ws.close();
}, 30_000);
}, 60_000);
// @ts-expect-error guhh
ws.pingTimeout = null;
},
message(ws, msg) {
const obj = JSON.parse(msg.toString());
let user: string | any[];
switch (obj.op) {
case OpcodesServerbound.identify:
user = users.filter(user => user.token === obj.data.token);
if (!(user && user.length !== 0)) {
user = users.filter(
(user) =>
user.password ===
new SHA512_256().update(obj.data.password ?? "").digest("base64")
);
}
if (user && user.length !== 0) {
ws.send(
JSON.stringify({
op: OpcodesClientbound.authorizeLogin,
data: user[0],
})
);
ws.send(
JSON.stringify({
op: OpcodesClientbound.initMessages,
data: messages,
})
);
} else {
sendError(ws, "Not a real user");
}
break;
case OpcodesServerbound.sendMessage:
user = users.filter((user) => user.token === obj.data.token) as user[];
if (user && user.length !== 0) {
obj.op = OpcodesClientbound.updateMessages;
obj.data.id = generator.generateSnowflake();
obj.data.author = JSON.parse(JSON.stringify(user[0]));
obj.data.author.password = null;
obj.data.author.token = null;
obj.data.hasSent = true;
obj.data.token = null;
obj.data.author.dev = user[0].dev;
ws.publish("messages", JSON.stringify(obj));
ws.send(JSON.stringify(obj));
messages.push(obj.data);
Bun.write("./db/messages.json", JSON.stringify(messages));
} else {
sendError(ws, "Not a real user");
}
break;
case OpcodesServerbound.getUsers:
let mmiow = JSON.parse(JSON.stringify(users)) as user[];
mmiow.forEach(user => {
// @ts-expect-error shush
user.token = null;
// @ts-expect-error shush
user.password = null;
})
ws.send(JSON.stringify({
op: OpcodesClientbound.users,
data: mmiow
}))
break;
case OpcodesServerbound.keepAlive:
// @ts-expect-error guhh
clearTimeout(ws.pingTimeout);
// @ts-expect-error guhh
ws.pingTimeout = setTimeout(() => {
ws.close();
}, 90_000);
break;
default:
break;
}
},
close(ws) {
console.log("client disconnected");
},
},
async fetch(req, server) {
const url = new URL(req.url);
let path = url.pathname;
if (path === "/") {
path = "/index.html";
}
if (url.pathname === "/ws") {
const upgraded = server.upgrade(req, {
// @ts-expect-error guhh whqt
data: {
name: new URL(req.url).searchParams.get("name"),
},
});
if (!upgraded) {
return new Response("Upgrade failed", { status: 400 });
}
return;
}
path = path.replace(/\.\./g, "");
try {
const file = Bun.file(`./dist${path}`);
if (!(await file.exists())) {
return new Response(
"<center><h1>404 Not Found</h1><hr><p>Bun</p></center>",
{ status: 404, headers: { "Content-Type": "text/html" } }
);
}
return new Response(file, {
headers: {
"Content-Type": getContentType(path),
},
});
} catch (err) {
console.error("Error serving file:", err);
return new Response(
"<center><h1>500 Internal Server Error</h1><hr><p>Bun</p></center>",
{ status: 500, headers: { "Content-Type": "text/html" } }
);
}
},
});
console.log(`Server running at http://localhost:${server.port}`);

68
snowflake.ts Normal file
View File

@@ -0,0 +1,68 @@
export class SnowflakeGenerator {
private epoch: bigint;
private workerId: bigint;
private processId: bigint;
private sequence: bigint;
private lastTimestamp: bigint;
constructor(workerId: bigint = 1n, processId: bigint = 1n, epoch: bigint = 1766617200000n) {
// dec 1 2025 (creation of this)
this.epoch = epoch;
this.workerId = workerId;
this.processId = processId;
this.sequence = 0n;
this.lastTimestamp = -1n;
}
private getCurrentTimestamp(): bigint {
return BigInt(Date.now());
}
private waitForNextMillisecond(lastTimestamp: bigint): bigint {
let timestamp = this.getCurrentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = this.getCurrentTimestamp();
}
return timestamp;
}
public generateSnowflake(): number {
const timestamp = this.getCurrentTimestamp() - this.epoch;
if (timestamp === this.lastTimestamp) {
this.sequence += 1n;
if (this.sequence > 4095n) { // max sequence value (12 bits)
// Wait for the next millisecond if we've reached the max sequence value
const newTimestamp = this.waitForNextMillisecond(this.lastTimestamp);
this.sequence = 0n;
return Number.parseInt(String(this.createSnowflake(newTimestamp)));
}
} else {
this.sequence = 0n;
}
this.lastTimestamp = timestamp;
return Number.parseInt(String(this.createSnowflake(timestamp)));
}
private createSnowflake(timestamp: bigint): bigint {
return (timestamp << 22n) | (BigInt(this.workerId) << 18n) | (BigInt(this.processId) << 12n) | BigInt(this.sequence);
}
public decodeSnowflake(id: bigint | number) {
const snowflake = BigInt(id);
const sequence = snowflake & (1n << 6n) - 1n;
const processId = (snowflake >> 6n) & ((1n << 6n) - 1n);
const workerId = (snowflake >> 12n) & ((1n << 10n) - 1n);
const timestamp = (snowflake >> 22n) + this.epoch;
return {
timestamp, // bigint (ms since Unix epoch)
date: new Date(Number(timestamp)),
workerId,
processId,
sequence,
};
}
}

18
src/components/App.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { RenderMessages } from "@components/Message";
import { useState, type ReactElement } from "react";
import { Chatbar } from "@components/Chatbar.tsx";
import { ChannelBar } from "@components/ChannelBar.tsx";
import { ServerBar } from "@components/ServerBar.tsx";
export function App(): ReactElement {
const [serverName, setServerName] = useState("undefined");
return (
<div>
<ServerBar setServerName={setServerName} currentServer={serverName} />
<ChannelBar servername={serverName} />
<RenderMessages />
<Chatbar />
</div>
)
}

View File

@@ -0,0 +1,21 @@
import { Mentions, Pill } from "@components/PillMentions";
import { ReactElement } from "react";
export function ChannelBar({servername}: {servername: string}): ReactElement {
return (
<div className="channel-bar">
<h2>{servername}</h2>
<Channel channelName="meow" mentions={0} unread={true} muted={false} />
</div>
)
}
function Channel({channelName, mentions, unread, muted}: {channelName: string, mentions: number, unread: boolean, muted: boolean}): ReactElement {
return (
<div className="channel">
<Pill unread={unread} muted={muted} open={false} hover={false} />
<span className="channelname">{channelName}</span>
<Mentions count={mentions} />
</div>
)
}

View File

@@ -0,0 +1,67 @@
import { user } from "@localtypes";
import { sendChatMessage, token, self, getUsers, subscribeUsers } from "@ws";
import { useRef, useState, useSyncExternalStore, type ReactElement } from "react";
export function Chatbar(): ReactElement {
const [message, setMessage] = useState("");
const users: user[] = useSyncExternalStore(
subscribeUsers,
getUsers
);
const editorRef = useRef<HTMLDivElement | null>(null);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (message.trim() !== "") {
const msg = replaceMentions(message.trim());
sendChatMessage({id: Date.now(), author: self, content: msg, timestamp: Date.now(), token: token!, hasSent: false});
console.log("Sent message:", msg);
setMessage("");
if (editorRef.current) {
editorRef.current.innerText = "";
}
}
}
};
function replaceMentions(
text: string,
): string {
// Build a quick lookup map: username -> id
const userMap = new Map(
users.map(u => [u.username, u.id])
);
console.log(userMap);
// Match @[username] (username = anything except ])
return text.replace(/@(\S+)/g, (match, username) => {
const userId = userMap.get(username);
console.log(userId);
return userId ? `<@${userId}>` : match;
});
}
function handleChange(event: React.FormEvent<HTMLDivElement>) {
if (editorRef.current) {
setMessage(editorRef.current.innerText.trim());
if (editorRef.current.innerText === "\n") {
editorRef.current.innerHTML = "";
}
}
};
return (
<div className="textbox">
<div
ref={editorRef}
className="editor"
contentEditable
onKeyDown={handleKeyDown}
onInput={handleChange}
data-text="Type here..."
></div>
</div>
);
}

150
src/components/Message.tsx Normal file
View File

@@ -0,0 +1,150 @@
import { RefObject, useEffect, useRef, useSyncExternalStore, type ReactElement } from "react";
import { user, message } from "@localtypes";
import { subscribeUsers, getUsers, subscribeMessages, getMessages, self } from "@ws";
function formatUnixTimestamp(ts: number) {
const date = new Date(ts);
const now = new Date();
const isToday =
date.getDate() === now.getDate() &&
date.getMonth() === now.getMonth() &&
date.getFullYear() === now.getFullYear();
const HH = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
if (isToday) {
return `Today at ${HH}:${min}`;
}
const dd = String(date.getDate()).padStart(2, '0');
const mm = String(date.getMonth() + 1).padStart(2, '0');
const yy = String(date.getFullYear())
return `${dd}/${mm}/${yy} ${HH}:${min}`;
}
export function RenderMessages(): ReactElement {
let currentAuthor: user | null = null;
const messages: (message | null)[] = useSyncExternalStore(
subscribeMessages,
getMessages
);
console.log(messages);
const bottomRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "instant" });
}, [messages.length]);
return (
<div className="messages">
{messages.map((message, index) => {
if (message) {
const nextMessage = messages[index + 1];
const nextAuthor = nextMessage ? nextMessage.author : null;
const isLastMessageFromAuthor = nextAuthor?.id !== message.author.id;
const inline = currentAuthor?.id === message.author.id;
if (!inline) {
currentAuthor = message.author;
}
return (
<Message
key={message.id}
author={message.author}
content={message.content}
inline={inline}
bottom={isLastMessageFromAuthor}
timestamp={message.timestamp}
hasSent={message.hasSent}
mention={message.content.includes(`<@${self.id}>`) || message.content.includes("@everyone") || message.content.includes("@here")}
/>
);
} else return ""
})}
<div ref={bottomRef} />
</div>
);
}
function Message({author, content, inline = false, bottom = false, timestamp = 0, hasSent, mention}: {author: user, content: string, inline?: boolean, bottom?: boolean, timestamp?: number, hasSent: boolean, mention: boolean}): ReactElement {
const users: user[] = useSyncExternalStore(
subscribeUsers,
getUsers
);
function formatContent(content: string): ReactElement[] {
const parts = content.split('\n'); // Split the content into parts by newlines
return parts.flatMap((part, index) => {
const elements: ReactElement[] = [];
let lastIndex = 0;
// Match all mentions in the part (e.g., <@123>)
const mentionRegex = /<@(\d+)>/g;
let match;
while ((match = mentionRegex.exec(part)) !== null) {
const [mentionTag, userId] = match;
const textBeforeMention = part.slice(lastIndex, match.index);
// Add the text before the mention as plain text
if (textBeforeMention) {
elements.push(<span key={`${index}-text-${lastIndex}`}>{textBeforeMention}</span>);
}
// Add the Mention component
const user = users.find((user) => user.id === parseInt(userId!)); // Use parseInt for ID comparison
elements.push(
<Mention
key={`${index}-mention-${userId}`}
name={user?.name}
username={user ? user.username : 'unknown-user'}
/>
);
lastIndex = mentionRegex.lastIndex; // Update the last index
}
// Add any remaining text after the last mention (if any)
const remainingText = part.slice(lastIndex);
if (remainingText) {
elements.push(<span key={`${index}-text-${lastIndex}`}>{remainingText}</span>);
}
// Add a <br /> between parts if its not the last part
if (index < parts.length - 1) {
elements.push(<br key={`br-${index}`} />);
}
return elements;
});
}
const formattedContent = formatContent(content);
return (
<div className={`message ${inline ? "inline" : ""} ${bottom ? "bottom" : ""} ${hasSent ? "" : "sending"} ${mention ? "mentionsyou" : ""}`}>
{!inline ? <img className="messagePfp" src={`/pfps/${author.pfp}.png`} width={48} height={48}></img> : ""}
{!inline ? <div className="messageAuthor"><span>{self.dev ? `${author.name} (${author.username}, ${author.id})` : author.name ?? author.username}</span> {author.dev ? <Badge text="DEV" /> : ""} <Timestamp ts={timestamp} /></div> : ""}
<div className="messageContent">{formattedContent}</div>
</div>
)
}
function Badge({text, color = "#a200ff"}: {text: string, color?: string}) {
return (
<div className="badge" style={{["--badge-color" as any]: color ?? "var(--brand-color)"}}>{text}</div>
)
}
function Timestamp({ts}: {ts: number}): ReactElement {
return (
<div className="messageTimestamp">{formatUnixTimestamp(ts)}</div>
)
}
function Mention({name, username}: { name?: string, username: string }): ReactElement {
return (
<div className="mention">@{name ?? username}</div>
)
}

View File

@@ -0,0 +1,19 @@
import { ReactElement } from "react";
export function Pill({unread, muted, open, hover}: {unread: boolean, muted: boolean, open: boolean, hover: boolean}): ReactElement {
return (
<div className="pillWrapper">
<div className={`pill${muted ? "" : open ? " open" : hover ? " hover" : unread ? " unread" : ""}`}></div>
</div>
)
}
export function Mentions({count}: {count: number}): ReactElement {
return (
<>
{count !== 0 ?<div className="mentions">
<div className="count">{count}</div>
</div> : ""}
</>
)
}

View File

@@ -0,0 +1,26 @@
import { Pill } from "@components/PillMentions.tsx";
import { guild } from "@localtypes";
import { ReactElement, useState } from "react";
let serverId = null;
export function ServerBar({setServerName, currentServer}: {setServerName: (name: string) => void, currentServer: string}): ReactElement {
return (
<div className="server-bar">
<Guild guild={{channels: [], guildId: 2, members: [], name: "meow", unread: true, muted: false}} open={"meow" === currentServer} setServerName={setServerName} />
<Guild guild={{channels: [], guildId: 2, members: [], name: "mrrow", unread: true, muted: false}} open={"mrrow" === currentServer} setServerName={setServerName} />
<Guild guild={{channels: [], guildId: 2, members: [], name: "mrrp", unread: true, muted: false}} open={"mrrp" === currentServer} setServerName={setServerName} />
</div>
)
}
function Guild({guild, open, setServerName}: {guild: guild, open: boolean, setServerName: (name: string) => void}): ReactElement {
const [hover, setHover] = useState(false);
return (
<div className="guild" onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onClick={() => setServerName(guild.name)}>
<Pill unread={guild.unread} muted={guild.muted} hover={hover} open={open} />
<div className="guildname">{guild.name}</div>
</div>
)
}

8
src/index.tsx Normal file
View File

@@ -0,0 +1,8 @@
import { createRoot } from "react-dom/client";
import { App } from "@components/App";
import "./style.css";
const domNode = document.body as HTMLBodyElement;
const root = createRoot(domNode);
root.render(<App />);

193
src/style.css Normal file
View File

@@ -0,0 +1,193 @@
:root {
--brand-color: #a200ff;
}
* {
margin: 0;
}
html {
background-color: #333;
color: #fff;
font-family: Arial, Helvetica, sans-serif;
}
.mentions {
color: white;
background-color: #ff1717;
padding: 2px;
width: 15px;
height: 15px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
}
.channel-bar {
padding: 5px;
width: 200px;
position: absolute;
right: calc(100% - 250px);
}
.pillWrapper {
align-items: center;
contain: layout size;
display: flex;
height: 100%;
justify-content: flex-start;
overflow: hidden;
position: relative;
width: 8px
}
.pill {
width: 8px;
height: 0;
background-color: white;
border-radius: 0 4px 4px 0;
transition: height 0.2s ease, translate 0.3s ease, opacity 0.1s ease;
opacity: 0;
&.unread {
height: 8px;
opacity: 1;
}
&.hover {
height: 20px;
opacity: 1;
}
&.open {
height: 40px;
opacity: 1;
}
}
.channel {
.mentions {
position: relative;
bottom: 27px;
left: 85%;
}
.pillWrapper {
position: relative;
right: 2px;
top: 9px;
height: 17px;
}
.channelname {
position: relative;
top: -10px;
left: 10px;
}
}
.server-bar {
width: 50px;
position: absolute;
left: 0;
}
.guild {
width: 40px;
height: 40px;
font-size: 12px;
.guildname {
right: -10px;
top: -20px;
position: relative;
}
.pillWrapper {
position: relative;
right: 2px;
top: 9px;
}
}
.messages {
padding: 5px;
width: calc(100% - 250px);
height: calc(100% - 44px);
overflow: hidden scroll;
position: absolute;
right: 0;
}
.message {
margin-left: 50px;
margin-bottom: 2px;
&.bottom {
margin-bottom: 15px;
}
position: relative;
&.sending {
color: #777;
}
&.mentionsyou {
background-color: #b87d00;
}
.messagePfp {
left: -50px;
position: absolute;
border-radius: 100%;
cursor: pointer;
&:active {
top: 1px;
}
}
.messageAuthor {
span {
font-weight: 600;
cursor: pointer;
max-width: fit-content;
&:hover {
text-decoration: underline;
}
}
.messageTimestamp {
font-weight: initial;
color: #777;
font-size: 10px;
display: inline;
}
}
.messageContent {
top: -1px;
position: relative;
}
.mention {
display: inline;
background-color: #0099ff54;
padding: 2px;
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: #0099ff79;
}
}
.badge {
background-color: var(--badge-color);
max-width: fit-content;
padding: 2px;
border-radius: 10%;
font-size: 8pt;
display: inline;
}
}
.textbox {
padding: 3px;
position: absolute;
bottom: 5px;
right: 5px;
width: calc(100% - 250px);
background-color: #222;
}
.editor:empty:before {
content: attr(data-text);
color: grey;
}

7
src/types/channel.ts Normal file
View File

@@ -0,0 +1,7 @@
import { id, message } from "@localtypes";
export interface channel {
channelId: id;
messages: message[];
}

10
src/types/guild.ts Normal file
View File

@@ -0,0 +1,10 @@
import { id, channel, user } from "@localtypes";
export interface guild {
guildId: id;
channels: channel[];
name: string;
members: user[];
unread: boolean;
muted: boolean;
}

5
src/types/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from "./url"
export * from "./user"
export * from "./message"
export * from "./guild"
export * from "./channel"

13
src/types/message.ts Normal file
View File

@@ -0,0 +1,13 @@
import { user } from "@localtypes";
export type id = number;
export interface message {
id: id;
author: user;
content: string;
timestamp: number;
editedTimestamp?: number
replyTo?: id;
hasSent: boolean;
}

1
src/types/url.ts Normal file
View File

@@ -0,0 +1 @@
export type url = string;

11
src/types/user.ts Normal file
View File

@@ -0,0 +1,11 @@
import { id, url } from "@localtypes";
export interface user {
name?: string,
username: string,
pfp: url,
id: id,
token: null,
password: null,
dev: boolean;
}

192
src/ws/index.ts Normal file
View File

@@ -0,0 +1,192 @@
import { message, user } from "@localtypes";
let passwd = "";
let token = localStorage.getItem("token") ?? "";
while (!(passwd && passwd !== "") && !token) {
passwd = prompt("input passwd")!;
}
export let ws = new WebSocket("ws://localhost/ws");
let messages: message[] = [];
let self: user;
let users: user[] = [];
let queue: (message | null)[] = new Array(6).fill(null);
const listenersMessages = new Set<() => void>();
const listenersUsers = new Set<() => void>();
let visibleMessages: (message | null)[] = [];
let dirty = true;
function rebuildVisibleMessages() {
if (!dirty) return;
visibleMessages = [...messages, ...queue];
dirty = false;
}
ws.onopen = () => {
console.log("opened websocket!");
if (!token) {
ws.send(JSON.stringify({
op: OpcodesServerbound.identify,
data: {
password: passwd,
}
}))
} else {
ws.send(JSON.stringify({
op: OpcodesServerbound.identify,
data: {
token,
}
}))
}
}
ws.onmessage = message => {
const msg = message.data;
parseMessages(msg);
}
const onclose = () => {
console.log("ws disconnected. reconnecting");
ws = new WebSocket("ws://localhost/ws");
ws.onopen = () => {
console.log("opened websocket!");
if (!token) {
ws.send(JSON.stringify({
op: OpcodesServerbound.identify,
data: {
password: passwd,
}
}))
} else {
ws.send(JSON.stringify({
op: OpcodesServerbound.identify,
data: {
token,
}
}))
}
}
ws.onmessage = message => {
const msg = message.data;
parseMessages(msg);
}
ws.onclose = onclose;
}
ws.onclose = onclose;
function notifyMessages() {
listenersMessages.forEach((l) => l());
}
function notifyUsers() {
listenersUsers.forEach((l) => l());
}
enum OpcodesClientbound {
authorizeLogin,
initMessages,
updateMessages,
registerSuccess,
error,
users,
keepAlive = 9,
}
enum OpcodesServerbound {
identify,
sendMessage,
register,
getUsers,
keepAlive = 9,
}
function parseMessages(message: string) {
const json: {op: number, data: any} = JSON.parse(message) as {op: number, data: any};
console.log(json);
switch (json.op) {
case OpcodesClientbound.authorizeLogin:
token = json.data.token;
localStorage.setItem("token", token!);
self = json.data;
self.token = null;
self.password = null;
ws.send(JSON.stringify({
op: OpcodesServerbound.getUsers,
data: null
}))
break;
case OpcodesClientbound.initMessages:
messages.push(...json.data);
dirty = true;
notifyMessages();
break;
case OpcodesClientbound.users:
users = json.data;
console.log(users);
notifyUsers();
break;
case OpcodesClientbound.keepAlive:
ws.send(JSON.stringify({
op: OpcodesServerbound.keepAlive,
data: null,
}))
break;
case OpcodesClientbound.updateMessages:
messages.push(json.data);
if (json.data.author.id === self.id) {
console.log(queue);
queue.reverse();
queue.pop();
queue.reverse();
queue.push(null as never);
console.log(queue);
}
dirty = true;
notifyMessages();
break;
default:
break;
}
}
export function getMessages(): (message | null)[] {
rebuildVisibleMessages();
return visibleMessages;
}
export function getUsers(): user[] {
return users;
}
export function sendChatMessage(message: message & { token?: string }) {
if (queue.filter(m => m !== null).length < 6) {
console.log(message);
for (let i = 0; i < queue.length; i++) {
if (!queue[i]) {
queue[i] = message;
i = 6;
}
}
dirty = true;
notifyMessages();
ws.send(JSON.stringify({op: OpcodesServerbound.sendMessage, data: message}));
}
}
export function subscribeMessages(listener: () => void) {
listenersMessages.add(listener);
return () => listenersMessages.delete(listener);
}
export function subscribeUsers(listener: () => void) {
listenersUsers.add(listener);
return () => listenersUsers.delete(listener);
}
export { token, self };

37
tsconfig.json Normal file
View File

@@ -0,0 +1,37 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"paths": {
"@components/*": ["./src/components/*"],
"@localtypes": ["./src/types/index.ts"],
"@ws": ["./src/ws/index.ts"]
}
},
}