Compare commits

...

2 Commits

Author SHA1 Message Date
f6df3901b1 crappy message history load 2022-09-18 16:52:51 -05:00
92d1faf3e5 emoji selector improvements 2022-09-15 00:03:10 -05:00
3 changed files with 101 additions and 105 deletions

View File

@ -1,42 +1,45 @@
{ {
"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": {
"browserslist": { "react/jsx-no-target-blank": "off"
"production": [ }
">0.2%", },
"not dead", "browserslist": {
"not op_mini all" "production": [
], ">0.2%",
"development": [ "not dead",
"last 1 chrome version", "not op_mini all"
"last 1 firefox version", ],
"last 1 safari version" "development": [
] "last 1 chrome version",
}, "last 1 firefox version",
"devDependencies": { "last 1 safari version"
"autoprefixer": "^10.4.8", ]
"postcss": "^8.4.16", },
"tailwindcss": "^3.1.8" "devDependencies": {
} "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} user={user} updateUser={updateUser} /> <MessageList messages={messages} setMessages={setMessages} />
<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,78 +90,64 @@ function Chat({user, setUser}) {
function MessageList({messages, user, updateUser}) { function MessageList({messages, setMessages}) {
var [atBottom, setAtBottom] = useState(true);
var [scrollLock, setScrollLock] = useState(true); var [atTop, setAtTop] = useState(false);
var endRef = useRef(); var endRef = useRef();
function onScroll(event) { function onScroll(event) {
var { scrollTop, scrollHeight, clientHeight } = event.target; var { scrollTop, scrollHeight, clientHeight } = event.target;
setScrollLock(scrollHeight - (scrollTop + clientHeight) < 32); setAtBottom(scrollHeight - (scrollTop + clientHeight) < 32);
setAtTop(scrollTop < 32);
}; };
function scrollToBottom() {
endRef.current?.scrollIntoView();
}
useEffect(() => {
if (scrollLock) scrollToBottom();
});
var observer = new ResizeObserver(entries => { var scrollToBottom = () => endRef.current?.scrollIntoView();
if (scrollLock) scrollToBottom(); 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]);
}
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 => { {messages.map(message => <Message message={message} key={message._id} />)}
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>
}
@ -244,8 +230,10 @@ 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='w-64 h-64 rounded border fixed right-6 bottom-16 overflow-auto bg-white dark:bg-black'> return <div className='fixed top-0 left-0 w-full h-full' onClick={e=>setOpen(false)}>
{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} />)} <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-10 h-10 inline-block p-0.5 cursor-pointer border border-transparent hover:border-gray-600" onClick={onEmojiClick} />)}
</div>
</div> </div>
} }
@ -343,7 +331,6 @@ 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,6 +24,12 @@ 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});
@ -107,7 +113,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}).sort({timestamp: -1}).limit(100).toArray(); var history = await messages.find().project({"file.data": 0, "user.ip": 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", () => {