Chat App - Part 2 (1)

Sang heon lee·2022년 7월 4일
0

1. 인증 후 메인 화면

  • Stack Navigator 안에 Tab Navigator 가 포함되도록 작성
// src/navigations/Main.tsx

import React, { useContext } from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { Channel, ChannelCreation } from '../screens';
import { ThemeContext } from 'styled-components/native';
import Home from './Home';

export type MainStackParamList = {
  Channel: undefined;
  ChannelCreation: undefined;
  Home: undefined;
};

const Stack = createStackNavigator<MainStackParamList>();

const Main = () => {
  const theme = useContext(ThemeContext);

  return (
    <Stack.Navigator
      screenOptions={{
        headerTitleAlign: 'center',
        headerTintColor: theme.text,
        headerBackTitleVisible: false,
        cardStyle: { backgroundColor: theme.background },
      }}
    >
      <Stack.Screen name="Home" component={Home} /> // ⭐️⭐️⭐️
      <Stack.Screen name="Channel" component={Channel} />
      <Stack.Screen name="ChannelCreation" component={ChannelCreation} />
    </Stack.Navigator>
  );
};

export default Main;
// src/navigations/Home.tsx
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { ChannelList, Profile } from '../screens';

export type MainTabParamList = {
  List: undefined;
  Profile: { user: object };
};

const Tab = createBottomTabNavigator<MainTabParamList>();

const Home = () => {
  return (
    <Tab.Navigator screenOptions={{ headerShown: false }}>
      <Tab.Screen name="List" component={ChannelList} />
      <Tab.Screen name="Profile" component={Profile} />
    </Tab.Navigator>
  );
};

export default Home;

2. 탭 버튼과 헤더 타이틀

  • expo 의 materialIcons 활용
  • screen의 focused props활용
  • tabBarIcon 의 활용법 주의 : React.Node를 리턴해야 한다.

  • getFocusedRouteNameFromRoute : 현재 선택한 화면의 Name
// src/navigations/Home.tsx
  
import { getFocusedRouteNameFromRoute } from '@react-navigation/native';
  
const TabIcon = ({ name, focused }: { name; focused: boolean }) => {
  const theme: themeType = useContext(ThemeContext);
  return (
    <MaterialIcons
      name={name}
      size={26}
      color={focused ? theme.tabBtnActive : theme.tabBtnInActive}
    />
  );
};

const Home = ({ navigation, route }) => {
  useEffect(() => {
    const screenName = getFocusedRouteNameFromRoute(route) || 'List';
    navigation.setOptions({
      headerTitle: screenName,
    });
  });

  return (
    <Tab.Navigator screenOptions={{ headerShown: false }}>
      <Tab.Screen
        name="List"
        component={ChannelList}
        options={{
          tabBarIcon: ({ focused }) =>
            TabIcon({
              name: focused ? 'chat-bubble' : 'chat-bubble-outline',
              focused,
            }),
        }}
      />
      <Tab.Screen
        name="Profile"
        component={Profile}
        options={{
          tabBarIcon: ({ focused }) =>
            TabIcon({
              name: focused ? 'person' : 'person-outline',
              focused,
            }),
        }}
      />
    </Tab.Navigator>
  );
};

export default Home;  

3. 프로필 화면

  • 현재 로그인한 유저의 로그인 정보를 가져오는 함수를 작성하여 로그인 정보를 불러온뒤 랜더링 한다.
  • 불러온 정보 중 일부는 수정이 불가능하게끔 TextInput 컴포넌트의 editable 속성을 사용한다. 이에 어울리게끔 CSS도 수정한다.
// src/firebase.tsx
        
export const getCurrentUser = () => {
  const { uid, displayName, email, photoURL } = Auth.currentUser;
  return { uid, name: displayName, email, photo: photoURL };
};
        
// src/screens/Profile.tsx
        
export default function Profile({ navigation }: Props) {
  const { setUser } = useContext(UserContext);
  const theme = useContext(ThemeContext);
  const user = getCurrentUser(); // ⭐️⭐️⭐️⭐️⭐️

  const [photo, setPhoto] = useState(user.photo);

  const { spinner } = useContext(ProgressContext);

  const _handlePhotoChange = async (url: string) => {
    try {
      spinner.start();
      const photoURL = await updateUserInfo(url);
      setPhoto(photoURL);
    } catch (e) {
      Alert.alert('Photo upload Error');
    } finally {
      spinner.stop();
    }
  };

  return (
    <Container>
      <Image url={photo} onChanePhoto={_handlePhotoChange} showButton />
      // ⭐️⭐️⭐️⭐️⭐️
      <Input label="Name" value={user.name} disabled /> 
      <Input label="Email" value={user.email} disabled />
      <Button
        title="Logout"
        // onPress={() => setUser({ uid: null })}
        onPress={async () => {
          try {
            spinner.start();
            await signout();
          } catch (e) {
          } finally {
            setUser({ uid: null });
            spinner.stop();
          }
        }}
        containerStyle={{ backgroundColor: theme.btnSignout }}
      />
    </Container>
  );
}
// src/components/Input.tsx
      
const StyledInput = styled.TextInput.attrs<styledPropsType>(({ theme }) => ({
  placeholderTextColor: theme.inputPlaceholder,
}))<styledPropsType>`
  width: 100%;
  background-color: ${({ theme, editable }) =>
    editable ? theme.inputBackground : theme.inputDisabled}; // ⭐️⭐️⭐️⭐️⭐️
  color: ${({ theme }) => theme.text};
  padding: 20px 10px;
  font-size: 16px;
  border: 1px solid
    ${({ theme, isFocused }) => (isFocused ? theme.text : theme.inputBorder)};
  border-radius: 4px;
`;

const Input = forwardRef<TextInput, props>(
  (
    {
      label,
      value,
      onChangeText,
      onSubmitEditing,
      onBlur,
      placeholder,
      returnKeyType,
      maxLength,
      isPassword,
      disabled, // ⭐️⭐️⭐️⭐️⭐️
    },
    ref,
  ) => {
    const [isFocused, SetIsFocused] = useState(false);

    return (
      <Container>
        <Label isFocused={isFocused}>{label}</Label>
        <StyledInput
          ref={ref}
          value={value}
          onChangeText={onChangeText}
          onSubmitEditing={onSubmitEditing}
          onBlur={() => {
            SetIsFocused(false);
            onBlur;
          }}
          placeholder={placeholder}
          returnKeyType={returnKeyType}
          maxLength={maxLength}
          autoCapitalize="none" // 첫글자 대문자 방지
          autoCorrect={false} // 자동 고침 방지
          isFocused={isFocused}
          onFocus={() => SetIsFocused(true)}
          secureTextEntry={isPassword} // 비밀번호 ** 표시용
          editable={!disabled} // ⭐️⭐️⭐️⭐️⭐️
        />
      </Container>
    );
  },
);

export default Input;
  • 유저 정보중 프로필 이미지만 수정하도록 한다.
  1. 이미지 업로드 버튼 클릭
  2. Image 컴포넌트의 _handlePhotoBtnPress 함수 동작
    ImagePicker 동작, 이미지 업로드 url 반환
  3. _handlePhotoBtnPress 함수 내의 onChangePhoto 함수 동작
  4. Profile 컴포넌트의 onChangePhoto props로 _handlePhotoChange 함수 전달
  5. _handlePhotoChange 함수 내에 유저 정보 업데이트 함수 작동
  6. 유저 정보 업데이트 함수는 updateUserInfo(firebase.tsx)에 작성
  7. updateUserInfo 에서 firebase storage에 사진 정보를 저장하고 저장한 사진 url 주소를 반환하여 유저정보 업데이트
        
// src/screens/Profile.tsx
const _handlePhotoChange = async (url: string) => {
    try {
      spinner.start();
      const photoURL = await updateUserInfo(url);
      setPhoto(photoURL);
    } catch (e) {
      Alert.alert('Photo upload Error');
    } finally {
      spinner.stop();
    }
  };
        
return (
    <Container>
      <Image url={photo} onChanePhoto={_handlePhotoChange} showButton />
      <Input label="Name" value={user.name} disabled />
      <Input label="Email" value={user.email} disabled />     
// src/firebase.tsx
export const updateUserInfo = async (photo: string) => {
  const photoURL = await uploadImage(photo);
  await updateProfile(Auth.currentUser, { photoURL });
  return photoURL;
};      
import React, { useState } from 'react';
import styled from 'styled-components/native';
import { themeType } from '../theme';
import { MaterialIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';

const Container = styled.View`
  margin-bottom: 30px;
`;

interface styledPropsType {
  theme: themeType;
}

const ProfileImage = styled.Image<styledPropsType>`
  background-color: ${({ theme }) => theme.imgBackground};
  width: 100px;
  height: 100px;
  border-radius: 50px;
`;

const ButtonConatiner = styled.TouchableOpacity<styledPropsType>`
  background-color: ${({ theme }) => theme.imgBtnBackground};
  position: absolute;
  bottom: 0;
  right: 0;
  width: 30px;
  height: 30px;
  border-radius: 15px;
  justify-content: center;
  align-items: center;
`;

const ButtonIcon = styled(MaterialIcons).attrs<styledPropsType>(
  ({ theme }) => ({
    name: 'photo-camera',
    size: 22,
    color: theme.imgBtnIcon,
  }),
)``;

const PhotoButton = ({ onPress }: { onPress: () => void }) => {
  return (
    <ButtonConatiner onPress={onPress}>
      <ButtonIcon></ButtonIcon>
    </ButtonConatiner>
  );
};

interface PropsType {
  url?: string;
  showButton?: boolean;
  onChanePhoto: (text: string) => void;
}

const Image = ({ url, showButton = false, onChanePhoto }: PropsType) => {
  const _handlePhotoBtnPress = async () => {
    // No permissions request is necessary for launching the image library
    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images, // All : 모든 파일, Images : 사진(이미지)만
      allowsEditing: true, // 편집 가능
      aspect: [4, 3], // 이미지 사이즈 조정
      quality: 1, // 품질 옵션 0 ~ 1
    });

    console.log(result);

    if (!result.cancelled) {
      onChanePhoto(result['uri']);
    }
  };

  return (
    <Container>
      <ProfileImage source={{ uri: url }} resizeMode="contain" />
      {showButton && <PhotoButton onPress={_handlePhotoBtnPress} />}
    </Container>
  );
};

export default Image;
profile
개초보

0개의 댓글