not so initial commit
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
db
|
||||||
|
|
||||||
|
node_modules
|
||||||
36
build.ts
Normal 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
@@ -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
@@ -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
@@ -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
27
dist/index.js.map
vendored
Normal file
7
dist/pfps/dicor.htm
vendored
Normal 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
@@ -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
BIN
dist/pfps/dicor_files/pfp.png
vendored
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
dist/pfps/dicor_files/pfp2.png
vendored
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
dist/pfps/pfp.png
vendored
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
dist/pfps/pfp2.png
vendored
Normal file
|
After Width: | Height: | Size: 191 KiB |
13
package.json
Normal 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
@@ -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
@@ -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
public/pfps/dicor_files/index.css
Normal 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
public/pfps/dicor_files/index.js
Normal file
BIN
public/pfps/dicor_files/pfp.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/pfps/dicor_files/pfp2.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
public/pfps/pfp.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/pfps/pfp2.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
228
serve.ts
Normal 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
@@ -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
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/components/ChannelBar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
67
src/components/Chatbar.tsx
Normal 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
@@ -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 it’s 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
src/components/PillMentions.tsx
Normal 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> : ""}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
26
src/components/ServerBar.tsx
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
|
|||||||
|
import { id, message } from "@localtypes";
|
||||||
|
|
||||||
|
export interface channel {
|
||||||
|
channelId: id;
|
||||||
|
messages: message[];
|
||||||
|
|
||||||
|
}
|
||||||
10
src/types/guild.ts
Normal 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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export type url = string;
|
||||||
11
src/types/user.ts
Normal 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
@@ -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
@@ -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"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||