Skip to content

Commit

Permalink
Merge pull request #140 from B03-Killer/feature/#139
Browse files Browse the repository at this point in the history
feat: 공지 기능, 상단 공지 노출 기능 추가
  • Loading branch information
1eeyerin authored Aug 2, 2024
2 parents 52d0d47 + a3f34ca commit 5fd7b03
Show file tree
Hide file tree
Showing 37 changed files with 301 additions and 125 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"class-variance-authority": "^0.7.0",
"dayjs": "^1.11.11",
"livekit-server-sdk": "^2.5.1",
"lodash.isempty": "^4.4.0",
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
Expand Down
19 changes: 14 additions & 5 deletions src/api/chatAPI.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { CreateChatMessageProps, GetChatMessagesProps, GetChatMessagesResponse } from '@/types/chat';
import type {
CreateChatMessageProps,
GetChatMessagesProps,
GetChatMessagesResponse,
GetChatMessageType
} from '@/types/chat';
import type { APIResponse } from '@/types/common';
import { AxiosInstance } from 'axios';

Expand All @@ -19,14 +24,12 @@ class ChatAPI {
channel_id,
content,
workspace_user_id,
type,
is_notice
type
}: CreateChatMessageProps): Promise<APIResponse<[]>> => {
const { data } = await this.axios.post(`/api/chat/${channel_id}`, {
content,
workspace_user_id,
type,
is_notice
type
});

return data;
Expand All @@ -37,6 +40,12 @@ class ChatAPI {

return data.data?.name;
};

getLatestNotice = async ({ id }: { id: string }): Promise<GetChatMessageType> => {
const { data } = await this.axios.get(`/api/chat/${id}/latest-notice`);

return data.data;
};
}

export default ChatAPI;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { GetChatChannelsResponse } from '@/types/channel';
import type { ChatSubscribePayloadProps } from '@/types/chat';
import useWorkspaceId from '@/hooks/useWorkspaceId';
import { useWorkspaceUserId } from '@/hooks/useWorkspaceUserId';
import { isEmpty } from '@/utils/isEmpty';

const ChatListPage = () => {
const workspaceId = useWorkspaceId();
Expand Down Expand Up @@ -46,7 +47,7 @@ const ChatListPage = () => {
[channelIds]
);

if (!channels || channels.length === 0) {
if (isEmpty(channels)) {
return <div>채팅 리스트가 없습니다.</div>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ChatMessages from '../_components/ChatMessages';
import ChatFooter from '../_components/ChatFooter';
import { RealtimeSubscribeProps } from '@/utils/createRealtimeSubscription';
import { useWorkspaceUserId } from '@/hooks/useWorkspaceUserId';
import ChatNotice from '../_components/ChatNotice';

type RealtimePayloadMessagesType = GetChatMessageType & {
channel_id: string;
Expand All @@ -24,7 +25,8 @@ type RealtimeChatPayloadType = {
};

const ChatDetailPage = () => {
const { id: channelId }: { id: string; workspaceId: string } = useParams();
const { id: channelId } = useParams();
const stringId = Array.isArray(channelId) ? channelId[0] : channelId;
const workspaceUserId = useWorkspaceUserId();

const containerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -73,25 +75,26 @@ const ChatDetailPage = () => {
containerRef.current.scrollIntoView({ block: 'end' });
}, [isPending, payloadMessages]);

useEffect(subscribeToChat({ handleUserUpdates, handleChatUpdates, id: channelId, userIds }), [userIds]);
useEffect(subscribeToChat({ handleUserUpdates, handleChatUpdates, id: stringId, userIds }), [userIds]);

if (isPending) return <div>Loading...</div>;

return (
<div className="flex flex-col">
<>
<div
className={`flex flex-col flex-grow h-[calc(100dvh+35px)] transform ease-in-out duration-300 ${
className={`flex flex-col flex-grow h-[calc(100dvh+42px)] transform ease-in-out duration-300 ${
isOpenUtil ? 'translate-y-[-96px]' : 'translate-y-[0px]'
}`}
>
<ChatNotice />
<ChatMessagesWrapper ref={containerRef}>
<ChatMessages data={chatMessages} usersInChannel={usersInChannel} />
<ChatMessages data={payloadMessages} usersInChannel={usersInChannel} />
</ChatMessagesWrapper>
<ChatFooter id={channelId} handleOpenUtil={handleOpenUtil} />
<ChatFooter id={stringId} handleOpenUtil={handleOpenUtil} />
{isOpenUtil && <div className="fixed top-0 left-0 w-full h-full z-40" onClick={handleOpenUtil} />}
</div>
</div>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const FileListPage = () => {
}, []);

return (
<ul className="grid grid-cols-2 gap-x-2 gap-y-3 mt-[26px]">
<ul className="grid grid-cols-2 gap-x-2 gap-y-3 py-[22px] px-4">
{fileList.map((file) => (
<li
key={file.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const MediaListPage = () => {
}, []);

return (
<ul className="grid grid-cols-3 gap-x-2 gap-y-3">
<ul className="grid grid-cols-3 gap-x-2 gap-y-3 py-[22px] px-4">
{mediaList.map((media) => {
if (!media.content) return null;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
'use client';

import { supabase } from '@/utils/supabase/supabaseClient';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { CHAT_TYPE } from '@/constants/chat';
import Typography from '@/components/Typography';
import dayjs from 'dayjs';

const NoticeListPage = () => {
return <div>NoticeListPage</div>;
const { id } = useParams();
const [noticeList, setNoticeList] = useState<any[]>([]);

useEffect(() => {
const getNoticeList = async () => {
const res = await supabase
.from('chat')
.select('*')
.eq('channel_id', id)
.eq('type', CHAT_TYPE.notice)
.order('created_at', { ascending: false });
setNoticeList(res.data || []);
};

getNoticeList();
}, []);

return (
<ul className="flex flex-col gap-4 py-[22px] px-4">
{noticeList.map((notice) => (
<li
key={notice.id}
className="h-[94px] bg-[#F7F7F7] shadow-[0px_1px_8px_0px_rgba(0,0,0,0.15)] rounded-[6px] px-3 py-4 flex flex-col justify-between"
>
<Typography variant="Subtitle14px" color="grey700Black">
{notice.content}
</Typography>
<Typography variant="Body12px" color="grey300">
{dayjs(notice.created_at).format('YYYY.MM.DD HH:mm')}
</Typography>
</li>
))}
</ul>
);
};

export default NoticeListPage;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import clsx from 'clsx';
import ChatImage from '../ChatImage';
import ChatVideo from '../ChatVideo';
import { useContextMenu } from '../../_provider/ContextMenuProvider';
import Link from 'next/link';
import { FileTextIcon } from '@/icons';

type ClassNameProps = Pick<ComponentProps<'div'>, 'className'>;

Expand Down Expand Up @@ -52,7 +54,7 @@ export const ChatOtherProfileName = ({ children }: StrictPropsWithChildren) => {
type ChatTextProps = ComponentProps<'div'> & ClassNameProps & { isMe: boolean };

const ChatText = ({ children, className, isMe, ...props }: ChatTextProps) => {
const chatTextClass = isMe ? 'rounded-tr-none bg-[#EBECFE]' : 'rounded-tl-none bg-grey50 ml-[40px] mt-[6px]';
const chatTextClass = isMe ? 'rounded-br-none bg-[#EBECFE]' : 'rounded-tl-none bg-grey50 ml-[40px] mt-[6px]';

return (
<Typography
Expand Down Expand Up @@ -81,16 +83,44 @@ const ChatFile = ({ fileUrl, fileName, ...props }: ChatFileProps) => {
);
};

type ChatNoticeProps = ComponentProps<'div'> & ClassNameProps & { isMe: boolean; noticeUrl: string };

const ChatNotice = ({ children, className, isMe, noticeUrl, ...props }: ChatNoticeProps) => {
const chatTextClass = isMe ? 'rounded-br-none' : 'rounded-tl-none ml-[40px] mt-[6px]';

return (
<div className={clsx(chatTextClass, 'rounded-[20px] bg-[#F7F7F7] py-2 px-3 min-w-[188px]')} {...props}>
<Typography as="span" variant="Body12px" className="border-b border-grey100 pb-[6px] mb-[5px] block">
공지가 등록되었습니다.
</Typography>
<Typography
as="span"
variant="Body12px"
className="whitespace-pre-wrap border-b border-grey100 h-[45px] flex items-center pb-[3px]"
color="grey700Black"
>
{children}
</Typography>
<Link href={noticeUrl}>
<Typography as="span" variant="Body12px" className="flex items-center gap-1 mt-2" color="grey400">
<FileTextIcon /> 글 확인하기
</Typography>
</Link>
</div>
);
};

type ChatMessageProps = {
content: string;
type: string;
isMe: boolean;
id: number;
noticeUrl: string;
};

const TOP_BAR_HEIGHT = 52;

export const ChatMessage = ({ content, type, isMe, id }: ChatMessageProps) => {
export const ChatMessage = ({ content, type, isMe, id, noticeUrl }: ChatMessageProps) => {
const { openContextMenu } = useContextMenu();

const handleContextMenu = (event: React.MouseEvent<HTMLDivElement | HTMLButtonElement | HTMLVideoElement>) => {
Expand Down Expand Up @@ -143,6 +173,13 @@ export const ChatMessage = ({ content, type, isMe, id }: ChatMessageProps) => {
{content}
</ChatText>
);

case CHAT_TYPE.notice:
return (
<ChatNotice onContextMenu={handleContextMenu} isMe={isMe} noticeUrl={noticeUrl}>
{content}
</ChatNotice>
);
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef } from 'react';
import { useMutationChatMessage } from '../../../_hooks/useMutationChat';
import MessageTextarea from '../MessageTextarea';
import UtilsMenus from '../UtilsMenus';
import UtilsMenu from '../UtilsMenu';
import ContextMenu from '../ContextMenu';
import { useWorkspaceUserId } from '@/hooks/useWorkspaceUserId';

Expand All @@ -23,7 +23,7 @@ const ChatFooter = ({ id, handleOpenUtil }: ChatFooterProps) => {

if (!ref.current?.value) return;

mutateChatMessage({ content: ref.current.value, type: 'text', is_notice: false });
mutateChatMessage({ content: ref.current.value, type: 'text' });
ref.current.value = '';
};

Expand All @@ -32,7 +32,7 @@ const ChatFooter = ({ id, handleOpenUtil }: ChatFooterProps) => {
<form onSubmit={handleSendMessage}>
<MessageTextarea handleOpenUtil={handleOpenUtil} ref={ref} />
</form>
<UtilsMenus handleOpenUtil={handleOpenUtil} />
<UtilsMenu handleOpenUtil={handleOpenUtil} />
<ContextMenu />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { GetUsersInChannelResponse } from '@/types/channel';
import Link from 'next/link';
import useWorkspaceId from '@/hooks/useWorkspaceId';
import { useWorkspaceUserId } from '@/hooks/useWorkspaceUserId';
import { useParams } from 'next/navigation';
import { isEmpty } from '@/utils/isEmpty';

type ChatMessagesProps = {
data: GetChatMessageType[] & { channel_id?: string };
Expand All @@ -13,10 +15,14 @@ type ChatMessagesProps = {
// TODO: video onLoad event 가 있으면 모두 다 로딩된 후에 스크롤을 가장 아래로 내리도록 수정

const ChatMessages = ({ data = [], usersInChannel = {} }: ChatMessagesProps) => {
const { id: channelId } = useParams();
const workspaceId = useWorkspaceId();
const workspaceUserId = useWorkspaceUserId();

if (Object.keys(usersInChannel).length === 0) return null;
// TODO: Notice Card 안에서 사용하는 건데, 안에서 만들면 렌더링에 너무 영향이 가지 않을까?
const noticeUrl = `/${workspaceId}/chat/${channelId}/notice`;

if (isEmpty(usersInChannel)) return null;

return (
<>
Expand All @@ -39,7 +45,7 @@ const ChatMessages = ({ data = [], usersInChannel = {} }: ChatMessagesProps) =>
<ChatOtherProfileName>{userInfo?.name}</ChatOtherProfileName>
</ChatOtherProfileContainer>
)}
<ChatMessage content={chat.content} type={chat.type} id={chat.id} isMe={isMe} />
<ChatMessage content={chat.content} type={chat.type} id={chat.id} isMe={isMe} noticeUrl={noticeUrl} />
</ChatContainer>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useParams } from 'next/navigation';
import { useGetLatestNotice } from '../../../_hooks/useQueryChat';
import { BellIcon, ChevronDownIcon } from '@/icons';
import Typography from '@/components/Typography';
import Link from 'next/link';
import useWorkspaceId from '@/hooks/useWorkspaceId';
import { isEmpty } from '@/utils/isEmpty';

const ChatNotice = () => {
const { id } = useParams();
const workspaceId = useWorkspaceId();
const stringId = Array.isArray(id) ? id[0] : id;
const { data: latestNotice, isPending: isPendingLatestNotice } = useGetLatestNotice({ id: stringId });

if (isEmpty(latestNotice) || isPendingLatestNotice) return null;

return (
<>
<Link
href={`/${workspaceId}/chat/${stringId}/notice`}
className="fixed top-0 left-0 right-0 mx-4 h-[34px] shadow-2xl rounded-[4px] flex items-center gap-1 bg-[#F7F7F7] py-2 px-3 z-30"
>
<BellIcon />
<Typography
variant="Body12px"
color="grey500"
className="flex-grow text-ellipsis whitespace-nowrap overflow-hidden"
>
{latestNotice.content}
</Typography>
<ChevronDownIcon className="w-4 h-4 stroke-grey500" />
</Link>
<div className="h-[34px] flex-shrink-0" />
</>
);
};

export default ChatNotice;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ChatNotice';
Loading

0 comments on commit 5fd7b03

Please sign in to comment.