This commit is contained in:
2026-01-18 21:04:49 +01:00
parent 2226d8bcfc
commit 34f3552a40
4 changed files with 100 additions and 15 deletions

View File

@@ -2,7 +2,7 @@ import { user } from "@localtypes";
import { sendChatMessage, token, self, getUsers, subscribeUsers } from "@ws";
import { useRef, useState, useSyncExternalStore, type ReactElement } from "react";
export function Chatbar(): ReactElement {
export function Chatbar({channelName}: {channelName: string}): ReactElement {
const [message, setMessage] = useState("");
const users: user[] = useSyncExternalStore(
subscribeUsers,
@@ -12,11 +12,11 @@ export function Chatbar(): ReactElement {
const editorRef = useRef<HTMLDivElement | null>(null);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === "Enter" && !event.shiftKey) {
if (event.key === "Enter" && !event.shiftKey && message.trim().length !== 0) {
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});
sendChatMessage({id: Date.now(), author: self, content: msg, timestamp: Date.now(), token: token!, hasSent: false, replyTo: undefined});
console.log("Sent message:", msg);
setMessage("");
if (editorRef.current) {
@@ -57,10 +57,12 @@ export function Chatbar(): ReactElement {
<div
ref={editorRef}
className="editor"
id="editor"
contentEditable
tabIndex={0}
onKeyDown={handleKeyDown}
onInput={handleChange}
data-text="Type here..."
data-text={`Message ${channelName}`}
></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 { user, message } from "@localtypes";
import { subscribeUsers, getUsers, subscribeMessages, getMessages, self } from "@ws";
import { RefObject, useEffect, useRef, useState, useSyncExternalStore, type ReactElement } from "react";
import { user, message, id } from "@localtypes";
import { subscribeUsers, getUsers, subscribeMessages, getMessages, self, replyToMessage } from "@ws";
import { ReplyIcon } from "@components/Icons.tsx";
function formatUnixTimestamp(ts: number) {
const date = new Date(ts);
@@ -23,13 +24,11 @@ function formatUnixTimestamp(ts: number) {
export function RenderMessages(): ReactElement {
let currentAuthor: user | null = null;
const messages: (message | null)[] = useSyncExternalStore(
const messages: (null | message)[] = useSyncExternalStore(
subscribeMessages,
getMessages
);
console.log(messages);
const bottomRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
@@ -53,11 +52,13 @@ export function RenderMessages(): ReactElement {
key={message.id}
author={message.author}
content={message.content}
inline={inline}
id={message.id}
inline={inline && !message.replyTo}
bottom={isLastMessageFromAuthor}
timestamp={message.timestamp}
hasSent={message.hasSent}
mention={message.content.includes(`<@${self.id}>`) || message.content.includes("@everyone") || message.content.includes("@here")}
repliesTo={message.replyTo}
/>
);
} 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(
subscribeUsers,
getUsers
);
const messages: (message | null)[] = useSyncExternalStore(
subscribeMessages,
getMessages
)
const [hover, setHover] = useState(false);
function formatContent(content: string): ReactElement[] {
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);
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 (
<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 ? <div className="messageAuthor"><span>{self.dev ? `${author.name} (${author.username}, ${author.id})` : author.name ?? author.username}</span> {author.dev ? <Badge text="DEV" /> : ""} {author.bot && <Badge text="BOT" />} <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>
)
@@ -147,4 +194,22 @@ function Mention({name, username}: { name?: string, username: string }): ReactEl
return (
<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

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