Files
totly-rela-discord/src/components/Message.tsx
2026-01-11 03:50:03 +01:00

150 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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