Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 채팅 관련 UI 및 로직 #96

Merged
merged 3 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .hintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이건 무슨 파일이죠?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webhint 하나 비활성화해서 저장됐나보네요

"extends": [
"development"
],
"hints": {
"axe/structure": [
"default",
{
"list": "off"
}
]
}
}
6 changes: 6 additions & 0 deletions public/images/send.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions src/app/(root)/(routes)/chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useState } from 'react'
import Image from 'next/image'
import Button from '@/components/ui/button'
import Input from '@/components/ui/input'
import Assets from '@/config/assets'

const ChatInput = ({
onSubmit,
}: {
onSubmit: (_newMessage: string) => void
}) => {
const [newMessage, setNewMessage] = useState<string>('')

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setNewMessage(e.target.value)
}

const onSubmitMessage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!newMessage.trim()) return

onSubmit(newMessage)
setNewMessage('')
}

return (
<form
onSubmit={onSubmitMessage}
className="absolute bottom-0 grid items-center w-full grid-cols-5 p-4 align-middle h-chat_input"
>
<div className="col-span-1" />
<Input
onChange={onChange}
value={newMessage}
type="text"
placeholder="메세지를 입력하세요."
className="col-span-3"
/>
<div className="flex justify-center col-span-1">
<Button
type="submit"
variant={null}
className="transition-opacity hover:opacity-70"
>
<Image src={Assets.sendIcon} alt="발송" />
</Button>
</div>
</form>
)
}

export default ChatInput
79 changes: 79 additions & 0 deletions src/app/(root)/(routes)/chat/components/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { forwardRef, memo } from 'react'
import { TYPOGRAPHY } from '@/styles/sizes'
import type { Message } from '@/types/message'
import { cn } from '@/utils'

type ChatListProps = {
messages: Message[]
currentUserNickname?: string
}

type ChatProps = {
message: Message
isMyMessage: boolean
}

const ChatList = forwardRef<HTMLDivElement, ChatListProps>(
({ messages, currentUserNickname }, ref) => {
return (
<ul className="flex flex-col w-full h-full gap-1">
{messages.map((message: Message) => {
return (
<Chat
key={message.id}
message={message}
isMyMessage={message.sender === currentUserNickname}
/>
)
})}
{messages.length === 0 && (
<h1 className="text-red-500">채팅을 시작해보세요!</h1>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

채팅방에 진입했을 때 메세지들이 있는데도 채팅을 시작해보세요 문구가 1초정도 보였다가 사라지는데 혹시 의도하신 건가요? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아닙니다. api처리하면서 수정할 예정입니다!

)}
<div className="invisible" ref={ref} />
</ul>
)
},
)
ChatList.displayName = 'ChatList'

export default memo(ChatList)

const Chat = ({ message, isMyMessage }: ChatProps) => {
return isMyMessage ? (
<MyChat message={message} />
) : (
<OtherChat message={message} />
)
}

const MyChat = ({ message }: Pick<ChatProps, 'message'>) => {
return (
<li className={cn('flex flex-row gap-2 justify-end')}>
<p className={cn('mt-auto break-keep', TYPOGRAPHY.description)}>
{message.createdAt.toDate().toLocaleTimeString().slice(0, -3)}
</p>
<div
className={
'p-2 text-black rounded-lg max-w-[75%] w-fit bg-background-secondary-color'
}
>
{message.text}
</div>
</li>
)
}

const OtherChat = ({ message }: Pick<ChatProps, 'message'>) => {
return (
<li className={cn('flex flex-row gap-2 mr-auto justify-start')}>
<div
className={'p-2 text-white rounded-lg max-w-[75%] bg-primary-color '}
>
{message.text}
</div>
<p className={cn('mt-auto break-keep', TYPOGRAPHY.description)}>
{message.createdAt.toDate().toLocaleTimeString().slice(0, -3)}
</p>
</li>
)
}
95 changes: 49 additions & 46 deletions src/app/(root)/(routes)/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,64 @@
'use client'

import React from 'react'
import { collection, limit, orderBy, query, addDoc } from 'firebase/firestore'
import useFirestoreQuery, { Message } from '@/hooks/useFirestoreQuery'
import { db, getMessageRef } from '@/lib/firebase'
import React, { useEffect, useRef } from 'react'
import { limit, orderBy, query, addDoc } from 'firebase/firestore'
import PageTitle from '@/components/domain/page-title'
import { CHAT_LIMIT } from '@/config/firebaseConfig'
import { useAuth } from '@/contexts/AuthProvider'
import useFirestoreQuery from '@/hooks/useFirestoreQuery'
import { useToast } from '@/hooks/useToast'
import { getMessageRef } from '@/lib/firebase'
import ChatInput from './components/ChatInput'
import ChatList from './components/ChatList'

const ChatPage = () => {
const messageRef = getMessageRef('room2')
const { currentUser } = useAuth()
const { toast } = useToast()

const messageRef = getMessageRef('room2') // TODO: room id를 받아서 처리
const messages = useFirestoreQuery(
query(messageRef, orderBy('createdAt', 'asc'), limit(200)),
query(messageRef, orderBy('createdAt', 'asc'), limit(CHAT_LIMIT)),
oaoong marked this conversation as resolved.
Show resolved Hide resolved
)

const [newMessage, setNewMessage] = React.useState<string>('')
const chatBottomRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (chatBottomRef.current) {
chatBottomRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [messageRef])

const onSubmitMessage = async (message: string) => {
chatBottomRef.current?.scrollIntoView({ behavior: 'smooth' })

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setNewMessage(e.target.value)
try {
await addDoc(messageRef, {
text: message,
sender: currentUser?.nickname ?? '익명',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

익명이 필요한 경우가 있나요?? 채팅같은 경우 무조건 로그인 상태여야만 접근할 수 있다면 필요 없을 것 같아서요 (잘 모름) 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필요 없을 것 같습니다. 권한 처리하면서 처리 예정입니다!

createdAt: new Date(),
})
} catch (e) {
toast({
title: '메세지 전송에 실패했습니다.',
variant: 'destructive',
duration: 3000,
})
}
}

return (
<div className="flex flex-col items-center w-full gap-10">
<h1 className="text-4xl">chat</h1>
<h1>new message</h1>
<form
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log(newMessage)
addDoc(collection(db, 'chats', 'room2', 'messages'), {
text: newMessage,
sender: 'me',
createdAt: new Date().toISOString(),
}).then(() => {
setNewMessage('')
})
}}
>
<input
onChange={onChange}
value={newMessage}
type="text"
placeholder="메세지를 입력하세요."
<main className="relative flex flex-col items-center w-full gap-10 h-page pb-chat_input">
<PageTitle title="채팅방" />
<section className="flex flex-col items-center px-2 overflow-scroll overflow-x-hidden">
<ChatList
messages={messages}
currentUserNickname={currentUser?.nickname}
ref={chatBottomRef}
/>
<button type="submit">send</button>
</form>
<div className="flex flex-col items-center w-full">
{messages.map((item: Message, idx: number) => {
console.log(item)
return (
<div key={idx} className="flex flex-row gap-2">
<h2>{`${item.sender}:`} </h2>
<h2>{item.text}</h2>
</div>
)
})}
{messages.length === 0 && <h1 className="text-red-500">no data</h1>}
</div>
</div>
</section>
<ChatInput onSubmit={onSubmitMessage} />
<div className="w-full h-0 border border-t-background-secondary-color" />
</main>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const NewCardButton = () => {
if (isLoggedIn) router.push(AppPath.newCard())
}

// TODO: 챗 페이지에서는 안보이게 처리
if (!isLoggedIn || path === AppPath.newCard()) return <></>

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/domain/page-title/PageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Assets from '@/config/assets'

const PageTitle = ({ title }: { title: string }) => {
return (
<div className="flex grid items-center justify-between w-full h-8 grid-cols-3 my-4">
<div className="grid items-center justify-between w-full h-8 grid-cols-3 my-4">
<Image
src={Assets.arrowLeftIcon}
alt="이전 아이콘"
Expand Down
2 changes: 2 additions & 0 deletions src/config/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import VMoreIcon from '/public/images/more-vertical.svg'
import NoData from '/public/images/no-data-img.svg'
import QuitCircle from '/public/images/quit-circle.svg'
import Search from '/public/images/search.svg'
import SendIcon from '/public/images/send.svg'
import TradeComplete from '/public/images/trade-complete.svg'
import UnavailableIcon from '/public/images/unavailable.png'
import XIcon from '/public/images/x-icon.svg'
Expand Down Expand Up @@ -84,6 +85,7 @@ const Assets = {
allCardIcon: AllCards,
headerLogo: HeaderLogo,
noDataIcon: NoData,
sendIcon: SendIcon,
} as const

export default Assets
13 changes: 13 additions & 0 deletions src/config/firebaseConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_APP_ID,
}

const CHAT_LIMIT = 200 //TODO: 제한 개수 수정 또는 불러오는 법?

export default firebaseConfig
export { CHAT_LIMIT }
8 changes: 1 addition & 7 deletions src/hooks/useFirestoreQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@

import { useEffect, useRef, useState } from 'react'
import { Query, onSnapshot, queryEqual } from 'firebase/firestore'

export interface Message {
text: string
createdAt: Date
sender: string
id: string
}
import { Message } from '@/types/message'

const isMessageType = (arg: any): arg is Message => {
return (
Expand Down
17 changes: 1 addition & 16 deletions src/lib/firebase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@ import {
getFirestore,
collection,
} from 'firebase/firestore'

export const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_APP_ID,
}
import firebaseConfig from '@/config/firebaseConfig'

const fb = initializeApp(firebaseConfig)
const db = getFirestore(fb)
Expand All @@ -21,11 +13,4 @@ const getMessageRef = (roomId: string): CollectionReference => {
return collection(db, 'chats', roomId, 'messages')
}

// async function getData(db: Firestore) {
// const dataCol = collection(db, 'chats', 'room2', 'messages')
// const dataSnap = await getDocs(dataCol)
// const dataList = dataSnap.docs.map((doc) => doc.data())
// return dataList
// }

export { db, getMessageRef }
3 changes: 3 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
@layer base {
:root {
--nav-height: 3.5rem;
--page-height: calc(100vh - var(--nav-height));
--chat-input-height: 4.5rem;

--page-min-width: 320px;
--page-max-width: 640px;
}
Expand Down
10 changes: 10 additions & 0 deletions src/types/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Timestamp } from 'firebase/firestore'

interface Message {
text: string
createdAt: Timestamp
sender: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sender에는 유저의 nickName이 들어가는 것으로 이해하면 될까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 근데 유저 닉네임 변경 가능하네요... 아이디로 바꿔야겠습니다. 감사합니다

id: string
}

export type { Message }
5 changes: 5 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ module.exports = {
height: {
...HEIGHT,
nav: 'var(--nav-height)',
page: 'var(--page-height)',
chat_input: 'var(--chat-input-height)',
},
borderRadius: {
...BORDER_RADIUS,
Expand All @@ -36,6 +38,9 @@ module.exports = {
fontFamily: {
pretendard: ['Pretendard'],
},
padding: {
chat_input: 'var(--chat-input-height)',
},
},
},
plugins: [
Expand Down