Compare commits
2 Commits
5c4d258384
...
f6df3901b1
Author | SHA1 | Date | |
---|---|---|---|
f6df3901b1 | |||
92d1faf3e5 |
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
115
app/src/App.js
115
app/src/App.js
@ -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
|
||||||
|
@ -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", () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user