대화방을 클릭 했을때, 대화 내용이 뿌려지는 방
역시, 코딩에 있어서 가장 어려운 부분임.
cache와 scriptions가 같이 들어가기 때문에 집중!!집중!!
반복!!반복!!이 필요한 부분.
!!!persistCache 부분은 주석처리한다.
꼭 필요한 부분도 아니고, subscriptions를 돌리는데 있어서,
충돌을 일으킬 가능성이 있어서 고럼!
import React, { useState } from 'react'
import { Ionicons } from '@expo/vector-icons'
import * as Font from 'expo-font'
import { Asset } from 'expo-asset'
import AppLoading from 'expo-app-loading'
import LoggedOutNav from './navigators/LoggedOutNav'
import { NavigationContainer } from '@react-navigation/native'
import LoggedInNav from './navigators/LoggedInNav'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { ApolloProvider, useReactiveVar } from '@apollo/client'
import { client, isLoggedInVar, TOKEN, tokenVar, cache } from './apollo'
import { AsyncStorageWrapper, persistCache } from 'apollo3-cache-persist'
export default function App() {
const [loading, setLoading] = useState(true)
const onFinish = () => setLoading(false)
const isLoggedIn = useReactiveVar(isLoggedInVar)
const preloadAssets = () => {
const fontsToLoad = [Ionicons.font]
const fontPromises = fontsToLoad.map((font) => Font.loadAsync(font))
const imagesToLoad = [require('./assets/logo.png')]
const imagesPromises = imagesToLoad.map((image) => Asset.loadAsync(image))
return Promise.all([...fontPromises, ...imagesPromises])
}
const preload = async () => {
const token = await AsyncStorage.getItem(TOKEN)
if (token) {
isLoggedInVar(true)
tokenVar(token)
}
// await persistCache({
// cache,
// storage: new AsyncStorageWrapper(AsyncStorage),
// serialize: false,
// })
return preloadAssets()
}
if (loading) {
return (
<AppLoading
startAsync={preload}
onError={console.warn}
onFinish={onFinish}
/>
)
}
return (
<ApolloProvider client={client}>
<NavigationContainer>
{isLoggedIn ? <LoggedInNav /> : <LoggedOutNav />}
</NavigationContainer>
</ApolloProvider>
)
}
Room(1)에서는 subscriptions부분은 빼고 다루고,
Room(2)에서 subscriptions부분만 다루도록 한다.
import { gql, useApolloClient, useMutation, useQuery } from '@apollo/client'
import React, { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { View, Text, KeyboardAvoidingView, FlatList } from 'react-native'
import styled from 'styled-components/native'
import ScreenLayout from '../components/ScreenLayout'
import useMe from '../hooks/useMe'
import { Ionicons } from '@expo/vector-icons'
const ROOM_UPDATES = gql`
subscription roomUpdates($id: Int!) {
roomUpdates(id: $id) {
id
payload
user {
username
avatar
}
read
}
}
`
const SEND_MESSAGE_MUTATION = gql`
mutation sendMessage($payload: String!, $roomId: Int, $userId: Int) {
sendMessage(payload: $payload, roomId: $roomId, userId: $userId) {
ok
error
id
}
}
`
///1)sendMessage Muataion 불러들임.
const ROOM_QUERY = gql`
query seeRoom($id: Int!) {
seeRoom(id: $id) {
id
messages {
id
payload
user {
username
avatar
}
read
}
}
}
`
///2)seeRoom Query 불러들임.
const MessageContainer = styled.View`
padding: 0px 10px;
flex-direction: ${(props) => (props.outGoing ? 'row-reverse' : 'row')};
align-items: flex-end;
`
///3) props를 이용하여, 내가 보내는 메세지와, 받는 메시지가 뿌려지는 부분을 구별해줌.!
const Author = styled.View``
const Avatar = styled.Image`
width: 30px;
height: 30px;
border-radius: 25px;
background-color: rgba(255, 255, 255, 0.2);
`
const Username = styled.Text`
color: yellowgreen;
`
const Messaage = styled.Text`
color: white;
background-color: ${(props) => (props.outGoing ? 'yellowgreen' : 'skyblue')};
border: 1px solid rgba(255, 255, 255, 0.5);
padding: 10px 20px;
border-radius: 50px;
margin: 5px 20px;
`
///4)props를 이용해서 내가 보내는 메세지와, 받는 메세지의 배경색을 구분해줌.
const Input = styled.TextInput`
background-color: rgba(255, 255, 255, 0.1);
padding: 10px 20px;
border: 2px solid white;
border-radius: 100px;
color: white;
width: 90%;
margin-right: 10px;
`
///5)메세지를 보내는 칸을 만들어줌.
const InputContainer = styled.View`
width: 95%;
margin-bottom: 50px;
margin-top: 25px;
flex-direction: row;
align-items: center;
`
const SendButton = styled.TouchableOpacity``
export default function Room({ route, navigation }) {
const { data: meData } = useMe()
///6)me의 Data를 사용해야 되어서 useMe훅을 불러서 loggedInUser의 data를 불러들임.
const { register, setValue, handleSubmit, getValues, watch } = useForm()
///7)useForm()을 만들어서 메세지를 입력할 수 있게 함.
const updateSendMessage = (cache, result) => {
///24)subscription이 아니면, 아래와 같은 방법으로 cache를 update할 수 있는데.
///subscription에서 만드는 cache와 아이디 중복문제를 일으켜,
///사용은 하지 않으나, 방법을 참고하라고 남겨놀음.
const {
data: {
sendMessage: { ok, error, id },
},
} = result
///server의 sendMessage.resolvers 와 typeDefs에서 message를 만들고,
///message의 id를 return하게 설정함.
if (ok && meData) {
const { message } = getValues() ///TextInput의 watch로 message받아오는게 가능
setValue('message', '')
const messageObj = {
id,
payload: message,
user: {
username: meData.me.username,
avatar: meData.me.avatar,
},
read: true,
__typename: 'Message',
}
///fake messageObj를 만듬.
///cache에 쓰여지는 모양과 같게 messageObj를 만들어줌.
///만드는 방법 집중해서 봐 놓을것.
const messageFragment = cache.writeFragment({
fragment: gql`
fragment NewMessage on Message {
id
payload
user {
username
avatar
}
read
}
`,
data: messageObj,
})
///위에서 만든 fake messageObj에 cache를 그대로 write함.
///모양은 seeRoom으로 받아오는 data모양으로
cache.modify({
id: `Room:${route.params.id}`,
fields: {
messages(prev) {
return [...prev, messageFragment]
///messageFragment를 만들었으며, cache에 modify 해줌.
},
},
})
}
}
const [sendMessageMutation, { loading: sendingMessage }] = useMutation(
SEND_MESSAGE_MUTATION,
{
refetchQueries: [
{ query: ROOM_QUERY, variables: { id: route.params.id } },
],
// update: updateSendMessage,
}
)
///8)sendMessage mutation을 만들어줌. 원래는, updateSendMessage 함수를
///만들어서 cache를 update하면 되는데, 나중에 subscriptions에서 만드는
///message cache와 여기서 만드는cache가 충돌을 일으켜서(id가 unique하지 않다고ㅠ)
///cache update는 subscriptions에 사용하여, 여기서는 refetchQueries를
///사용함. refetchQueries 사용 문법 집중해서 한번 더 볼것!!
console.log(route)
const { data, loading, refetch, subscribeToMore } = useQuery(ROOM_QUERY, {
variables: {
id: route?.params?.id,
},
})
///9)useQuery를 아용해서, seeRoom query를 불러서 방의 data를 읽어들임.
///variables는 Rooms.js에서 navigation.navigate로 보내주었음.
const client = useApolloClient()
const updateQuery = (prevQuery, options) => {
const {
subscriptionData: {
data: { roomUpdates: message },
},
} = options
if (message.id) {
const incomingMessage = client.cache.writeFragment({
fragment: gql`
fragment NewMessage123 on Message {
id
payload
user {
username
avatar
}
read
}
`,
data: message,
})
client.cache.modify({
id: `Room:${route.params.id}`,
fields: {
messages(prev) {
const existingMessage = prev.find(
(aMessage) => aMessage.__ref === incomingMessage.__ref
)
console.log(prev)
console.log('----------------------')
console.log(incomingMessage)
if (existingMessage) {
return prev
}
return [...prev, incomingMessage]
},
},
})
}
}
const [subscribed, setSubscribed] = useState(false)
useEffect(() => {
if (data?.seeRoom && !subscribed) {
subscribeToMore({
document: ROOM_UPDATES,
variables: {
id: route?.params?.id,
},
updateQuery,
})
setSubscribed(true)
}
}, [data, subscribed])
const onValid = ({ message }) => {
if (!sendingMessage) {
sendMessageMutation({
variables: {
payload: message,
roomId: route?.params?.id,
},
})
setValue('message', '')
}
}
///10)sendMessage를 클릭했을 시, 발생하는 onValid 함수.
///!sendingMessage는 loading이 중복되어 loading: sendingMessage로 rename해줬음.
///variables는 TextInput에서 message를 받음.
///위 mutation을 실행하고 나서, message 칸은 비워줌.
useEffect(() => {
register('message', { required: true })
}, [register])
///11)TextInput을 'message'로 useForm()과 연결시켜줌.
useEffect(() => {
navigation.setOptions({
title: `Talking with ${route?.params?.talkingTo?.username}`,
})
})
///12)Room화면의 title을 대화하는 상대방의 username으로 바꿔줌
const renderItem = ({ item: message }) => (
<MessageContainer outGoing={message.user.username === meData?.me?.username}>
///14)outGoing prop을 만들어서 내가 보내는 메세지와, 받는 메시지를 구분해줌.
<Author>
<Avatar source={{ uri: message.user.avatar }} />
<Username>{message.user.username}</Username>
</Author>
<Messaage outGoing={message.user.username === meData?.me?.username}>
{message.payload}
</Messaage>
///15)위와 마찬가지로 내가 보내는 메세지와, 받는 메세지의 배경색을 구분해 줌.
</MessageContainer>
)
///13)FlatList에서 사용되는 renderItem을 만들어줌.
const refresh = async () => {
setRefreshing(true)
await refetch()
setRefreshing(false)
}
const [refreshing, setRefreshing] = useState(false)
///16)loading이나 fetch가 잘 안될 경우를 생각해서 만들어 놓음.
const messages = [...(data?.seeRoom?.messages ?? [])]
messages.reverse()
///17)이번 POST에서 가장 중요한 부분.
///채팅 메세지는 거꾸로 나열되어야 되기떄문에, 위와 같은 방법을 사용해야 함.
///채팅어플을 만들때는 걍 저렇게 써야 된다고 외워야함.
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: 'black' }}
behavior="height"
keyboardVerticalOffset={60}
>
///18)메세지 입력을 위해서 keyboardAvoidingView로 감싸줌.
///여기서 중요한 것은 behavior와 keyboardVerticalOffset임..
///두 가지 설정이 매우 중요함. 채팅어플에선~
<ScreenLayout loadng={loading}>
///19)ScreenLayout component 를 불러서 감싸줌.
<FlatList
ItemSeparatorComponent={() => <View style={{ height: 5 }}></View>}
refreshing={refreshing}
onRefresh={refresh}
inverted
style={{ width: '100%', marginVertical: 30 }}
data={messages}
keyExtractor={(message) => '' + message.id}
renderItem={renderItem}
/>
///20)FlatList 고대로 사용.
<InputContainer>
<Input
placeholder="Write a message..."
placeholderTextColor="white"
returnKeyLabel="Send Message"
returnKeyType="send"
onChangeText={(text) => setValue('message', text)}
onSubmitEditing={handleSubmit(onValid)}
value={watch('message')}
/>
///21)TextInput설정하는거, 다시한번 집중해서 볼것!!
///onChangeText, onSubmitEditing, value 세개 집중해서 볼것!!
<SendButton
onPress={handleSubmit(onValid)}
disabled={!Boolean(watch('message'))}
>
///22)아래 Ionicons의 비행기(send)누르면, 위와 마찬가지로 onValid 살행되게 함.
<Ionicons
name="send"
color={
!Boolean(watch('message')) ? 'rgba(255,255,255,0.5)' : 'white'
}
///23)Boolen과 watch를 사용해서 비행기의 색깔 변화줌(disabled와 abled)
size={30}
/>
</SendButton>
</InputContainer>
</ScreenLayout>
</KeyboardAvoidingView>
)
}