|
|
@ -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,56 +90,50 @@ 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} />)}
|
|
|
|
|
|
|
|
<div ref={endRef}></div>
|
|
|
|
function onAuthorClick(event) {
|
|
|
|
</ul>
|
|
|
|
if (message.user.uuid !== user.uuid) return;
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
var website = prompt("Set website URL", user.website);
|
|
|
|
|
|
|
|
if (website) updateUser({website});
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (message.user)
|
|
|
|
function Message({message}) {
|
|
|
|
// eslint-disable-next-line react/jsx-no-target-blank
|
|
|
|
if (message.user) {
|
|
|
|
var prefix = <b>{message.user.website ? <a
|
|
|
|
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>
|
|
|
|
href={message.user.website}
|
|
|
|
}
|
|
|
|
target="_blank"
|
|
|
|
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>
|
|
|
|
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) {
|
|
|
|
if (message.file) {
|
|
|
|
let url = BASE_URL + `/file/${message._id}/${message.file.name}`;
|
|
|
|
let url = BASE_URL + `/file/${message._id}/${message.file.name}`;
|
|
|
|
let embed =
|
|
|
|
var file =
|
|
|
|
message.file.type?.startsWith("image") ?
|
|
|
|
message.file.type?.startsWith("image") ?
|
|
|
|
<img src={url} alt={message.file.name} className="max-h-32 inline-block align-top border" />
|
|
|
|
<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") ?
|
|
|
|
: message.file.type?.startsWith("video") ?
|
|
|
|
<video className='max-h-64 inline-block align-top border' controls>
|
|
|
|
<video className='max-h-64 inline-block align-top border' controls>
|
|
|
|
<source src={url} type={message.file.type} />
|
|
|
|
<source src={url} type={message.file.type} />
|
|
|
@ -148,18 +142,10 @@ function MessageList({messages, user, updateUser}) {
|
|
|
|
<audio className='max-h-64 inline-block align-top' controls>
|
|
|
|
<audio className='max-h-64 inline-block align-top' controls>
|
|
|
|
<source src={url} type={message.file.type} />
|
|
|
|
<source src={url} type={message.file.type} />
|
|
|
|
</audio>
|
|
|
|
</audio>
|
|
|
|
: <span>{message.file.name}</span>
|
|
|
|
: <a href={url} target="_blank" rel="noopener" style={{color:"revert",textDecoration:"revert"}}>{message.file.name}</a>
|
|
|
|
// 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}>
|
|
|
|
return <li>{prefix} {content} {file}</li>
|
|
|
|
{prefix} {content} {file}
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
<div ref={endRef}></div>
|
|
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -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
|
|
|
|