Chatserver
This commit is contained in:
parent
75f7a7ed61
commit
3cca3b269e
9 changed files with 1001 additions and 22 deletions
123
package-lock.json
generated
123
package-lock.json
generated
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/postgres-kysely": "^0.8.0",
|
"@vercel/postgres-kysely": "^0.8.0",
|
||||||
|
"cookies-next": "^4.1.1",
|
||||||
"formik": "^2.4.5",
|
"formik": "^2.4.5",
|
||||||
"kysely": "^0.27.3",
|
"kysely": "^0.27.3",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
"use-sound": "^4.0.1"
|
"use-sound": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -175,6 +177,11 @@
|
||||||
"node": ">= 10"
|
"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": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
||||||
|
@ -183,6 +190,11 @@
|
||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
|
@ -423,6 +435,29 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
@ -484,6 +519,60 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
@ -1980,6 +2069,32 @@
|
||||||
"loose-envify": "^1.1.0"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/postgres-kysely": "^0.8.0",
|
"@vercel/postgres-kysely": "^0.8.0",
|
||||||
|
"cookies-next": "^4.1.1",
|
||||||
"formik": "^2.4.5",
|
"formik": "^2.4.5",
|
||||||
"kysely": "^0.27.3",
|
"kysely": "^0.27.3",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
"use-sound": "^4.0.1"
|
"use-sound": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
392
src/app/chat/page.tsx
Normal file
392
src/app/chat/page.tsx
Normal file
|
@ -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<string, {[key:string]:any}> = 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<MessageType[]>([]);
|
||||||
|
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<React.ReactNode>(null);
|
||||||
|
let Message = useRef<(message:MessageType) => React.ReactNode>(() => <></>);
|
||||||
|
const [, forceUpdate] = useState({});
|
||||||
|
const externCachedUsers = useRef(new Map());
|
||||||
|
/* END_SECT LHUA; */
|
||||||
|
|
||||||
|
|
||||||
|
const chatInput = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const chatPaneRef = useRef<HTMLDivElement>(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(<div className={styles.textBox}>
|
||||||
|
<textarea rows={2} className={styles.textBoxInput} ref={chatInput}></textarea>
|
||||||
|
<button className={styles.textBoxButton} onClick={() => socket.emit('SIG_CHAT_MESSAGE', { message: chatInput.current?.value }, messageChatRes)}>Send</button>
|
||||||
|
</div>)
|
||||||
|
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 <div className={styles.Message} key={message.id}>
|
||||||
|
<h3 className={styles.MessageHeader}>
|
||||||
|
<a href={`/jams/user/${user?.id}`} target="_blank">{user?.username}@{user?.instance}</a>
|
||||||
|
{" "}
|
||||||
|
<Conditional condition={canDelete}
|
||||||
|
truthy={<button onClick={() => socket.emit('SIG_CHAT_RMMESSAGE', { messageId: message.id }, rmmessageChatRes)} className={styles.deleteButton}>
|
||||||
|
<small>{timeSent}</small>
|
||||||
|
</button>}
|
||||||
|
falsy={<small>{timeSent}</small>}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<Markdown>{message.message}</Markdown>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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 (
|
||||||
|
<ChatLayout
|
||||||
|
title="Chat"
|
||||||
|
subtitle={error}
|
||||||
|
props={{
|
||||||
|
channels,
|
||||||
|
users,
|
||||||
|
currentChannel: channel,
|
||||||
|
connected,
|
||||||
|
// @ts-ignore
|
||||||
|
whoami,
|
||||||
|
chatPaneRef
|
||||||
|
}}
|
||||||
|
footerChildren={textBox}
|
||||||
|
>
|
||||||
|
{messages.length > 0 && !isLoading ?
|
||||||
|
messages.map(Message.current)
|
||||||
|
: (whoami.admin || channels.find(x => x.name == channel)?.writable ?
|
||||||
|
<div style={{textAlign: "center"}}>
|
||||||
|
<h1>This channel is empty.</h1>
|
||||||
|
<p>Use the bar below to post something.</p>
|
||||||
|
</div>
|
||||||
|
: <></>)
|
||||||
|
}
|
||||||
|
</ChatLayout>
|
||||||
|
)
|
||||||
|
}
|
33
src/app/chat/styles.module.css
Normal file
33
src/app/chat/styles.module.css
Normal file
|
@ -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%);
|
||||||
|
}
|
275
src/layout/ChatLayout/ChatLayout.module.css
Normal file
275
src/layout/ChatLayout/ChatLayout.module.css
Normal file
|
@ -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;
|
||||||
|
} */
|
140
src/layout/ChatLayout/ChatLayout.tsx
Normal file
140
src/layout/ChatLayout/ChatLayout.tsx
Normal file
|
@ -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<PropFormat>({
|
||||||
|
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<HTMLDivElement>
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.ChatLayout}`}>
|
||||||
|
<aside className={`${styles.ChatLayout_Aside} ${menuOpen ? styles.Modifier_Open : ""}`}>
|
||||||
|
<h1 style={{marginBottom:0}}>Chat</h1>
|
||||||
|
<p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
< abtmtr.link
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<ConditionalNull condition={props?.connected == true && props?.channels != null}>
|
||||||
|
<p>Channels</p>
|
||||||
|
<div className={styles.Main_List}>
|
||||||
|
{props?.channels?.map((channel) => (<button
|
||||||
|
onClick={channel.action}
|
||||||
|
className={props.currentChannel === channel.name ? styles.Main_List_CurrentLink : ""}
|
||||||
|
title={`#${channel.name}`}
|
||||||
|
>
|
||||||
|
<div className={`fw ${styles.Main_List_Button}`}>
|
||||||
|
<span className="icon">tag</span>
|
||||||
|
<span>{channel.name}</span>
|
||||||
|
</div>
|
||||||
|
</button>))}
|
||||||
|
</div>
|
||||||
|
</ConditionalNull>
|
||||||
|
<ConditionalNull condition={
|
||||||
|
props?.connected == true && props?.channels != null
|
||||||
|
&& props?.connected == true && props?.users != null
|
||||||
|
}>
|
||||||
|
<hr />
|
||||||
|
</ConditionalNull>
|
||||||
|
<ConditionalNull condition={props?.connected == true && props?.users != null}>
|
||||||
|
<p>Users</p>
|
||||||
|
<div className={styles.Main_List}>
|
||||||
|
{props?.users.map((user) => (<Link
|
||||||
|
href={`/jams/user/${user.id}`}
|
||||||
|
className={props.whoami?.id === user.id ? styles.Main_List_CurrentLink : ""}
|
||||||
|
title={`${user.username}@${user.instance}`}
|
||||||
|
>
|
||||||
|
<div className={`fw ${styles.Main_List_Button}`}>
|
||||||
|
<span className="icon">person</span>
|
||||||
|
<span>{user.username}@{user.instance}</span>
|
||||||
|
</div>
|
||||||
|
</Link>))}
|
||||||
|
</div>
|
||||||
|
</ConditionalNull>
|
||||||
|
<ConditionalNull condition={
|
||||||
|
props?.connected == true && props?.users != null
|
||||||
|
}>
|
||||||
|
<hr />
|
||||||
|
</ConditionalNull>
|
||||||
|
<p>Chat API v2</p>
|
||||||
|
<p><i>made with hate for React<br />and love for irisnk.me</i></p>
|
||||||
|
</aside>
|
||||||
|
<div className={styles.ChatLayout_Inner}>
|
||||||
|
<div className={`${
|
||||||
|
styles.ChatLayout_Inner_Header
|
||||||
|
} ${
|
||||||
|
backButton ? styles.Modifier_Back : ""
|
||||||
|
}`}>
|
||||||
|
<ConditionalNull condition={!backButton}>
|
||||||
|
<div className={`${styles.ChatLayout_Inner_Header_Back} ${styles.ChatLayout_Inner_Header_Menu}`}>
|
||||||
|
<button className={styles.ChatLayout_Inner_Header_Back_Button} onClick={() => setMenuOpen(!menuOpen)}>
|
||||||
|
menu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ConditionalNull>
|
||||||
|
<ConditionalNull condition={backButton}>
|
||||||
|
<div className={styles.ChatLayout_Inner_Header_Back}>
|
||||||
|
<button className={styles.ChatLayout_Inner_Header_Back_Button} onClick={() => router.back()}>
|
||||||
|
arrow_back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ConditionalNull>
|
||||||
|
<div className={styles.ChatLayout_Inner_Header_Title}>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<span>{subtitle}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main className={styles.ChatLayout_Inner_Main} ref={props?.chatPaneRef}>
|
||||||
|
<div className={`${styles.ChatLayout_Inner_Main_Content} MainContent`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<div className={`${styles.ChatLayout_Inner_Footer}`}>
|
||||||
|
{footerChildren}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,18 +5,22 @@ import { useState } from "react";
|
||||||
import styles from "./MainLayout.module.css";
|
import styles from "./MainLayout.module.css";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export function Desktop({
|
export function Desktop<PropFormat>({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
currentPage,
|
||||||
backButton = false,
|
backButton = false,
|
||||||
children,
|
children,
|
||||||
|
props,
|
||||||
sidebar
|
sidebar
|
||||||
}:{
|
}:{
|
||||||
title: string,
|
title: string,
|
||||||
subtitle?: string,
|
subtitle?: string,
|
||||||
|
currentPage: string,
|
||||||
backButton?: boolean,
|
backButton?: boolean,
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
sidebar: React.ReactNode
|
props?:PropFormat,
|
||||||
|
sidebar: (x:{currentPage:string, props?:PropFormat}) => React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -24,7 +28,7 @@ export function Desktop({
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.MainLayout}`}>
|
<div className={`${styles.MainLayout}`}>
|
||||||
<aside className={`${styles.MainLayout_Aside} ${menuOpen ? styles.Modifier_Open : ""}`}>
|
<aside className={`${styles.MainLayout_Aside} ${menuOpen ? styles.Modifier_Open : ""}`}>
|
||||||
{sidebar}
|
{sidebar({ currentPage, props })}
|
||||||
</aside>
|
</aside>
|
||||||
<div className={styles.MainLayout_Inner}>
|
<div className={styles.MainLayout_Inner}>
|
||||||
<div className={`${
|
<div className={`${
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { SidebarMain } from "./Sidebar/Main/Main";
|
import { SidebarMain } from "./Sidebar/Main/Main";
|
||||||
import { Desktop } from "./Desktop";
|
import { Desktop } from "./Desktop";
|
||||||
|
|
||||||
export function MainLayout({
|
export function MainLayout<PropFormat>({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
backButton = false,
|
backButton = false,
|
||||||
children,
|
children,
|
||||||
|
props,
|
||||||
sidebar,
|
sidebar,
|
||||||
currentPage
|
currentPage
|
||||||
}:{
|
}:{
|
||||||
|
@ -13,8 +14,9 @@ export function MainLayout({
|
||||||
subtitle?: string,
|
subtitle?: string,
|
||||||
backButton?: boolean,
|
backButton?: boolean,
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
sidebar?: React.ReactNode,
|
props?:PropFormat,
|
||||||
|
sidebar?: (x:{ currentPage:string, props?:PropFormat }) => React.ReactNode,
|
||||||
currentPage: string
|
currentPage: string
|
||||||
}) {
|
}) {
|
||||||
return <Desktop title={title} subtitle={subtitle} backButton={backButton} sidebar={sidebar ?? <SidebarMain currentPage={currentPage} />}>{children}</Desktop>
|
return <Desktop title={title} subtitle={subtitle} currentPage={currentPage} props={props} backButton={backButton} sidebar={sidebar ?? SidebarMain}>{children}</Desktop>
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@ import Link from "next/link";
|
||||||
import styles from "./Main.module.css";
|
import styles from "./Main.module.css";
|
||||||
// import useSound from 'use-sound';
|
// import useSound from 'use-sound';
|
||||||
|
|
||||||
export function SidebarMain({currentPage}:{currentPage:string}) {
|
export function SidebarMain<PropFormat>({currentPage, props}:{currentPage:string, props?:PropFormat}) {
|
||||||
// const [openPlay] = useSound("/sfx/s_open.mp3");
|
// const [openPlay] = useSound("/sfx/s_open.mp3");
|
||||||
// const [impossiblePlay] = useSound("/sfx/s_impossible.mp3");
|
// const [impossiblePlay] = useSound("/sfx/s_impossible.mp3");
|
||||||
// const [hoverPlay] = useSound("/sfx/s_hover.mp3");
|
// const [hoverPlay] = useSound("/sfx/s_hover.mp3");
|
||||||
|
@ -16,7 +16,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className={currentPage === "/" ? styles.Main_List_CurrentLink : ""}
|
className={currentPage === "/" ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage === "/" ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage === "/" ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -25,23 +25,31 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<span>Root</span>
|
<span>Root</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/chat/"
|
||||||
|
className={currentPage.includes("/chat/") ? styles.Main_List_CurrentLink : ""}
|
||||||
|
|
||||||
|
>
|
||||||
|
<div className={`fw ${styles.Main_List_Button}`}>
|
||||||
|
<span className="icon">chat</span>
|
||||||
|
<span>Chat</span>
|
||||||
|
<span className={styles.Main_List_Button_Badge}>NEW</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/jams/"
|
href="/jams/"
|
||||||
className={currentPage.includes("/jams/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/jams/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage === "/" ? impossiblePlay() : openPlay()}
|
|
||||||
// onMouseEnter={hoverPlay}
|
|
||||||
>
|
>
|
||||||
<div className={`fw ${styles.Main_List_Button}`}>
|
<div className={`fw ${styles.Main_List_Button}`}>
|
||||||
<span className="icon">format_paint</span>
|
<span className="icon">format_paint</span>
|
||||||
<span>Jams</span>
|
<span>Jams</span>
|
||||||
<span className={styles.Main_List_Button_Badge}>NEW</span>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/search/"
|
href="/search/"
|
||||||
className={currentPage.includes("/search/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/search/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage === "/" ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage === "/" ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -53,7 +61,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/projects/"
|
href="/projects/"
|
||||||
className={currentPage.includes("/projects/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/projects/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/projects/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/projects/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -65,7 +73,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/characters/"
|
href="/characters/"
|
||||||
className={currentPage.includes("/characters/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/characters/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/characters/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/characters/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -77,7 +85,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/blog/"
|
href="/blog/"
|
||||||
className={currentPage.includes("/blog/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/blog/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/blog/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/blog/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -89,7 +97,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/gallery/"
|
href="/gallery/"
|
||||||
className={currentPage.includes("/gallery/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/gallery/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/gallery/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/gallery/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -101,7 +109,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/stories/"
|
href="/stories/"
|
||||||
className={currentPage.includes("/stories/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/stories/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/stories/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/stories/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -113,7 +121,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/orgs/"
|
href="/orgs/"
|
||||||
className={currentPage.includes("/orgs/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/orgs/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/orgs/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/orgs/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -125,7 +133,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/links/"
|
href="/links/"
|
||||||
className={currentPage.includes("/links/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/links/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/links/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/links/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -137,7 +145,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/oao/"
|
href="/oao/"
|
||||||
className={currentPage.includes("/oao/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/oao/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/oao/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/oao/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
@ -149,7 +157,7 @@ export function SidebarMain({currentPage}:{currentPage:string}) {
|
||||||
<Link
|
<Link
|
||||||
href="/about/"
|
href="/about/"
|
||||||
className={currentPage.includes("/about/") ? styles.Main_List_CurrentLink : ""}
|
className={currentPage.includes("/about/") ? styles.Main_List_CurrentLink : ""}
|
||||||
tabIndex={-1}
|
|
||||||
// onClick={() => currentPage.includes("/about/") ? impossiblePlay() : openPlay()}
|
// onClick={() => currentPage.includes("/about/") ? impossiblePlay() : openPlay()}
|
||||||
// onMouseEnter={hoverPlay}
|
// onMouseEnter={hoverPlay}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in a new issue