150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
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>
|
||
)
|
||
} |