Compare commits

..

8 Commits

Author SHA1 Message Date
34f3552a40 rerply 2026-01-18 21:04:49 +01:00
2226d8bcfc key -> focus chatbar 2026-01-18 21:03:47 +01:00
ba2178613b styling 2026-01-18 21:03:43 +01:00
c7d79fd5b8 sring 2026-01-18 21:03:33 +01:00
7e23c3173a smaller msgdb size 2026-01-15 18:58:34 +01:00
8fa8698c1e ok so what 2026-01-15 17:54:40 +01:00
df0b1f3573 make it not refill messages 2026-01-15 17:41:24 +01:00
3c3799dfd5 bot 2026-01-15 17:38:22 +01:00
13 changed files with 192 additions and 17828 deletions

View File

@@ -1,7 +0,0 @@
<!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

@@ -1,209 +0,0 @@
/* 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.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

View File

@@ -2,7 +2,7 @@ import { SHA512_256, type ServerWebSocket } from "bun";
import { SnowflakeGenerator } from "./snowflake.ts"; import { SnowflakeGenerator } from "./snowflake.ts";
function getContentType(path: string): string { function getContentType(path: string): string {
if (path.endsWith(".html")) return "text/html"; if (path.endsWith(".html") || path.endsWith(".htm")) return "text/html";
if (path.endsWith(".css")) return "text/css"; if (path.endsWith(".css")) return "text/css";
if (path.endsWith(".js")) return "application/javascript"; if (path.endsWith(".js")) return "application/javascript";
if (path.endsWith(".json")) return "application/json"; if (path.endsWith(".json")) return "application/json";
@@ -32,18 +32,19 @@ enum OpcodesServerbound {
} }
interface user { interface user {
name: string; name?: string;
username: string; username: string;
pfp: string; pfp?: string;
id: number; id: string;
token: string; token?: string;
password: string; password?: string;
dev: boolean; dev?: boolean;
bot?: boolean;
} }
interface message { interface message {
id: string; id: string;
author: user; author: string | user; // server sends the user object but stores only the id for easier user.json modification
content: string; content: string;
timestamp: number; timestamp: number;
editedTimestamp?: number; editedTimestamp?: number;
@@ -120,10 +121,15 @@ const server = Bun.serve({
data: user[0], data: user[0],
}) })
); );
let msgs = JSON.parse(JSON.stringify(messages));
for (const ms of msgs) {
const user = users.find(user => user.id === ms.author);
ms.author = user || { username: "unknown-user", id: "0" };
}
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
op: OpcodesClientbound.initMessages, op: OpcodesClientbound.initMessages,
data: messages, data: msgs,
}) })
); );
} else { } else {
@@ -134,7 +140,7 @@ const server = Bun.serve({
user = users.filter((user) => user.token === obj.data.token) as user[]; user = users.filter((user) => user.token === obj.data.token) as user[];
if (user && user.length !== 0) { if (user && user.length !== 0) {
obj.op = OpcodesClientbound.updateMessages; obj.op = OpcodesClientbound.updateMessages;
obj.data.id = generator.generateSnowflake(); obj.data.id = String(generator.generateSnowflake());
obj.data.author = JSON.parse(JSON.stringify(user[0])); obj.data.author = JSON.parse(JSON.stringify(user[0]));
obj.data.author.password = null; obj.data.author.password = null;
obj.data.author.token = null; obj.data.author.token = null;
@@ -143,6 +149,7 @@ const server = Bun.serve({
obj.data.author.dev = user[0].dev; obj.data.author.dev = user[0].dev;
ws.publish("messages", JSON.stringify(obj)); ws.publish("messages", JSON.stringify(obj));
ws.send(JSON.stringify(obj)); ws.send(JSON.stringify(obj));
obj.data.author = obj.data.author.id;
messages.push(obj.data); messages.push(obj.data);
Bun.write("./db/messages.json", JSON.stringify(messages)); Bun.write("./db/messages.json", JSON.stringify(messages));
} else { } else {
@@ -186,7 +193,14 @@ const server = Bun.serve({
path = "/index.html"; path = "/index.html";
} }
if (url.pathname === "/ws") { if (path === "/pfps/" || path === "/pfps") {
return new Response(
"<center><h1>403 Forbidden</h1><hr><p>Bun</p></center>",
{ status: 403, headers: { "Content-Type": "text/html" } }
);
}
if (path === "/ws") {
const upgraded = server.upgrade(req, { const upgraded = server.upgrade(req, {
// @ts-expect-error guhh whqt // @ts-expect-error guhh whqt
data: { data: {

View File

@@ -7,12 +7,19 @@ import { ServerBar } from "@components/ServerBar.tsx";
export function App(): ReactElement { export function App(): ReactElement {
const [serverName, setServerName] = useState("undefined"); const [serverName, setServerName] = useState("undefined");
window.addEventListener("keydown", (e) => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
const chatbar = document.getElementById("editor");
chatbar?.focus();
chatbar?.onkeydown?.(e);
});
return ( return (
<div> <div>
<ServerBar setServerName={setServerName} currentServer={serverName} /> <ServerBar setServerName={setServerName} currentServer={serverName} />
<ChannelBar servername={serverName} /> <ChannelBar servername={serverName} />
<RenderMessages /> <RenderMessages />
<Chatbar /> <Chatbar channelName="undefined" />
</div> </div>
) )
} }

View File

@@ -2,7 +2,7 @@ import { user } from "@localtypes";
import { sendChatMessage, token, self, getUsers, subscribeUsers } from "@ws"; import { sendChatMessage, token, self, getUsers, subscribeUsers } from "@ws";
import { useRef, useState, useSyncExternalStore, type ReactElement } from "react"; import { useRef, useState, useSyncExternalStore, type ReactElement } from "react";
export function Chatbar(): ReactElement { export function Chatbar({channelName}: {channelName: string}): ReactElement {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const users: user[] = useSyncExternalStore( const users: user[] = useSyncExternalStore(
subscribeUsers, subscribeUsers,
@@ -12,11 +12,11 @@ export function Chatbar(): ReactElement {
const editorRef = useRef<HTMLDivElement | null>(null); const editorRef = useRef<HTMLDivElement | null>(null);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === "Enter" && !event.shiftKey) { if (event.key === "Enter" && !event.shiftKey && message.trim().length !== 0) {
event.preventDefault(); event.preventDefault();
if (message.trim() !== "") { if (message.trim() !== "") {
const msg = replaceMentions(message.trim()); const msg = replaceMentions(message.trim());
sendChatMessage({id: Date.now(), author: self, content: msg, timestamp: Date.now(), token: token!, hasSent: false}); sendChatMessage({id: Date.now(), author: self, content: msg, timestamp: Date.now(), token: token!, hasSent: false, replyTo: undefined});
console.log("Sent message:", msg); console.log("Sent message:", msg);
setMessage(""); setMessage("");
if (editorRef.current) { if (editorRef.current) {
@@ -57,10 +57,12 @@ export function Chatbar(): ReactElement {
<div <div
ref={editorRef} ref={editorRef}
className="editor" className="editor"
id="editor"
contentEditable contentEditable
tabIndex={0}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onInput={handleChange} onInput={handleChange}
data-text="Type here..." data-text={`Message ${channelName}`}
></div> ></div>
</div> </div>
); );

9
src/components/Icons.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { ReactElement } from "react";
export function ReplyIcon(): ReactElement {
return (
<svg className="icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" view-box="0 0 24 24">
<path fill="currentColor" d="M2.3 7.3a1 1 0 0 0 0 1.4l5 5a1 1 0 0 0 1.4-1.4L5.42 9H11a7 7 0 0 1 7 7v4a1 1 0 1 0 2 0v-4a9 9 0 0 0-9-9H5.41l3.3-3.3a1 1 0 0 0-1.42-1.4l-5 5Z"></path>
</svg>
)
}

View File

@@ -1,6 +1,7 @@
import { RefObject, useEffect, useRef, useSyncExternalStore, type ReactElement } from "react"; import { RefObject, useEffect, useRef, useState, useSyncExternalStore, type ReactElement } from "react";
import { user, message } from "@localtypes"; import { user, message, id } from "@localtypes";
import { subscribeUsers, getUsers, subscribeMessages, getMessages, self } from "@ws"; import { subscribeUsers, getUsers, subscribeMessages, getMessages, self, replyToMessage } from "@ws";
import { ReplyIcon } from "@components/Icons.tsx";
function formatUnixTimestamp(ts: number) { function formatUnixTimestamp(ts: number) {
const date = new Date(ts); const date = new Date(ts);
@@ -23,13 +24,11 @@ function formatUnixTimestamp(ts: number) {
export function RenderMessages(): ReactElement { export function RenderMessages(): ReactElement {
let currentAuthor: user | null = null; let currentAuthor: user | null = null;
const messages: (message | null)[] = useSyncExternalStore( const messages: (null | message)[] = useSyncExternalStore(
subscribeMessages, subscribeMessages,
getMessages getMessages
); );
console.log(messages);
const bottomRef = useRef<HTMLDivElement | null>(null); const bottomRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
@@ -53,11 +52,13 @@ export function RenderMessages(): ReactElement {
key={message.id} key={message.id}
author={message.author} author={message.author}
content={message.content} content={message.content}
inline={inline} id={message.id}
inline={inline && !message.replyTo}
bottom={isLastMessageFromAuthor} bottom={isLastMessageFromAuthor}
timestamp={message.timestamp} timestamp={message.timestamp}
hasSent={message.hasSent} hasSent={message.hasSent}
mention={message.content.includes(`<@${self.id}>`) || message.content.includes("@everyone") || message.content.includes("@here")} mention={message.content.includes(`<@${self.id}>`) || message.content.includes("@everyone") || message.content.includes("@here")}
repliesTo={message.replyTo}
/> />
); );
} else return "" } else return ""
@@ -67,11 +68,39 @@ export function RenderMessages(): ReactElement {
); );
} }
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 { function Message({
author,
content,
id,
inline = false,
bottom = false,
timestamp = 0,
hasSent = false,
mention,
repliesTo,
roleColor,
}: {
author: user,
content: string,
id: id,
inline?: boolean,
bottom?: boolean,
timestamp?: number,
hasSent: boolean,
mention: boolean,
repliesTo?: id,
roleColor?: string,
}): ReactElement {
const users: user[] = useSyncExternalStore( const users: user[] = useSyncExternalStore(
subscribeUsers, subscribeUsers,
getUsers getUsers
); );
const messages: (message | null)[] = useSyncExternalStore(
subscribeMessages,
getMessages
)
const [hover, setHover] = useState(false);
function formatContent(content: string): ReactElement[] { function formatContent(content: string): ReactElement[] {
const parts = content.split('\n'); // Split the content into parts by newlines const parts = content.split('\n'); // Split the content into parts by newlines
@@ -122,10 +151,28 @@ function Message({author, content, inline = false, bottom = false, timestamp = 0
} }
const formattedContent = formatContent(content); const formattedContent = formatContent(content);
let replyContent: ReactElement[] = [];
const replyTo = messages.find(msg => msg?.id === repliesTo);
if (replyTo) {
replyContent = formatContent(replyTo.content);
}
function reply() {
console.log(id);
replyToMessage(id);
}
return ( return (
<div className={`message ${inline ? "inline" : ""} ${bottom ? "bottom" : ""} ${hasSent ? "" : "sending"} ${mention ? "mentionsyou" : ""}`}> <div
className={`message ${inline ? "inline" : ""} ${bottom ? "bottom" : ""} ${hasSent ? "" : "sending"} ${mention ? "mentionsyou" : ""}`}
style={{["--role-color" as any]: roleColor ?? "#fff"}}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{hover && <MessageHoverActions reply={reply} />}
{replyTo && replyContent ? <Reply author={replyTo.author} content={replyContent} /> : ""}
{!inline ? <img className="messagePfp" src={`/pfps/${author.pfp}.png`} width={48} height={48}></img> : ""} {!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> : ""} {!inline ? <div className="messageAuthor"><span>{self.dev ? `${author.name ?? "no name"} (${author.username}, ${author.id})` : author.name ?? author.username}</span> {author.dev ? <Badge text="DEV" /> : ""} {author.bot && <Badge text="BOT" />} <Timestamp ts={timestamp} /></div> : ""}
<div className="messageContent">{formattedContent}</div> <div className="messageContent">{formattedContent}</div>
</div> </div>
) )
@@ -147,4 +194,22 @@ function Mention({name, username}: { name?: string, username: string }): ReactEl
return ( return (
<div className="mention">@{name ?? username}</div> <div className="mention">@{name ?? username}</div>
) )
}
function Reply({author, content}: {author: user, content: ReactElement[] | string}): ReactElement {
return (
<div className="reply">
<div className="replyBorderThing"></div>
<span>{self.dev ? `${author.name ?? "no name"} (${author.username}, ${author.id})` : author.name ?? author.username}</span> {author.dev ? <Badge text="DEV" /> : ""} {author.bot && <Badge text="BOT" />}
<div className="replyContent">{content}</div>
</div>
)
}
function MessageHoverActions({reply}: {reply: () => void}): ReactElement {
return (
<div className="messageHoverActions">
<div className="reply" onClick={reply}><ReplyIcon /></div>
</div>
)
} }

View File

@@ -123,6 +123,9 @@ html {
&.bottom { &.bottom {
margin-bottom: 15px; margin-bottom: 15px;
} }
&:hover {
background-color: #444;
}
position: relative; position: relative;
&.sending { &.sending {
color: #777; color: #777;
@@ -136,7 +139,7 @@ html {
border-radius: 100%; border-radius: 100%;
cursor: pointer; cursor: pointer;
&:active { &:active {
top: 1px; transform: translateY(1px);
} }
} }
.messageAuthor { .messageAuthor {
@@ -147,6 +150,7 @@ html {
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
color: var(--role-color);
} }
.messageTimestamp { .messageTimestamp {
font-weight: initial; font-weight: initial;
@@ -177,6 +181,52 @@ html {
font-size: 8pt; font-size: 8pt;
display: inline; display: inline;
} }
.messageHoverActions {
position: absolute;
right: 0;
top: -15px;
}
}
.reply {
margin-left: 31px;
position: relative;
&>span {
cursor: pointer;
color: var(--role-color);
&:hover {
text-decoration: underline;
}
}
.replyBorderThing {
width: 24px;
height: 9px;
display: inline;
position: absolute;
transform: translateY(7px);
left: -28px;
border-left: solid #595a63 2px;
border-top: solid #595a63 2px;
border-top-left-radius: 6px;
}
.replyContent {
top: -1px;
position: relative;
display: inline;
color: #999;
&:hover {
color: white;
cursor: pointer;
}
}
.badge {
background-color: var(--badge-color);
max-width: fit-content;
padding: 2px;
border-radius: 10%;
font-size: 8pt;
display: inline;
}
} }
.textbox { .textbox {
@@ -185,6 +235,8 @@ html {
bottom: 5px; bottom: 5px;
right: 5px; right: 5px;
width: calc(100% - 250px); width: calc(100% - 250px);
max-height: 208px;
overflow-y: scroll;
background-color: #222; background-color: #222;
} }
.editor:empty:before { .editor:empty:before {

View File

@@ -8,4 +8,5 @@ export interface user {
token: null, token: null,
password: null, password: null,
dev: boolean; dev: boolean;
bot: boolean;
} }

View File

@@ -1,4 +1,4 @@
import { message, user } from "@localtypes"; import { id, message, user } from "@localtypes";
let passwd = ""; let passwd = "";
let token = localStorage.getItem("token") ?? ""; let token = localStorage.getItem("token") ?? "";
@@ -16,6 +16,7 @@ let queue: (message | null)[] = new Array(6).fill(null);
const listenersMessages = new Set<() => void>(); const listenersMessages = new Set<() => void>();
const listenersUsers = new Set<() => void>(); const listenersUsers = new Set<() => void>();
let visibleMessages: (message | null)[] = []; let visibleMessages: (message | null)[] = [];
let replyingTo: id | undefined = undefined;
let dirty = true; let dirty = true;
function rebuildVisibleMessages() { function rebuildVisibleMessages() {
@@ -54,6 +55,11 @@ const onclose = () => {
console.log("ws disconnected. reconnecting"); console.log("ws disconnected. reconnecting");
ws = new WebSocket("ws://localhost/ws"); ws = new WebSocket("ws://localhost/ws");
ws.onopen = () => { ws.onopen = () => {
messages = [];
users = [];
queue = new Array(6).fill(null);
dirty = true;
console.log("opened websocket!"); console.log("opened websocket!");
if (!token) { if (!token) {
ws.send(JSON.stringify({ ws.send(JSON.stringify({
@@ -165,6 +171,11 @@ export function getUsers(): user[] {
return users; return users;
} }
export function replyToMessage(id: id) {
replyingTo = id;
console.log(replyingTo);
}
export function sendChatMessage(message: message & { token?: string }) { export function sendChatMessage(message: message & { token?: string }) {
if (queue.filter(m => m !== null).length < 6) { if (queue.filter(m => m !== null).length < 6) {
console.log(message); console.log(message);
@@ -174,6 +185,9 @@ export function sendChatMessage(message: message & { token?: string }) {
i = 6; i = 6;
} }
} }
message.replyTo = replyingTo;
replyingTo = undefined;
console.log(replyingTo);
dirty = true; dirty = true;
notifyMessages(); notifyMessages();
ws.send(JSON.stringify({op: OpcodesServerbound.sendMessage, data: message})); ws.send(JSON.stringify({op: OpcodesServerbound.sendMessage, data: message}));