diff --git a/package-lock.json b/package-lock.json index 1feef11..1105945 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@vercel/postgres-kysely": "^0.8.0", + "cookies-next": "^4.1.1", "formik": "^2.4.5", "kysely": "^0.27.3", "nanoid": "^5.0.7", @@ -18,6 +19,7 @@ "react-markdown": "^9.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.0", + "socket.io-client": "^4.7.5", "use-sound": "^4.0.1" }, "devDependencies": { @@ -175,6 +177,11 @@ "node": ">= 10" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -183,6 +190,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -423,6 +435,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies-next": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-4.1.1.tgz", + "integrity": "sha512-20QaN0iQSz87Os0BhNg9M71eM++gylT3N5szTlhq2rK6QvXn1FYGPB4eAgU4qFTunbQKhD35zfQ95ZWgzUy3Cg==", + "dependencies": { + "@types/cookie": "^0.6.0", + "@types/node": "^16.10.2", + "cookie": "^0.6.0" + } + }, + "node_modules/cookies-next/node_modules/@types/node": { + "version": "16.18.97", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.97.tgz", + "integrity": "sha512-4muilE1Lbfn57unR+/nT9AFjWk0MtWi5muwCEJqnOvfRQDbSfLCUdN7vCIg8TYuaANfhLOV85ve+FNpiUsbSRg==" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -484,6 +519,60 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1980,6 +2069,32 @@ "loose-envify": "^1.1.0" } }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -2280,6 +2395,14 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index d662fb5..59d4aad 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@vercel/postgres-kysely": "^0.8.0", + "cookies-next": "^4.1.1", "formik": "^2.4.5", "kysely": "^0.27.3", "nanoid": "^5.0.7", @@ -19,6 +20,7 @@ "react-markdown": "^9.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.0", + "socket.io-client": "^4.7.5", "use-sound": "^4.0.1" }, "devDependencies": { diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx new file mode 100644 index 0000000..108fb9d --- /dev/null +++ b/src/app/chat/page.tsx @@ -0,0 +1,392 @@ +'use client'; +import { ChatLayout } from "@/layout/ChatLayout/ChatLayout"; +import { io } from "socket.io-client"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { getCookie } from 'cookies-next'; +import styles from "./styles.module.css"; +import Markdown from "react-markdown"; +import { Conditional } from "@/components/utility/Conditional"; + +type MessageType = { + channel: string, + message: string, + me: boolean, + user: string, + id: string, + timestamp: number +}; + +type Res = {type: string, spec: string, data: any}; + +export default function Home() { + const message_translation_tables = new Map([ + ["ERR_CHAT_JOIN", new Map([ + ["main", "Error when joining chat"], + ["JOINED", "You've already joined Chat"], + ["MALFORMED_TOKEN", "You have a malformed token"], + ["INVALID_TOKEN", "Your token is invalid or expired"], + ["INVALID_USER", "Your token has no user or your user is banned (shame on you)"], + ])], + ["ERR_CHAT_WHOISTHIS", new Map([ + ["main", "Error when fetching user"], + ["NO_USER", "Unauthenticated request"], + ["MALFORMED_USER", "You have a malformed user ID"], + ["INVALID_USER", "No user with this ID"], + ])], + ["ERR_CHAT_CHGCHANNEL", new Map([ + ["main", "Error when changing channel"], + ["NO_USER", "Unauthenticated request"], + ["MALFORMED_CHANNEL", "You have a malformed channel selection; try again"], + ["INVALID_CHANNEL", "No channel with this name"], + ["CHANNEL_UNREADABLE", "You are not authorized to read this channel"] + ])], + ["ERR_CHAT_MESSAGE", new Map([ + ["main", "Error when sending message"], + ["NO_USER", "Unauthenticated request"], + ["MALFORMED_MESSAGE", "You have a malformed message; try again"], + ["DATABASE_ERROR", "The database encountered an error (contact me@abtmtr.link)"], + ["CHANNEL_UNWRITABLE", "You are not authorized to write to this channel"], + ["RATE_LIMIT", "You are being rate-limited"], + ["RATE_LIMIT_BAN", "You have been disconnected due to exceeding the rate-limit"] + ])], + ["ERR_CHAT_RMMESSAGE", new Map([ + ["main", "Error when removing message"], + ["NO_USER", "Unauthenticated request"], + ["MALFORMED_MESSAGEID", "You have a malformed message selection; try again"], + ["NULL_MESSAGE", "No message with this ID"], + ["NUH_UH", "You are not authorized to remove this message"], + ["DATABASE_ERROR", "The database encountered an error (contact me@abtmtr.link)"], + ["RATE_LIMIT", "You are being rate-limited"], + ["RATE_LIMIT_BAN", "You have been disconnected due to exceeding the rate-limit"] + ])], + ["ERR_CHAT_USERS", new Map([ + ["main", "Error when fetching users"], + ["NO_USER", "Unauthenticated request"] + ])], + ["ERR_CHAT_CHANNELS", new Map([ + ["main", "Error when fetching channels"], + ["NO_USER", "Unauthenticated request"], + ["DATABASE_ERROR", "The database encountered an error (contact me@abtmtr.link)"] + ])], + ["ERR_CHAT_MESSAGES", new Map([ + ["main", "Error when fetching messages"], + ["NO_USER", "Unauthenticated request"], + ["DATABASE_ERROR", "The database encountered an error (if this continues, contact me@abtmtr.link)"], + ["CHANNEL_UNREADABLE", "You are not authorized to read this channel"] + ])] + ]); + const data_translation_tables:Map = new Map([ + ["RATE_LIMIT", { + reset: (x:number) => `(Refreshes in ${x} seconds)` + }] + ]) + const socketLocation = "https://chatserver.abtmtr.link/"; + const socketObject = useMemo(() => io(socketLocation), []); + const socket = socketObject; + + const [ connected, setConnected ] = useState(false); + const [ error, _setError ] = useState(""); + const [ whoami, _setWhoAmI ] = useState<{[key:string]:any}>({}); + const whoamiRef = useRef(whoami); + const setWhoAmI = (x:any) => {whoamiRef.current = x;_setWhoAmI(x);}; + const [ channel, setChannel ] = useState(""); + const [ channels, setChannels ] = useState<{ + name: string, + action: () => void, + readable: boolean, + writable: boolean + }[]>([]); + const [ users, _setUsers ] = useState<{ + id: string, + username: string, + instance: string, + admin: boolean + }[]>([]); + const usersRef = useRef(users); + const setUsers = (x:any) => {usersRef.current = x;_setUsers(x);}; + const [ messages, _setMessages ] = useState([]); + const messagesRef = useRef(messages); + const setMessages = (x:any) => {messagesRef.current = x;_setMessages(x);}; + + const [ isLoading, setIsLoading ] = useState(false); + + /* SECT: LHUA; Lord help us all these ones */ + const [ textBox, setTextBox ] = useState(null); + let Message = useRef<(message:MessageType) => React.ReactNode>(() => <>); + const [, forceUpdate] = useState({}); + const externCachedUsers = useRef(new Map()); + /* END_SECT LHUA; */ + + + const chatInput = useRef(null); + const chatPaneRef = useRef(null); + + function unsetError() { + _setError(""); + } + function setError(type:string, spec:string, data?:any) { + let errorTable = message_translation_tables.get(type); + let dataTable = data_translation_tables.get(spec); + + let errorTableType; + let errorTableSpec; + if (errorTable == null) { + errorTableType = type; + errorTableSpec = spec; + } else { + errorTableType = errorTable.get("main"); + if (errorTableType == null) + errorTableType = type; + errorTableSpec = errorTable.get(spec); + if (errorTableSpec == null) + errorTableSpec = spec; + } + + let dataTableSpec; + if (dataTable != null && data != null) { + dataTableSpec = Object.entries(data).reduce((prev, [k,v]) => prev + " " + dataTable[k](v), ""); + } + + if (dataTableSpec == null) + _setError(`${errorTableType}: ${errorTableSpec}`); + else + _setError(`${errorTableType}: ${errorTableSpec} ${dataTableSpec}`) + } + + useEffect(() => { + + /* SECT: BSN; Because "soft navigation" */ + // @ts-ignore + if (window.socket && window.socket !== socket) + // @ts-ignore + window.socket.disconnect(); + // @ts-ignore + window.socket = socket; + /* END_SECT BSN; */ + + function onConnect() { + setConnected(true); + + const token = getCookie('token'); + if (token != null) + socket.emit('SIG_CHAT_JOIN', { token }, joinChatRes); + + setTextBox(
+ + +
) + Message.current = (message:MessageType) => { + let user; + user = usersRef.current.find(x => x.id == message.user); + if (user == null) + user = externCachedUsers.current.get(message.user); + if (user == null) { + new Promise((res, rej) => { + socket.emit('SIG_CHAT_WHOISTHIS', { id: message.user }, ({ type, spec, data }:Res) => { + if (type.includes("ERR")) + rej(`${type} - ${spec}`); + else { + externCachedUsers.current.set(data.id, data); + res(""); + forceUpdate({}); + } + }); + }); + } + const canDelete = whoamiRef.current.admin || whoamiRef.current.id == message.user; + const timeSent = new Date(message.timestamp * 1000).toLocaleTimeString(); + return
+

+ {user?.username}@{user?.instance} + {" "} + socket.emit('SIG_CHAT_RMMESSAGE', { messageId: message.id }, rmmessageChatRes)} className={styles.deleteButton}> + {timeSent} + } + falsy={{timeSent}} + /> +

+ {message.message} +
+ }; + } + function onDisconnect() { + setConnected(false); + } + + if (socket.connected && !connected) { + onConnect(); + } + + socket.on("connect", onConnect); + socket.on("disconnect", onDisconnect); + + window.addEventListener("navigate", socket.disconnect); + + return () => { + socket.off("connect", onConnect); + socket.off("disconnect", onDisconnect); + }; + }, []); + + // user stuff + + function addUser(userdata:{ + id: string, + username: string, + instance: string, + admin: boolean + }) { + setUsers(usersRef.current.concat([ userdata ])); + } + + function removeUser(userId:string) { + const index = usersRef.current.findIndex(x => x.id == userId); + if (index < 0) return; + setUsers(usersRef.current.toSpliced(index, 1)); + } + + function usersChatRes({ type, spec, data }:Res) { + unsetError(); + if (type.includes("ERR")) { + setError(type, spec, data); + } else { + setUsers(data); + } + } + + socket.off("RES_CHAT_JOIN"); + socket.on("RES_CHAT_JOIN", ({ user }) => { + addUser(user); + }); + + socket.off("RES_CHAT_LEAVE"); + socket.on("RES_CHAT_LEAVE", ({ userId }) => { + removeUser(userId); + }); + + const prevscrtop = useRef(0); + useEffect(() => { + if ( + chatPaneRef.current != null + && chatPaneRef.current.scrollTop >= prevscrtop.current + ) { + chatPaneRef.current.scrollTop = chatPaneRef.current.scrollHeight; + prevscrtop.current = chatPaneRef.current.scrollTop; + } + }, [messages]) + + // message stuff + + function addMessage(messagesdata:MessageType) { + setMessages(messagesRef.current.concat([ messagesdata ])); + } + + function removeMessage(messageId:string) { + const index = messagesRef.current.findIndex(x => x.id == messageId); + if (index < 0) return; + setMessages(messagesRef.current.toSpliced(index, 1)); + } + function messagesChatRes({ type, spec, data }:Res) { + unsetError(); + if (type.includes("ERR")) { + setError(type, spec, data); + } else { + setMessages(data); + } + } + + function messageChatRes({ type, spec, data }:Res) { + unsetError(); + if (type.includes("ERR")) { + setError(type, spec, data); + } else { + addMessage(data); + } + if (chatInput.current != null) + chatInput.current.value = ""; + } + + function rmmessageChatRes({ type, spec, data }:Res) { + unsetError(); + if (type.includes("ERR")) { + setError(type, spec, data); + } else { + removeMessage(data.messageId); + } + } + + socket.off("RES_CHAT_MESSAGE"); + socket.on("RES_CHAT_MESSAGE", ({ message }) => { + console.log(messages); + addMessage(message); + }); + + socket.off("RES_CHAT_RMMESSAGE"); + socket.on("RES_CHAT_RMMESSAGE", ({ messageId }) => { + removeMessage(messageId); + }); + + function joinChatRes({ type, spec, data }:Res) { + unsetError(); + if (type.includes("ERR")) { + setError(type, spec, data); + } else { + setWhoAmI(data); + socket.emit('SIG_CHAT_CHANNELS', {}, channelsChatRes); + } + } + + function channelsChatRes({ type, spec, data }:Res) { + unsetError(); + if (type.includes("ERR")) { + setError(type, spec, data); + } else { + setChannels(data.map((x:{[key:string]:any}) => { + x.action = () => { + socket.emit('SIG_CHAT_CHGCHANNEL', { channel: x.name }, chgchannelChatRes); + } + return x; + })); + } + } + + function chgchannelChatRes({ type, spec, data }:Res) { + unsetError(); + if (type.includes("ERR")) { + setError(type, spec, data); + } else { + prevscrtop.current = 0; + setChannel(data.channel); + socket.emit('SIG_CHAT_USERS', {}, usersChatRes); + socket.emit('SIG_CHAT_MESSAGES', {}, messagesChatRes); + } + } + + return ( + + {messages.length > 0 && !isLoading ? + messages.map(Message.current) + : (whoami.admin || channels.find(x => x.name == channel)?.writable ? +
+

This channel is empty.

+

Use the bar below to post something.

+
+ : <>) + } +
+ ) +} \ No newline at end of file diff --git a/src/app/chat/styles.module.css b/src/app/chat/styles.module.css new file mode 100644 index 0000000..cb3a8cb --- /dev/null +++ b/src/app/chat/styles.module.css @@ -0,0 +1,33 @@ +.textBox { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px; +} + +.textBoxInput { + background-color: var(--bg-2); + border: 1px solid var(--fg-2); + color: var(--fg-2); + padding: 4px; + resize: none; +} + +.textBoxButton { + background-color: var(--bg-2); + border: 1px solid var(--fg-2); + color: var(--fg-2); + padding: 8px 16px; +} + +.Message { + margin-top: 16px; + margin-bottom: 32px; +} + +/* .MessageHeader { + font-size: 1.25em; +} */ + +.deleteButton:hover { + color: hsl(0, 20%, 75%); +} \ No newline at end of file diff --git a/src/layout/ChatLayout/ChatLayout.module.css b/src/layout/ChatLayout/ChatLayout.module.css new file mode 100644 index 0000000..302bf0a --- /dev/null +++ b/src/layout/ChatLayout/ChatLayout.module.css @@ -0,0 +1,275 @@ +@keyframes slidein { + 0% { + position: relative; + top: 10px; + opacity: 0; + } + + 99% { + position: relative; + top: 0; + opacity: 1; + } + + 100% { + position: static; + top: unset; + } +} + +@keyframes slidein_header { + 0% { + position: relative; + left: -10px; + opacity: 0; + } + + 99% { + position: relative; + left: 0; + opacity: 1; + } + + 100% { + position: static; + top: unset; + } +} + +.ChatLayout { + width: 100vw; + max-width: 100vw; + height: 100%; + max-height: 100%; + margin: 0; + padding: 0; + + overflow: hidden; + display: grid; + grid-template-columns: 256px 1fr; +} + +.ChatLayout_Aside { + padding: 4px 16px; + background-color: var(--bg-1); + color: var(--fg-1); + border-right: 1px solid var(--neutral); + overflow-y: scroll; +} + +.ChatLayout_Aside.Modifier_Open {} + +.ChatLayout_Inner { + max-width: 100%; + max-height: 100%; + + overflow: hidden; + + display: grid; + grid-template-rows: 48px auto 64px; +} + +.ChatLayout_Inner_Header { + padding: 6px 16px; + background-color: var(--bg-2); + color: var(--fg-2); + border-bottom: 1px solid var(--neutral); + overflow: hidden; + + display: grid; + grid-template-columns: auto; + gap: 8px; + align-items: center; +} + +.ChatLayout_Inner_Header>* { + animation-duration: 0.25s; + animation-name: slidein_header; +} + +.ChatLayout_Inner_Header.Modifier_Back { + padding: 6px 8px; + grid-template-columns: 32px 1fr; +} + +.ChatLayout_Inner_Header_Back_Button { + font-family: var(--font-MaterialSymbols); + font-size: 24px; + width: 32px; + height: 32px; +} + +.ChatLayout_Inner_Header_Menu { + display: none; +} + +.ChatLayout_Inner_Header_Title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + align-content: center; + width: 100%; + overflow: hidden; +} + +.ChatLayout_Inner_Header_Title h1 { + display: inline-block; + margin: 0; + vertical-align: middle; + font-size: 1em; + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.ChatLayout_Inner_Header_Title span { + display: inline-block; + margin: 0; + vertical-align: middle; + font-size: 0.75em; + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.ChatLayout_Inner_Footer { + padding: 6px 16px; + background-color: var(--bg-1); + color: var(--fg-1); + border-top: 1px solid var(--neutral); + overflow: hidden; + + display: grid; + grid-template-columns: auto; + gap: 8px; + align-items: center; +} + +.ChatLayout_Inner_Main { + display: inline-block; + background-color: var(--bg-1); + color: var(--fg-1); + width: 100%; + + overflow: hidden; + overflow-y: auto; + text-align: center; + position: relative; +} + +.ChatLayout_Inner_Main_Content { + display: inline-block; + text-align: left; + overflow: hidden; + background-color: var(--bg-2); + color: var(--fg-2); + padding: 0 16px; + max-width: 50em; + width: 100%; + min-height: 100%; + margin-bottom: -4px; + + /* border-left: 1px solid var(--neutral); + border-right: 1px solid var(--neutral); */ +} + +.ChatLayout_Inner_Main_Content>* { + animation-duration: 0.25s; + animation-name: slidein; +} + +.ChatLayout_Inner_Main_Content img { + max-width: 100%; +} + +@media screen and (max-width: 800px) { + .ChatLayout { + grid-template-columns: 1fr; + } + + .ChatLayout_Aside { + position: absolute; + height: calc(100% - 48px); + width: 100%; + top: 48px; + z-index: 50; + left: -100%; + transition: left 0.125s; + } + + .ChatLayout_Inner_Header_Menu { + display: inline-block; + } + + .ChatLayout_Inner_Header { + padding: 6px 8px; + grid-template-columns: 32px auto; + } + + .ChatLayout_Aside.Modifier_Open { + left: 0; + } +} + +/* sidebar */ + +.Main_Image { + text-align: center; + margin: 16px 0; +} + +.Main_Image img { + border-radius: 8px; +} + +.Main_List { + display: flex; + flex-direction: column; + gap: 8px; +} + +.Main_List>a { + text-decoration: none; +} + +.Main_List_CurrentLink>div { + background-color: var(--bg-2); +} + +.Main_List_Button { + color: var(--accent-color); + overflow: hidden; + text-overflow: ellipsis; +} + +.Main_List_Button_Badge { + background-color: rgb(255, 64, 64); + color: white; + padding: 0 4px; + margin: 4px; + float: right; + border-radius: 4px; + pointer-events: none; +} + +.Main_List_CurrentLink .Main_List_Button_Badge { + display: none; +} + +.Main_List_Button_Slim { + padding: 2px 8px !important; + background-color: var(--bg-3); + color: var(--fg-2); +} + +.Main_List_Button_Slim:hover { + background-color: var(--bg-2); + color: var(--fg-1); +} + +/* .Main_List_Button_Slim:active { + scale: 1; +} */ \ No newline at end of file diff --git a/src/layout/ChatLayout/ChatLayout.tsx b/src/layout/ChatLayout/ChatLayout.tsx new file mode 100644 index 0000000..638405a --- /dev/null +++ b/src/layout/ChatLayout/ChatLayout.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { ConditionalNull } from "@/components/utility/Conditional"; +import React, { useState } from "react"; +import styles from "./ChatLayout.module.css"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; + +export function ChatLayout({ + title, + subtitle, + backButton = false, + children, + footerChildren, + props, +}:{ + title: string, + subtitle?: string, + backButton?: boolean, + children: React.ReactNode, + footerChildren: React.ReactNode, + props?:{ + channels: { + name:string, + action: () => void, + readable: boolean, + writable: boolean + }[], + users: { + id: string, + username: string, + instance: string, + admin: boolean + }[], + currentChannel: string, + connected: boolean, + whoami?: { + id: string, + username: string, + instance: string + }, + chatPaneRef: React.Ref + } +}) { + const [menuOpen, setMenuOpen] = useState(false); + const router = useRouter(); + + return ( +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+

{title}

+ {subtitle} +
+
+
+
+ {children} +
+
+
+ {footerChildren} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/layout/MainLayout/Desktop.tsx b/src/layout/MainLayout/Desktop.tsx index 2462171..361008e 100644 --- a/src/layout/MainLayout/Desktop.tsx +++ b/src/layout/MainLayout/Desktop.tsx @@ -5,18 +5,22 @@ import { useState } from "react"; import styles from "./MainLayout.module.css"; import { useRouter } from "next/navigation"; -export function Desktop({ +export function Desktop({ title, subtitle, + currentPage, backButton = false, children, + props, sidebar }:{ title: string, subtitle?: string, + currentPage: string, backButton?: boolean, children: React.ReactNode, - sidebar: React.ReactNode + props?:PropFormat, + sidebar: (x:{currentPage:string, props?:PropFormat}) => React.ReactNode }) { const [menuOpen, setMenuOpen] = useState(false); const router = useRouter(); @@ -24,7 +28,7 @@ export function Desktop({ return (
({ title, subtitle, backButton = false, children, + props, sidebar, currentPage }:{ @@ -13,8 +14,9 @@ export function MainLayout({ subtitle?: string, backButton?: boolean, children: React.ReactNode, - sidebar?: React.ReactNode, + props?:PropFormat, + sidebar?: (x:{ currentPage:string, props?:PropFormat }) => React.ReactNode, currentPage: string }) { - return }>{children} + return {children} } \ No newline at end of file diff --git a/src/layout/MainLayout/Sidebar/Main/Main.tsx b/src/layout/MainLayout/Sidebar/Main/Main.tsx index 511a9e7..da260a0 100644 --- a/src/layout/MainLayout/Sidebar/Main/Main.tsx +++ b/src/layout/MainLayout/Sidebar/Main/Main.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import styles from "./Main.module.css"; // import useSound from 'use-sound'; -export function SidebarMain({currentPage}:{currentPage:string}) { +export function SidebarMain({currentPage, props}:{currentPage:string, props?:PropFormat}) { // const [openPlay] = useSound("/sfx/s_open.mp3"); // const [impossiblePlay] = useSound("/sfx/s_impossible.mp3"); // const [hoverPlay] = useSound("/sfx/s_hover.mp3"); @@ -16,7 +16,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage === "/" ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -25,23 +25,31 @@ export function SidebarMain({currentPage}:{currentPage:string}) { Root
+ +
+ chat + Chat + NEW +
+ currentPage === "/" ? impossiblePlay() : openPlay()} - // onMouseEnter={hoverPlay} + >
format_paint Jams - NEW
currentPage === "/" ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -53,7 +61,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/projects/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -65,7 +73,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/characters/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -77,7 +85,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/blog/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -89,7 +97,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/gallery/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -101,7 +109,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/stories/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -113,7 +121,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/orgs/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -125,7 +133,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/links/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -137,7 +145,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/oao/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} > @@ -149,7 +157,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) { currentPage.includes("/about/") ? impossiblePlay() : openPlay()} // onMouseEnter={hoverPlay} >