Compare commits
	
		
			No commits in common. "f6df3901b188fc1385bae919404b9909225ffeb2" and "5c4d2583840d657ceee785aab96823c07741cb19" have entirely different histories.
		
	
	
		
			f6df3901b1
			...
			5c4d258384
		
	
		
| @ -20,10 +20,7 @@ | |||||||
|     "extends": [ |     "extends": [ | ||||||
|       "react-app", |       "react-app", | ||||||
|       "react-app/jest" |       "react-app/jest" | ||||||
| 		], |     ] | ||||||
| 		"rules": { |  | ||||||
| 			"react/jsx-no-target-blank": "off" |  | ||||||
| 		} |  | ||||||
|   }, |   }, | ||||||
|   "browserslist": { |   "browserslist": { | ||||||
|     "production": [ |     "production": [ | ||||||
|  | |||||||
| @ -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,50 +90,56 @@ 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 => { | ||||||
| 		<div ref={endRef}></div> |  | ||||||
| 	</ul> |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function Message({message}) { | 			function onAuthorClick(event) { | ||||||
| 	if (message.user) { | 				if (message.user.uuid !== user.uuid) return; | ||||||
| 		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> | 				event.preventDefault(); | ||||||
|  | 				var website = prompt("Set website URL", user.website); | ||||||
|  | 				if (website) updateUser({website}); | ||||||
| 			} | 			} | ||||||
| 	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.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) { | 			if (message.file) { | ||||||
| 				let url = BASE_URL + `/file/${message._id}/${message.file.name}`; | 				let url = BASE_URL + `/file/${message._id}/${message.file.name}`; | ||||||
| 		var file = | 				let embed = | ||||||
| 					message.file.type?.startsWith("image") ? | 					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> | 						<img src={url} alt={message.file.name} className="max-h-32 inline-block align-top border" /> | ||||||
| 					: 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} /> | ||||||
| @ -142,10 +148,18 @@ function Message({message}) { | |||||||
| 						<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> | ||||||
| 			: <a href={url} target="_blank" rel="noopener" style={{color:"revert",textDecoration:"revert"}}>{message.file.name}</a> | 					: <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>{prefix} {content} {file}</li> | 			return <li key={message._id}> | ||||||
|  | 				{prefix} {content} {file} | ||||||
|  | 			</li> | ||||||
|  | 
 | ||||||
|  | 		})} | ||||||
|  | 		<div ref={endRef}></div> | ||||||
|  | 	</ul> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -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
 | ||||||
|  | |||||||
| @ -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", () => { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user