우선 이전의 message-service에 대한 시나리오를 다시 한번 보겠습니다.
1) 게시글(첫 메시지): 게시글을 이용해 메시지를 보내는 경우에는 게시글 작성자 state인 writer를 MessageRoom 컴포넌트로 보내어 메시지를 작성할 수 있습니다. 해당 경우에는 initRoom 메서드를 이용하여 채팅방을 생성하도록 합니다.
2) 게시글(첫 메시지 이후): 게시글을 통해 메시지를 보내는 경우 유저 간의 채팅방이 존재하는 경우가 있습니다. 이와 같은 경우 기존 채팅방을 불러옵니다.
3) 메시지 메인 컴포넌트에서 채팅방으로 넘어가는 경우: 해당 경우에도 유저 간 채팅방이 존재하니 기존의 채팅방을 불러옵니다.
시나리오를 진행하기 위해서 우선 게시글에 들어가서 사용자가 메시지를 보내야겠습니다. PostDetail의 UI에 메시지버튼을 추가해보도록 하겠습니다.
...
const DetailFooter = post => {
...
const toMessage = e => {
navigation.navigate("MessageRoom", {
"writer": writer
});
};
...
return(
<View style={ styles.container }>
<View style={ styles.message_box }>
<TouchableOpacity style={ styles.button }
onPress={ toMessage }
>
<Text style={ styles.font }>
채팅으로 할래요
</Text>
</TouchableOpacity>
</View>
<View style={ styles.search_box }>
<TextInput style={ styles.input }
multiline={ true }
onChange={ onChange }
value={ content }
/>
<CommentButton />
</View>
<View style={ styles.comment_box }>
<CommentList comments={ post.post.comments } />
</View>
</View>
);
};
const styles = StyleSheet.create({
...
message_box: {
flex: 1,
borderRadius: 10,
shadowColor: palette.black[0],
shadowOpacity: 0.8,
shadowOffset: {
width: 4,
height: 4
},
elevation: 10,
alignItems: 'flex-end',
padding: 5,
backgroundColor: palette.white[0]
},
button: {
backgroundColor: palette.blue[0],
padding: 10,
borderRadius: 5
},
font: {
fontWeight: 'bold',
color: palette.white[0],
fontSize: 14
}
});
export default DetailFooter;
게시글에서 채팅방으로 이동할 때, writer의 값을 동시에 보내도록 하겠습니다.
그러면 우선 Message 관련 컴포넌트를 만들고 redux 모듈을 작성하도록 하겠습니다.
하단 네비게이션에 Message를 추가하도록 하겠습니다.
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/Ionicons';
import palette from '../styles/palette';
import HomeScreen from '../pages/home/HomeScreen';
import PostStackNavigation from './post/PostStackNavigation';
import MapStackNavigation from './map/MapStackNavigation';
import MyPageStackNavigation from './user/MyPageStackNavigation';
import MessageStackNavigation from './message/MessageStackNavigation';
const Tab = createBottomTabNavigator();
const BottomNavigation = () => {
return(
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color }) => {
...
} else if (route.name === "Message") {
iconName = focused ? 'ios-chatbubble' : 'ios-chatbubble-outline';
iconSize = focused ? 32 : 24;
}
...
}
})}
>
...
<Tab.Screen name="Message"
options={{ headerShown: false }}
children={ () => <MessageStackNavigation /> }
/>
...
</Tab.Navigator>
);
};
export default BottomNavigation;
그리고 2개의 Screen에 대한 navigation을 사용해야 하므로 채팅 네비게이션을 만들도록 하겠습니다.
import 'react-native-gesture-handler';
import * as React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import MessageScreen from '../../pages/message/MessageScreen';
import MessageRoom from '../../pages/message/MessageRoom';
const Stack = createStackNavigator();
const MessageStackNavigation = () => {
return(
<Stack.Navigator>
<Stack.Screen name="MessageRooms"
component={ MessageScreen }
options={{ headerShown: false }}
/>
<Stack.Screen name="MessageRoom"
component={ MessageRoom }
/>
</Stack.Navigator>
);
};
export default MessageStackNavigation;
첫 화면은 채팅방들이 나열되어 있는 Screen입니다. 따라서 MessageScreen을 첫 번째, 채팅방을 두 번째에 두고 MessageScreen을 구현하겠습니다.
import React from 'react';
import { ScrollView } from 'react-native';
import MessageFragment from './components/MessageFragment';
import MessageHeader from './components/MessageHeader';
const MessageScreen = () => {
return(
<ScrollView>
<MessageHeader />
<MessageFragment />
</ScrollView>
);
};
export default MessageScreen;
header부분에는 찾고싶은 사용자와의 채팅방을 위한 컴포넌트이고, Fragment는 채팅방들이 나열되어있는 컴포넌트입니다.
import React from 'react';
import { StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import palette from '../../../styles/palette';
const MessageHeader = () => {
const onPress = e => {};
return(
<View style={ styles.container }>
<View style={ styles.searchBox }>
<TextInput style={ styles.input } />
<TouchableOpacity onPress={ onPress }>
<Icon name={ 'ios-search-outline' }
size={ 19 }
color={ palette.white[0] }
/>
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
height: 70,
backgroundColor: palette.blue[5],
justifyContent: 'center',
alignItems: 'center'
},
searchBox: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
width: 350,
height: 50,
backgroundColor: palette.blue[7],
borderRadius: 30,
},
input: {
flexDirection: 'row',
justifyContent: 'center',
width: 300,
height: 50,
backgroundColor: palette.blue[7],
borderRadius: 30,
color: palette.white[0],
fontWeight: '400',
fontSize: 16
},
});
export default MessageHeader;
import React, { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import Loading from '../../../styles/common/Loading';
import palette from '../../../styles/palette';
import RoomCard from './MessageRoomCard';
const MessageFragment = () => {
const dummy = [
{
"roomId": 1,
"messages": [
{
"sender": "biuea",
"receiver": "asd",
"content": "test-001 message that is send by biuea to asd",
"createdAt": Date("2021-10-26T04:24:37.295Z")
},
{
"sender": "biuea",
"receiver": "asd",
"content": "test-002 message that is send by biuea to asd",
"createdAt": Date("2021-10-26T04:24:37.295Z")
},
{
"sender": "asd",
"receiver": "biuea",
"content": "test-003 message that is send by asd to biuea",
"createdAt": Date("2021-10-26T04:24:37.295Z")
}
],
"createdAt": Date("2021-10-26T04:24:37.295Z")
}
];
return(
<View style={ styles.container }>
{
dummy ?
dummy.map(item => {
return <RoomCard item={ item }/>
}) : <Loading />
}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: palette.gray[1]
}
});
export default MessageFragment;
우선 더미데이터를 만들어 사용하도록 하겠습니다.
import { useNavigation } from '@react-navigation/native';
import React from 'react';
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useSelector } from 'react-redux';
import palette from '../../../styles/palette';
const RoomCard = item => {
const navigation = useNavigation();
const { nickname } = useSelector(({ user }) => ({ nickname: user.user.nickname }));
const toRoom = e => {
navigation.navigate("MessageRoom", {
room: item.item
});
};
const onAlert = e => {
Alert.alert(
'채팅방을 나가시겠어요?',
null,
[
{
text: '네, 나갈래요',
onPress: () => console.log("Exit event")
},
{
text: '아니요',
onPress: () => { return; }
}
]
);
};
return(
<View style={ styles.container }>
<TouchableOpacity onPress={ toRoom }
onLongPress={ onAlert }
>
<View style={ styles.row }>
<Text>
{ item.item.messages[item.item.messages.length - 1].createdAt.substring(0, 24) }
</Text>
</View>
<View style={ styles.row }>
<Text style={ styles.font }>
{
item.item.messages[0].sender === nickname ?
item.item.messages[0].receiver : item.item.messages[0].sender
} 님 과의 메시지
</Text>
</View>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: palette.white[0],
padding: 10,
margin: 10
},
row: {
margin: 5
},
font: {
fontSize: 15,
fontWeight: 'bold'
}
});
export default RoomCard;
RoomCard 컴포넌트에서는 navigation을 이용하여 채팅방으로 이동할 수 있습니다. 또한 채팅방을 LongPress하게 되면 alert를 띄워서 채팅방을 나갈 수 있게끔 하였습니다.
import { useRoute } from '@react-navigation/core';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import RoomFooter from './components/RoomFooter';
import RoomFragment from './components/RoomFragment';
import RoomHeader from './components/RoomHeader';
const MessageRoom = () => {
const route = useRoute();
const messages = route.params.room;
return(
<View style={ styles.container }>
<RoomHeader messages={ messages } />
<RoomFragment messages={ messages }/>
<RoomFooter messages={ messages }/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1
}
})
export default MessageRoom;
채팅방을 클릭하게 되면 이동하는 컴포넌트입니다. 이 컴포넌트에서 유저들 간 대화를 나눌 수 있습니다.
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useSelector } from 'react-redux';
import palette from '../../../styles/palette';
const RoomHeader = messages => {
messages = messages.messages;
const { nickname } = useSelector(({ user }) => ({ nickname: user.user.nickname }));
const otherMan = messages.messages[0].sender === nickname ? messages.messages[0].receiver : messages.messages[0].sender;
return(
<View style={ styles.container }>
<Text style={ styles.font }>
{ otherMan } 님과의 채팅방
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 0.2,
backgroundColor: palette.white[0],
padding: 10,
borderBottomColor: palette.gray[2],
borderBottomWidth: 2,
},
font: {
fontSize: 18,
fontWeight: 'bold'
}
});
export default RoomHeader;
상단에서 누구와의 채팅방인지 정보를 보여주는 컴포넌트입니다.
import React from 'react';
import { StyleSheet, ScrollView } from 'react-native';
import palette from '../../../styles/palette';
import MessageCard from './MessageCard';
const RoomFragment = messages => {
const items = messages.messages;
return(
<ScrollView style={ styles.container }>
{
items.messages.map(message => { return <MessageCard message={ message } /> })
}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: palette.white[0],
}
});
export default RoomFragment;
유저들 간의 채팅 내역을 보여주는 UI입니다. RoomScreen에서 전달받은 messages 데이터의 messages 내역을 가져와 MessageCard 컴포넌트에 map함수를 이용하여 개별적으로 데이터를 전달합니다.
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useSelector } from 'react-redux';
import palette from '../../../styles/palette';
const MessageCard = message => {
const { nickname } = useSelector(({ user }) => ({ nickname: user.user.nickname }));
message = message.message
return(
<View style={ styles.container }>
<View style={
nickname === message.sender ?
styles.send_date : styles.recevie_date
}>
<Text>
{ message.createdAt.substring(0, 21) }
</Text>
</View>
<View style={
nickname === message.sender ?
styles.send : styles.receiver
}>
<Text style={
nickname === message.sender ?
styles.sender_font : styles.receiver_font
}>
{ message.content }
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
margin: 10,
},
send: {
maxWidth: 300,
padding: 10,
marginLeft: 100,
backgroundColor: palette.blue[0],
borderColor: palette.blue[0],
borderWidth: 1,
borderRadius: 10
},
sender_font: {
color: palette.white[0]
},
send_date: {
marginLeft: 250,
},
receiver: {
maxWidth: 300,
padding: 10,
backgroundColor: palette.white[0],
borderColor: palette.blue[0],
borderWidth: 1,
borderRadius: 10,
},
receiver_font: {
color: palette.blue[0]
},
recevie_date: {
marginLeft: 10
}
});
export default MessageCard;
현재 로그인되어 있는 유저의 nickname state를 가져와 message의 sender, receiver가 누군지 판별해줍니다. 예를 들어 nickname이 sender와 같다면 현재 로그인한 유저를 기준으로 유저는 sender가 되므로 sender의 메시지들은 오른쪽으로, receiver의 메시지들은 왼쪽으로 보내게 되는 것이죠.
import React from 'react';
import { StyleSheet, View } from 'react-native';
import MessageInput from './MessageInput';
const RoomFooter = () => {
return(
<View style={ styles.container }>
<MessageInput />
</View>
);
};
const styles = StyleSheet.create({
container: {
margin: 5,
}
});
export default RoomFooter;
하단에 위치하는 컴포넌트입니다. 이 곳에서 유저가 채팅을 입력하고, 버튼을 눌러 전송을 할 수 있습니다.
import React from 'react';
import { Dimensions, StyleSheet, TextInput, View } from 'react-native';
import palette from '../../../styles/palette';
import SendButton from './SendButton';
const MessageInput = () => {
return(
<View style={ styles.container }>
<TextInput style={ styles.input }
multiline={ true }
/>
<SendButton />
</View>
);
};
const styles = StyleSheet.create({
container: {
width: Dimensions.get('window').width,
flexDirection: 'row',
},
input: {
width: '90%',
backgroundColor: palette.gray[0],
},
});
export default MessageInput;
사용자가 채팅을 입력할 수 있는 컴포넌트입니다.
import React from 'react';
import Icon from 'react-native-vector-icons/Ionicons';
import { StyleSheet, TouchableOpacity } from 'react-native';
import palette from '../../../styles/palette';
const SendButton = () => {
return <TouchableOpacity style={ styles.button }>
<Icon name={ "ios-paper-plane" }
color={ palette.blue[0] }
size={ 30 }
/>
</TouchableOpacity>
};
const styles = StyleSheet.create({
button: {
justifyContent: 'center',
backgroundColor: palette.gray[0]
}
});
export default SendButton
전송버튼 컴포넌트입니다. 추후에 리덕스 모듈을 만들고 sender는 현재 로그인한 유저, receiver는 상대방으로 state를 저장하여 api를 호출할 수 있습니다.
UI가 완성되었으니 테스트를 해보겠습니다.
1) 현재 로그인한 유저
2) 채팅 메인 화면
3) 채팅방 LongPress
4) 채팅방
UI가 잘 완성되었습니다. 다음 포스트에서는 리덕스 모듈을 만들어 message-service와 연동을 진행해보도록 하겠습니다.