ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 12. Arrow down & notification
    사이드프로젝트 2024. 7. 24. 02:58

    https://www.youtube.com/watch?v=-xXASlyU0Ck&t=2007s

    를 보며 실습하며 정리하는 글

     

    ---

     

    다음으로 스크롤을 올렸을때 제일 밑으로 내려오게하는 화살표 모양을 만들겠다.

     

    components/ListMessages에 들어가 작업해준다.

    "use client";
    
    import { IMessage, useMessage } from "@/lib/store/messages";
    import React, { useEffect, useRef, useState } from "react";
    import Message from "./Message";
    import { DeleteAlert, EditAlert } from "./MessageActions";
    import { createClient } from "@/utils/supabase/client";
    import { toast } from "sonner";
    import { ArrowDown } from "lucide-react";
    
    const ListMessages = () => {
      // const { messages, addMessage, optimisticIds } = useMessage((state) => state); // zustand에 전역으로 저장된 state를 불러옴. // addMessage는 예전에 만든 메세지를 추가하는 메서드
      const {
        messages,
        addMessage,
        optimisticIds,
        optimisticDeleteMessage,
        optimisticUpdateMessage,
      } = useMessage((state) => state); // zustand에 전역으로 저장된 state를 불러옴. // addMessage는 예전에 만든 메세지를 추가하는 메서드
      const scrollRef = useRef() as React.MutableRefObject<HTMLDivElement>;
    
      const [userScrolled, setUserScrolled] = useState(false); // scroll관리를 위한 state
    
      const supabase = createClient();
    
      useEffect(() => {
        console.log("messages : ", messages);
        const channel = supabase
          .channel("chat-room") // room id를 넣어준다. 여기서는 채팅방이 하나만 있으므로 chat-room으로 전부 통일시킨거다.
          .on(
            "postgres_changes",
            { event: "INSERT", schema: "public", table: "messages" },
            async (payload) => {
              console.log("Change received!", payload);
    
              if (optimisticIds.includes(payload.new.id)) {
                return;
              }
    
              // 받아온 페이로드로 user 정보를 가져오기
              const { error, data } = await supabase
                .from("users")
                .select("*")
                .eq("id", payload.new.user_id)
                .single();
    
              if (error) {
                toast.error(error.message);
              } else {
                const newMessage = {
                  ...payload.new,
                  users: data,
                };
                addMessage(newMessage as IMessage);
              }
            }
          )
          .on(
            "postgres_changes",
            { event: "DELETE", schema: "public", table: "messages" },
            (payload) => {
              console.log("Change received!", payload);
              optimisticDeleteMessage(payload.old.id);
            }
          )
          .on(
            "postgres_changes",
            { event: "UPDATE", schema: "public", table: "messages" },
            (payload) => {
              console.log("Change received!", payload);
              optimisticUpdateMessage(payload.new as IMessage);
            }
          )
          .subscribe();
    
        return () => {
          channel.unsubscribe();
        };
      }, [messages]);
    
      useEffect(() => {
        const scrollContainer = scrollRef.current;
    
        if (scrollContainer) {
          scrollContainer.scrollTop = scrollContainer.scrollHeight;
        }
      }, [messages]);
    
      const handleOnScroll = () => {
        const scrollContainer = scrollRef.current;
        if (scrollContainer) {
          // scroll 높이를 계산해 밑에있으면 false, 위로 뜨면 true
          const isScroll =
            scrollContainer.scrollTop <
            scrollContainer.scrollHeight - scrollContainer.clientHeight - 10;
    
          setUserScrolled(isScroll);
        }
      };
    
      // 스크롤을 내려주는 함수
      const scrollDown = () => {
        scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      };
    
      return (
        <div
          className="flex-1  flex flex-col p-5 h-full overflow-y-auto"
          ref={scrollRef}
          onScroll={handleOnScroll}
        >
          <div className="flex-1"></div>
          <div className="space-y-7">
            {messages.map((value, idx) => {
              return <Message key={idx} message={value} />;
            })}
          </div>
          {/* 아래로 가는 화살표. 아래로 내려갔을때만 표시되도록. */}
          {userScrolled && (
            <div className="absolute bottom-20 right-1/2">
              <div
                className="w-10 h-10 bg-blue-500 rounded-full flex justify-center items-center mx-auto border cursor-pointer hover:scale-110 transition-all"
                onClick={scrollDown}
              >
                <ArrowDown />
              </div>
            </div>
          )}
    
          <DeleteAlert />
          <EditAlert />
        </div>
      );
    };
    
    export default ListMessages;

     

    먼저

    const [userScrolled, setUserScrolled] = useState(false); // scroll관리를 위한 state

     

    이렇게 scroll관리를 위한 state를 만들고

    return (
        <div
          className="flex-1  flex flex-col p-5 h-full overflow-y-auto"
          ref={scrollRef}
          onScroll={handleOnScroll}
        >
          <div className="flex-1"></div>
          <div className="space-y-7">
            {messages.map((value, idx) => {
              return <Message key={idx} message={value} />;
            })}
          </div>
          {/* 아래로 가는 화살표. 아래로 내려갔을때만 표시되도록. */}
          {userScrolled && (
            <div className="absolute bottom-20 right-1/2">
              <div
                className="w-10 h-10 bg-blue-500 rounded-full flex justify-center items-center mx-auto border cursor-pointer hover:scale-110 transition-all"
                onClick={scrollDown}
              >
                <ArrowDown />
              </div>
            </div>
          )}
    
          <DeleteAlert />
          <EditAlert />
        </div>
      );

    여기서 userScroll이 true일 때만 화살표가 보여주도록 한다. ( <ArrowDown/> 은 lucid-react에서 임포트한다.)

     

    const handleOnScroll = () => {
        const scrollContainer = scrollRef.current;
        if (scrollContainer) {
          // scroll 높이를 계산해 밑에있으면 false, 위로 뜨면 true
          const isScroll =
            scrollContainer.scrollTop <
            scrollContainer.scrollHeight - scrollContainer.clientHeight - 10;
    
          setUserScrolled(isScroll);
        }
      };
      
      <div
          className="flex-1  flex flex-col p-5 h-full overflow-y-auto"
          ref={scrollRef}
          onScroll={handleOnScroll  /* 스크롤했을 때 이벤트를 달아준다! */}
        >

    그리고 scrollRef가 달린 div에 onScroll Event를 달아주고, 함수를 붙여준다.

    함수안에서는 scroll이 밑에있으면 false, 떠있으면 True로 상태를 관리해준다. 이렇게 하면 true가 됬을 때만 화살표가 보이게된다.

    ({userScrolled && (
            <div className="absolute bottom-20 right-1/2">
              <div
                className="w-10 h-10 bg-blue-500 rounded-full flex justify-center items-center mx-auto border cursor-pointer hover:scale-110 transition-all"
                onClick={scrollDown}
              >
                <ArrowDown />
              </div>
            </div>
          )} 으로 true일때만 화살표가 보이게 해줬기 때문에.)

     

     

     // 스크롤을 내려주는 함수
      const scrollDown = () => {
        scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      };
      
      {/* 아래로 가는 화살표. 아래로 내려갔을때만 표시되도록. */}
          {userScrolled && (
            <div className="absolute bottom-20 right-1/2">
              <div
                className="w-10 h-10 bg-blue-500 rounded-full flex justify-center items-center mx-auto border cursor-pointer hover:scale-110 transition-all"
                onClick={scrollDown}
              >
                <ArrowDown />
              </div>
            </div>
          )}

    마지막으로 스크롤을 내리는 함수만 만들어주면, 화살표를 누르면 스크롤 누르는 기능이 완성되었다!

     

    ++ 현재, 유저가 스크롤을 올려 이전 메세지를 보고 있는데, 새로운 메세지가 오면 바로 아래로 내려가버리는 버그가 있다.

    이를 고쳐주기 위해

    useEffect(() => {
        const scrollContainer = scrollRef.current;
    
        if (scrollContainer && !userScrolled) {
          scrollContainer.scrollTop = scrollContainer.scrollHeight;
        }
      }, [messages]);

    useEffect의 if 조건에 !userScrolled를 AND조건으로 붙여준다.

     

    이게 뭐냐면, 유저가 스크롤을 가장 아래로 내려서 최신 메세지를 보고 있을때는 새로운 메세지가 오면 자동으로 같이 스크롤이 내려가지만,

    스크롤을 올려 이전 메세지를 보고있을때는 userScrolled가 true이기 때문에, 새로운 메세지가 와도 스크롤 상태를 유지하게 된다.

     

    이제, 새로운 메세지가 왔을 때 알림기능을 만들어 보겠다.

     

    // components/ListMessages.tsx

    "use client";
    
    import { IMessage, useMessage } from "@/lib/store/messages";
    import React, { useEffect, useRef, useState } from "react";
    import Message from "./Message";
    import { DeleteAlert, EditAlert } from "./MessageActions";
    import { createClient } from "@/utils/supabase/client";
    import { toast } from "sonner";
    import { ArrowDown } from "lucide-react";
    
    const ListMessages = () => {
      // const { messages, addMessage, optimisticIds } = useMessage((state) => state); // zustand에 전역으로 저장된 state를 불러옴. // addMessage는 예전에 만든 메세지를 추가하는 메서드
      const {
        messages,
        addMessage,
        optimisticIds,
        optimisticDeleteMessage,
        optimisticUpdateMessage,
      } = useMessage((state) => state); // zustand에 전역으로 저장된 state를 불러옴. // addMessage는 예전에 만든 메세지를 추가하는 메서드
      const scrollRef = useRef() as React.MutableRefObject<HTMLDivElement>;
    
      const [userScrolled, setUserScrolled] = useState(false); // scroll관리를 위한 state
      const [notification, setNotification] = useState(0); // 새로운 메세지가 왔을때 notification을 위한 state
    
      const supabase = createClient();
    
      useEffect(() => {
        console.log("messages : ", messages);
        const channel = supabase
          .channel("chat-room") // room id를 넣어준다. 여기서는 채팅방이 하나만 있으므로 chat-room으로 전부 통일시킨거다.
          .on(
            "postgres_changes",
            { event: "INSERT", schema: "public", table: "messages" },
            async (payload) => {
              console.log("Change received!", payload);
    
              if (optimisticIds.includes(payload.new.id)) {
                return;
              }
    
              // 받아온 페이로드로 user 정보를 가져오기
              const { error, data } = await supabase
                .from("users")
                .select("*")
                .eq("id", payload.new.user_id)
                .single();
    
              if (error) {
                toast.error(error.message);
              } else {
                const newMessage = {
                  ...payload.new,
                  users: data,
                };
                addMessage(newMessage as IMessage);
              }
              setNotification((current) => current + 1);
            }
          )
          .on(
            "postgres_changes",
            { event: "DELETE", schema: "public", table: "messages" },
            (payload) => {
              console.log("Change received!", payload);
              optimisticDeleteMessage(payload.old.id);
            }
          )
          .on(
            "postgres_changes",
            { event: "UPDATE", schema: "public", table: "messages" },
            (payload) => {
              console.log("Change received!", payload);
              optimisticUpdateMessage(payload.new as IMessage);
            }
          )
          .subscribe();
    
        return () => {
          channel.unsubscribe();
        };
      }, [messages]);
    
      useEffect(() => {
        const scrollContainer = scrollRef.current;
    
        if (scrollContainer && !userScrolled) {
          scrollContainer.scrollTop = scrollContainer.scrollHeight;
        }
      }, [messages]);
    
      const handleOnScroll = () => {
        const scrollContainer = scrollRef.current;
        if (scrollContainer) {
          // scroll 높이를 계산해 밑에있으면 false, 위로 뜨면 true
          const isScroll =
            scrollContainer.scrollTop <
            scrollContainer.scrollHeight - scrollContainer.clientHeight - 10;
    
          setUserScrolled(isScroll);
    
          if (!isScroll) {
            setNotification(0);
          }
        }
      };
    
      // 스크롤을 내려주는 함수
      const scrollDown = () => {
        scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      };
    
      return (
        <>
          <div
            className="flex-1  flex flex-col p-5 h-full overflow-y-auto"
            ref={scrollRef}
            onScroll={handleOnScroll}
          >
            <div className="flex-1"></div>
            <div className="space-y-7">
              {messages.map((value, idx) => {
                return <Message key={idx} message={value} />;
              })}
            </div>
            {/* 아래로 가는 화살표. 아래로 내려갔을때만 표시되도록. */}
            {userScrolled && (
              <div className="absolute bottom-20 w-full">
                {notification ? (
                  <div
                    className="w-36 mx-auto bg-indigo-500 p-1 rounded-md cursor-pointer hover:scale-110 transition-all"
                    onClick={scrollDown}
                  >
                    <h1>New {notification} Messages</h1>
                  </div>
                ) : (
                  <div
                    className="w-10 h-10 bg-blue-500 rounded-full flex justify-center items-center mx-auto border cursor-pointer hover:scale-110 transition-all"
                    onClick={scrollDown}
                  >
                    <ArrowDown />
                  </div>
                )}
              </div>
            )}
    
            <DeleteAlert />
            <EditAlert />
          </div>
        </>
      );
    };
    
    export default ListMessages;

     

    살펴보면 먼저 메세지가 왔을때의 상태를 관리하는 state를 만들었다.

    const [notification, setNotification] = useState(0); // 새로운 메세지가 왔을때 notification을 위한 state

     

    그리고 websocket으로 새로운 데이터가 insert되었을 때 메세지를 추가하는 부분에서

    .on(
            "postgres_changes",
            { event: "INSERT", schema: "public", table: "messages" },
            async (payload) => {
              console.log("Change received!", payload);
    
              if (optimisticIds.includes(payload.new.id)) {
                return;
              }
    
              // 받아온 페이로드로 user 정보를 가져오기
              const { error, data } = await supabase
                .from("users")
                .select("*")
                .eq("id", payload.new.user_id)
                .single();
    
              if (error) {
                toast.error(error.message);
              } else {
                const newMessage = {
                  ...payload.new,
                  users: data,
                };
                addMessage(newMessage as IMessage);
              }
              setNotification((current) => current + 1);	// 알림을 위해
            }
          )

     

    이렇게 메세지가 오면 setNotification으로 notification을 1 올려준다.

    그리고 html에서는

    {/* 아래로 가는 화살표. 아래로 내려갔을때만 표시되도록. */}
            {userScrolled && (
              <div className="absolute bottom-20 w-full">
                {notification ? (
                  <div
                    className="w-36 mx-auto bg-indigo-500 p-1 rounded-md cursor-pointer hover:scale-110 transition-all"
                    onClick={scrollDown}
                  >
                    <h1>New {notification} Messages</h1>
                  </div>
                ) : (
                  <div
                    className="w-10 h-10 bg-blue-500 rounded-full flex justify-center items-center mx-auto border cursor-pointer hover:scale-110 transition-all"
                    onClick={scrollDown}
                  >
                    <ArrowDown />
                  </div>
                )}
              </div>
            )}

    이렇게 유저가 스크롤을 위로 하고 있을때, 새로운 메세지가 없다면 기존처럼 화살표모양만 보이겠지만,

    새로운 메세지가 있다면 (notification이 0이상. 0일때는 false값이다.) 새로운 메세지가 몇개가 있나 나타난다.

    또, 화살표와 마찬가지로 scrollDown 함수를 달아서 누르면 밑으로 내려가게끔 했다.

     

    그리고 제일 밑으로 내려가고 나면, 새로운 Notification이 없어야 하기에

    const handleOnScroll = () => {
        const scrollContainer = scrollRef.current;
        if (scrollContainer) {
          // scroll 높이를 계산해 밑에있으면 false, 위로 뜨면 true
          const isScroll =
            scrollContainer.scrollTop <
            scrollContainer.scrollHeight - scrollContainer.clientHeight - 10;
    
          setUserScrolled(isScroll);
    
          if (!isScroll) {	// 스크롤이 가장 밑에 있다면, 최신 메세지를 다 읽은것이므로 notification을 0으로 해준다.
            setNotification(0);
          }
        }
      };

    스크롤이 가장 밑에 있다면, 최신 메세지를 다 읽은것이므로 notification을 0으로 해준다.

    (handleOnScroll함수는

    <div
    className="flex-1 flex flex-col p-5 h-full overflow-y-auto"
    ref={scrollRef}
    onScroll={handleOnScroll}
    >

    와 연결되어 있다.

     

     

    새로운 메세지 알람이 잘 오는걸 볼 수 있다!

     

    글은 위에서부터 차근차근 썼지만, 실제 작업순서는 html부분부터 만들고, state만들고, 함수만들고 하는 식으로 진행됬다.

     

    ++ 깜빡하고 안적은 게 있는데, 화살표와 Notification 위치를 absolute로 주고 가운데로 해주기 위해 app/page.tsx의 div에 relative속성을 추가해 주었다.

     

    import ChatHeader from "@/components/ChatHeader";
    import { ChatInput } from "@/components/ChatInput";
    import ChatMessages from "@/components/ChatMessages";
    import ListMessages from "@/components/ListMessages";
    import { Button } from "@/components/ui/button";
    import { Input } from "@/components/ui/input";
    import InitUser from "@/lib/store/InitUser";
    import { createClient } from "@/utils/supabase/server"; // supabase 객체 불러오기. // 서버 컴포넌트니깐 서버에서 임포트해온다.
    import React from "react";
    
    const page = async () => {
      const supabase = createClient(); // supabase 객체 불러오기.
    
      // const session = await supabase.auth.getSession();
      // console.log(session.data.session?.user);
    
      const user = (await supabase.auth.getUser()).data.user;
      // console.log("user: ", user);
    
      return (
        <>
          <div className="max-w-3xl mx-auto md:py-10 h-screen">
            <div className="h-full border rounded-md flex flex-col relative">	<== 요기
              <ChatHeader user={user} />
              <ChatMessages />
              <ChatInput />
            </div>
          </div>
          <InitUser user={user} />
          {/* user state 관리를 시작할 수 있도록. */}
        </>
      );
    };
    
    export default page;

     

    Github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/12.ArrowDownAndNotification

     

    '사이드프로젝트' 카테고리의 다른 글

    13. Pagination  (0) 2024.07.24
    한글 두번씩 입력되는 버그해결  (2) 2024.07.24
    11.실시간 통신  (3) 2024.07.23
    10. 메세지 수정  (3) 2024.07.23
    9. Message Delete  (7) 2024.07.23

    댓글

Designed by Tistory.