From 1e123c7b74e10effd25d0f64dea18844dd44b8f5 Mon Sep 17 00:00:00 2001 From: Guillaume Dorce Date: Sat, 24 Feb 2024 18:26:41 +0100 Subject: [PATCH] add class to layout, add scrolltobottom, add newmessage, refresh on newmessage, store channel --- src/components/Members/Chat/NewMessage.tsx | 119 ++++++++++++++++++ .../Members/Chat/ScrollToBottom.tsx | 67 ++++++++++ src/components/Members/Home.tsx | 49 ++++++-- src/components/Members/LeftPanel/Channels.tsx | 16 ++- src/layouts/Layout.astro | 5 +- src/pages/espace-membres.astro | 2 +- 6 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 src/components/Members/Chat/NewMessage.tsx create mode 100644 src/components/Members/Chat/ScrollToBottom.tsx diff --git a/src/components/Members/Chat/NewMessage.tsx b/src/components/Members/Chat/NewMessage.tsx new file mode 100644 index 0000000..df478a3 --- /dev/null +++ b/src/components/Members/Chat/NewMessage.tsx @@ -0,0 +1,119 @@ +import { useContext, useRef, useState } from 'preact/hooks'; +import { FaPlus, FaTimes } from 'react-icons/fa'; +import { MdSend } from 'react-icons/md'; +import { Pb } from '../../EspaceMembres'; +import { CurrentChannel } from '../Home'; + +export default function NewMessage() { + const [message, setMessage] = useState(''); + const [image, setImage] = useState(''); + const pb = useContext(Pb); + const [preview, setPreview] = useState(''); + const imageRef = useRef(null); + const { get } = useContext(CurrentChannel); + + const handleImageSelect = (e: any) => { + setImage(e.target.value); + const file = e.target.files[0]; + if (file) { + const src = URL.createObjectURL(file); + setPreview(src); + } + }; + + const handleSubmit = (e: any) => { + e.preventDefault(); + if (message.trim().length === 0 && image.trim().length === 0) { + return; + } + + const data = new FormData(e.target as HTMLFormElement); + + //send message with image to pb + pb.collection('messages').create({ + content: message, + image: data.get('image'), + channel: get().id, + author: pb.authStore.model?.id, + }).then(() => { + setMessage(''); + setImage(''); + setPreview(''); + if (imageRef.current) { + imageRef.current.value = ''; + } + const event = new Event('newMessage'); + document.dispatchEvent(event); + }); + + }; + + const handleRemoveImage = () => { + setImage(''); + setPreview(''); + }; + + const handleInutChange = (e: any) => { + setMessage(e.target.value); + } + + return ( +
+
+ {preview !== '' && ( +
+ Image + + + +
+ )} +
+
+ + +
+ +
+ + +
+ +
+
+
+ ); +}; diff --git a/src/components/Members/Chat/ScrollToBottom.tsx b/src/components/Members/Chat/ScrollToBottom.tsx new file mode 100644 index 0000000..235d449 --- /dev/null +++ b/src/components/Members/Chat/ScrollToBottom.tsx @@ -0,0 +1,67 @@ +import type { ComponentChildren } from 'preact'; +import { useCallback, useMemo, useState } from 'preact/hooks'; +import { FaChevronDown } from 'react-icons/fa'; + +export default function ScrollToBottom ({ children, className = '' }: { children: ComponentChildren; className?: string }) { + const [node, setNode] = useState(null); + const [show, setShow] = useState(false); + + const bottom = useCallback((node: HTMLDivElement) => { + if (node) { + node.scrollIntoView({ behavior: 'smooth' }); + setNode(node); + } + }, []); + + useMemo(() => { + if (node) { + const container = document.querySelector('main > div'); + container?.addEventListener('DOMNodeInserted', (e) => { + if (!(e.target as Element).classList.contains('message')) return; + if (node?.getBoundingClientRect() && node.getBoundingClientRect().y >= window.innerHeight) { + node?.scrollIntoView({ behavior: 'smooth' }); + } + }); + + container?.querySelectorAll('.message > div > img').forEach((img) => { + img.addEventListener('load', () => { + if (node?.getBoundingClientRect() && node.getBoundingClientRect().y >= window.innerHeight) { + node?.scrollIntoView({ behavior: 'smooth' }); + } + }); + }); + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setShow(false); + } else { + setShow(true); + } + }, + { threshold: 1 } + ); + observer.observe(node); + return () => observer.disconnect(); + } + }, [node]); + + return ( + <> +
+ {children} +
+
+ + + ); +}; diff --git a/src/components/Members/Home.tsx b/src/components/Members/Home.tsx index 51f329b..b0c4e20 100644 --- a/src/components/Members/Home.tsx +++ b/src/components/Members/Home.tsx @@ -2,12 +2,19 @@ import LeftPanel from "./LeftPanel" import { Pb } from "../EspaceMembres" import { useContext, useEffect, useState } from "preact/hooks" import Message from "./Chat/Message" +import NewMessage from "./Chat/NewMessage" import type { MessageExpand } from "./Chat/Message" +import ScrollToBottom from "./Chat/ScrollToBottom" +import { createContext } from "preact" + +export const CurrentChannel = createContext({ + get: () => { return { name: '', id: '' } }, + set: (channel: { name: string, id: string }) => { } +}); function Chat() { const pb = useContext(Pb); const [messages, setMessages] = useState([]); - useEffect(() => { async function getMessages() { const data = await pb.collection('messages').getFullList({ @@ -17,23 +24,49 @@ function Chat() { setMessages(data); } + document.addEventListener('newMessage', getMessages); + getMessages(); }, [pb]); return ( -
- {messages.map((message) => ( - - ))} +
+ + {messages.map((message) => ( + + ))} + +
+
+ +
+
) } export default function Home() { + const [channel, setChannel] = useState({ name: '', id: ''}); + + useEffect(() => { + const channel = localStorage.getItem('channel'); + if (channel) { + setChannel(JSON.parse(channel)); + } + }, []); + + useEffect(() => { + if (!channel.id) return; + localStorage.setItem('channel', JSON.stringify(channel)); + console.log(channel); + }, [channel]); + return ( -
- - +
+ channel, set: setChannel }}> + + +
) } diff --git a/src/components/Members/LeftPanel/Channels.tsx b/src/components/Members/LeftPanel/Channels.tsx index ac65539..4bd2126 100644 --- a/src/components/Members/LeftPanel/Channels.tsx +++ b/src/components/Members/LeftPanel/Channels.tsx @@ -1,18 +1,26 @@ import { FaCog } from "react-icons/fa"; import { Pb } from "../../EspaceMembres"; +import { CurrentChannel } from "../Home"; import { useContext, useEffect, useState } from "preact/hooks"; -import type { ChannelsRecord } from "../../../types/pb_types"; +import type { ChannelsResponse } from "../../../types/pb_types"; export default function Channels() { const pb = useContext(Pb); - const [channels, setChannels] = useState>([]); + const { set } = useContext(CurrentChannel); + + const [channels, setChannels] = useState>([]); useEffect(() => { - pb.collection('channels').getFullList().then((data) => { + pb.collection('channels').getFullList().then((data) => { if (!data) return; setChannels(data); + set({ name: data[0].name ?? '', id: data[0].id ?? ''}); }); }, []); + const selectChannel = (channel: { name: string, id: string }) => { + set(channel); + } + return (
@@ -23,7 +31,7 @@ export default function Channels() {
    {channels.map((channel) => ( -
  • +
  • selectChannel({ name: channel.name ?? '', id: channel.id ?? '' })}>

    # {channel.name}

    diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 187e0e3..c8b9d07 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -5,9 +5,10 @@ import Footer from '../components/Footer.astro'; interface Props { title: string; footer?: boolean; + className?: string; } -const { title, footer = true } = Astro.props; +const { title, footer = true, className } = Astro.props; --- @@ -20,7 +21,7 @@ const { title, footer = true } = Astro.props; {title} - +
    diff --git a/src/pages/espace-membres.astro b/src/pages/espace-membres.astro index d86b193..0364968 100644 --- a/src/pages/espace-membres.astro +++ b/src/pages/espace-membres.astro @@ -2,6 +2,6 @@ import Layout from '../layouts/Layout.astro'; import EspaceMembres from '../components/EspaceMembres.tsx'; --- - +