Room.js(1)

김종민·2022년 6월 13일
0

insta-native

목록 보기
31/36


대화방을 클릭 했을때, 대화 내용이 뿌려지는 방
역시, 코딩에 있어서 가장 어려운 부분임.
cache와 scriptions가 같이 들어가기 때문에 집중!!집중!!
반복!!반복!!이 필요한 부분.

1. App.js

!!!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>
  )
}

2. screens/Room.js

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>
  )
}
profile
코딩하는초딩쌤

0개의 댓글