Compare commits

..

No commits in common. "f6df3901b188fc1385bae919404b9909225ffeb2" and "5c4d2583840d657ceee785aab96823c07741cb19" have entirely different histories.

3 changed files with 106 additions and 102 deletions

View File

@ -1,45 +1,42 @@
{ {
"name": "chat", "name": "chat",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-string-replace": "^1.1.0", "react-string-replace": "^1.1.0",
"socket.io-client": "^4.5.2", "socket.io-client": "^4.5.2",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
], ]
"rules": { },
"react/jsx-no-target-blank": "off" "browserslist": {
} "production": [
}, ">0.2%",
"browserslist": { "not dead",
"production": [ "not op_mini all"
">0.2%", ],
"not dead", "development": [
"not op_mini all" "last 1 chrome version",
], "last 1 firefox version",
"development": [ "last 1 safari version"
"last 1 chrome version", ]
"last 1 firefox version", },
"last 1 safari version" "devDependencies": {
] "autoprefixer": "^10.4.8",
}, "postcss": "^8.4.16",
"devDependencies": { "tailwindcss": "^3.1.8"
"autoprefixer": "^10.4.8", }
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8"
}
} }

View File

@ -76,7 +76,7 @@ function Chat({user, setUser}) {
return ( return (
<div className="Chat h-full flex flex-col"> <div className="Chat h-full flex flex-col">
<MessageList messages={messages} setMessages={setMessages} /> <MessageList messages={messages} user={user} updateUser={updateUser} />
<UserList users={users} updateUser={updateUser} user={user} socket={socket} /> <UserList users={users} updateUser={updateUser} user={user} socket={socket} />
<ChatInput sendMessage={sendMessage} user={user} updateUser={updateUser} setSettingsModalOpen={setSettingsModalOpen} socket={socket} /> <ChatInput sendMessage={sendMessage} user={user} updateUser={updateUser} setSettingsModalOpen={setSettingsModalOpen} socket={socket} />
<SettingsModal open={settingsModalOpen} setOpen={setSettingsModalOpen} user={user} updateUser={updateUser} /> <SettingsModal open={settingsModalOpen} setOpen={setSettingsModalOpen} user={user} updateUser={updateUser} />
@ -90,64 +90,78 @@ function Chat({user, setUser}) {
function MessageList({messages, setMessages}) { function MessageList({messages, user, updateUser}) {
var [atBottom, setAtBottom] = useState(true);
var [atTop, setAtTop] = useState(false);
var endRef = useRef();
var [scrollLock, setScrollLock] = useState(true);
var endRef = useRef();
function onScroll(event) { function onScroll(event) {
var { scrollTop, scrollHeight, clientHeight } = event.target; var { scrollTop, scrollHeight, clientHeight } = event.target;
setAtBottom(scrollHeight - (scrollTop + clientHeight) < 32); setScrollLock(scrollHeight - (scrollTop + clientHeight) < 32);
setAtTop(scrollTop < 32);
}; };
function scrollToBottom() {
var scrollToBottom = () => endRef.current?.scrollIntoView(); endRef.current?.scrollIntoView();
useEffect(() => {
if (atBottom) scrollToBottom();
});
var observer = new ResizeObserver(() => atBottom && scrollToBottom());
useEffect(()=>{
if (atTop) loadOlderMessages();
}, [atTop]);
async function loadOlderMessages() {
var olderMessages = await fetch(BASE_URL+"/messages?before="+messages[0].timestamp).then(res => res.json());
console.debug("loadOlderMessages", olderMessages);
setMessages(messages => [...olderMessages, ...messages]);
} }
useEffect(() => {
if (scrollLock) scrollToBottom();
});
var observer = new ResizeObserver(entries => {
if (scrollLock) scrollToBottom();
});
return <ul className="p-4 w-full flex-1 overflow-y-auto break-words" onScroll={onScroll} onLoad={e => observer.observe(e.target)}> return <ul className="p-4 w-full flex-1 overflow-y-auto break-words" onScroll={onScroll} onLoad={e => observer.observe(e.target)}>
{messages.map(message => <Message message={message} key={message._id} />)} {messages.map(message => {
function onAuthorClick(event) {
if (message.user.uuid !== user.uuid) return;
event.preventDefault();
var website = prompt("Set website URL", user.website);
if (website) updateUser({website});
}
if (message.user)
// eslint-disable-next-line react/jsx-no-target-blank
var prefix = <b>{message.user.website ? <a
href={message.user.website}
target="_blank"
rel="noopener"
//onClick={onAuthorClick}
className={message.user.website && "hover:underline"}
>{message.user.name}</a> : message.user.name}: </b>
var content = <span
className={(message.user ? '' : " font-bold")}
style={{color: message.color || message.user?.color}}
title={new Date(message.timestamp).toLocaleString()}
>{processMessageContent(message.content)}</span>
if (message.file) {
let url = BASE_URL + `/file/${message._id}/${message.file.name}`;
let embed =
message.file.type?.startsWith("image") ?
<img src={url} alt={message.file.name} className="max-h-32 inline-block align-top border" />
: message.file.type?.startsWith("video") ?
<video className='max-h-64 inline-block align-top border' controls>
<source src={url} type={message.file.type} />
</video>
: message.file.type?.startsWith("audio") ?
<audio className='max-h-64 inline-block align-top' controls>
<source src={url} type={message.file.type} />
</audio>
: <span>{message.file.name}</span>
// eslint-disable-next-line react/jsx-no-target-blank
var file = <a href={url} target="_blank" rel="noopener" style={{color:"revert",textDecoration:"revert"}}>{embed}</a>;
}
return <li key={message._id}>
{prefix} {content} {file}
</li>
})}
<div ref={endRef}></div> <div ref={endRef}></div>
</ul> </ul>
} }
function Message({message}) {
if (message.user) {
var prefix = <b>{message.user.website ? <a href={message.user.website} target="_blank" rel="noopener" className={message.user.website && "hover:underline"}>{message.user.name}</a> : message.user.name}: </b>
}
var content = <span className={(message.user ? '' : " font-bold")} style={{color: message.color || message.user?.color}} title={new Date(message.timestamp).toLocaleString()}>{processMessageContent(message.content)}</span>
if (message.file) {
let url = BASE_URL + `/file/${message._id}/${message.file.name}`;
var file =
message.file.type?.startsWith("image") ?
<a href={url} target="_blank" rel="noopener" style={{color:"revert",textDecoration:"revert"}}><img src={url} alt={message.file.name} className="max-h-32 inline-block align-top border" /></a>
: message.file.type?.startsWith("video") ?
<video className='max-h-64 inline-block align-top border' controls>
<source src={url} type={message.file.type} />
</video>
: message.file.type?.startsWith("audio") ?
<audio className='max-h-64 inline-block align-top' controls>
<source src={url} type={message.file.type} />
</audio>
: <a href={url} target="_blank" rel="noopener" style={{color:"revert",textDecoration:"revert"}}>{message.file.name}</a>
}
return <li>{prefix} {content} {file}</li>
}
@ -230,10 +244,8 @@ function EmojiPicker({open, setOpen, setChatInputContent}) {
setChatInputContent(content => `${content} :${event.target.dataset.emoji}:`); setChatInputContent(content => `${content} :${event.target.dataset.emoji}:`);
}; };
if (open) if (open)
return <div className='fixed top-0 left-0 w-full h-full' onClick={e=>setOpen(false)}> return <div className='w-64 h-64 rounded border fixed right-6 bottom-16 overflow-auto bg-white dark:bg-black'>
<div className='w-[258px] h-64 rounded border fixed right-6 bottom-16 overflow-auto bg-white dark:bg-black p-2' onClick={e=>e.stopPropagation()}> {emojis.map(emoji => <img src={BASE_URL+"/emoji/"+emoji} title={`:${emoji}:`} alt={`:${emoji}:`} key={emoji} data-emoji={emoji} className="w-8 h-8 inline-block m-1 cursor-pointer hover:border" onClick={onEmojiClick} />)}
{emojis.map(emoji => <img src={BASE_URL+"/emoji/"+emoji} title={`:${emoji}:`} alt={`:${emoji}:`} key={emoji} data-emoji={emoji} className="w-10 h-10 inline-block p-0.5 cursor-pointer border border-transparent hover:border-gray-600" onClick={onEmojiClick} />)}
</div>
</div> </div>
} }
@ -331,6 +343,7 @@ function processMessageContent(content) {
if (!content) return; if (!content) return;
// hyperlinks // hyperlinks
content = reactStringReplace(content, /(https?:\/\/\S+)/gi, link => content = reactStringReplace(content, /(https?:\/\/\S+)/gi, link =>
// eslint-disable-next-line react/jsx-no-target-blank
<a href={link} target="_blank" rel="noopener" style={{color: "revert", textDecoration: "revert"}}>{link}</a> <a href={link} target="_blank" rel="noopener" style={{color: "revert", textDecoration: "revert"}}>{link}</a>
) )
// emoji // emoji

View File

@ -24,12 +24,6 @@ app.use((req, res, next) => {
res.header("Access-Control-Allow-Methods", "*"); res.header("Access-Control-Allow-Methods", "*");
next(); next();
}); });
app.get("/messages", (req, res, next) => {
var query = req.query.before ? {timestamp: {$lt: new Date(req.query.before)}} : {};
messages.find(query).project({"file.data": 0, "user.ip": 0}).sort({timestamp: -1}).limit(100).toArray().then(messages => {
res.send(messages.reverse());
}).catch(e => next(e));
});
app.get("/file/:message_id/:filename", async (req, res, next) => { app.get("/file/:message_id/:filename", async (req, res, next) => {
try { try {
var doc = await messages.findOne({_id: new ObjectId(req.params.message_id)}, {file:1}); var doc = await messages.findOne({_id: new ObjectId(req.params.message_id)}, {file:1});
@ -113,7 +107,7 @@ dbclient.connect().then(async () => {
type: m.file.type?.substring(0,64) type: m.file.type?.substring(0,64)
} : undefined } : undefined
})); }));
var history = await messages.find().project({"file.data": 0, "user.ip": 0}).sort({timestamp: -1}).limit(100).toArray(); var history = await messages.find().project({"file.data": 0}).sort({timestamp: -1}).limit(100).toArray();
history = history.reverse(); history = history.reverse();
socket.emit("messages", history); socket.emit("messages", history);
socket.on("type", () => { socket.on("type", () => {