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

feat: 해당하는 페이지에서 하단 탭 터치시 스크롤 최상단으로 이동 #162

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
44 changes: 43 additions & 1 deletion app/(protected)/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Tabs } from "expo-router";
import { Platform, Text, View } from "react-native";
import { DeviceEventEmitter, Platform, Text, View } from "react-native";

import { HeaderWithBack, HeaderWithNotification } from "@/components/Header";
import useSubscribeNotification from "@/hooks/useSubscribeNotification";
Expand Down Expand Up @@ -81,6 +81,14 @@ export default function TabsLayout() {
title: "Home",
tabBarIcon: ({ color }) => <TabIcon color={color} name="HOME" />,
}}
listeners={({ navigation, route }) => ({
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
Contributor

Choose a reason for hiding this comment

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

상단탭까지 손가락이 올라가는 것보다는 하단탭 두드리는 게 나을 것 같아요!!

tabPress: () => {
const state = navigation.getState();
if (state.routes[state.index].name === route.name) {
DeviceEventEmitter.emit("SCROLL_HOME_TO_TOP");
}
},
})}
/>
<Tabs.Screen
name="friend"
Expand All @@ -89,6 +97,32 @@ export default function TabsLayout() {
title: "Friend",
tabBarIcon: ({ color }) => <TabIcon color={color} name="FRIEND" />,
}}
listeners={({ navigation, route }) => ({
tabPress: () => {
const rootState = navigation.getState();
if (rootState.routes[rootState.index].name !== route.name) return;

const nestedState = rootState.routes[rootState.index].state as {
index: number;
routeNames: string[];
};
if (!nestedState?.routeNames) {
DeviceEventEmitter.emit("SCROLL_FRIEND_TO_TOP");
return;
}

const topTabName = nestedState.routeNames[nestedState.index];
const eventMap = {
index: "SCROLL_FRIEND_TO_TOP",
request: "SCROLL_REQUEST_TO_TOP",
};

const eventName = eventMap[topTabName as keyof typeof eventMap];
if (eventName) {
DeviceEventEmitter.emit(eventName);
}
},
})}
/>
<Tabs.Screen
name="upload"
Expand Down Expand Up @@ -119,6 +153,14 @@ export default function TabsLayout() {
title: "MyPage",
tabBarIcon: ({ color }) => <TabIcon color={color} name="MY_PAGE" />,
}}
listeners={({ navigation, route }) => ({
tabPress: () => {
const state = navigation.getState();
if (state.routes[state.index].name === route.name) {
DeviceEventEmitter.emit("SCROLL_MY_PAGE_TO_TOP");
}
},
})}
/>
</Tabs>
</>
Expand Down
16 changes: 10 additions & 6 deletions app/(protected)/(tabs)/friend/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";

import ErrorScreen from "@/components/ErrorScreen";
import { FriendItem } from "@/components/FriendItem";
Expand All @@ -23,6 +23,7 @@ export default function Friend() {
isFetchingNextPage,
error,
loadMore,
refetch,
} = useInfiniteLoad({
queryFn: getFriends(keyword),
queryKey: ["friends", keyword],
Expand All @@ -35,11 +36,13 @@ export default function Friend() {
}, 200);

// 친구창에 focus 들어올 때마다 친구목록 새로고침 (검색중일 때 제외)
useFocusEffect(() => {
if (!keyword && !isFetchingNextPage) {
queryClient.invalidateQueries({ queryKey: ["friends"] });
}
});
useFocusEffect(
useCallback(() => {
if (!keyword && !isFetchingNextPage) {
queryClient.invalidateQueries({ queryKey: ["friends"] });
}
}, [queryClient, keyword, isFetchingNextPage]),
);

// 친구의 운동 정보가 바뀌면 쿼리 다시 패치하도록 정보 구독
useEffect(() => {
Expand Down Expand Up @@ -80,6 +83,7 @@ export default function Friend() {

return (
<SearchLayout
refetch={refetch}
data={friends}
onChangeKeyword={handleKeywordChange}
loadMore={loadMore}
Expand Down
38 changes: 31 additions & 7 deletions app/(protected)/(tabs)/friend/request.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useEffect } from "react";
import { ActivityIndicator, FlatList, View } from "react-native";
import { useCallback, useEffect, useRef } from "react";
import {
ActivityIndicator,
DeviceEventEmitter,
FlatList,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

import ErrorScreen from "@/components/ErrorScreen";
Expand All @@ -20,6 +25,7 @@ const LIMIT = 12;

export default function Request() {
const queryClient = useQueryClient();
const flatListRef = useRef<FlatList>(null);

// 유저의 친구 요청 정보 조회
const {
Expand All @@ -28,6 +34,7 @@ export default function Request() {
isFetchingNextPage,
error,
loadMore,
refetch,
} = useInfiniteLoad({
queryFn: getFriendRequests,
queryKey: ["friendRequests"],
Expand All @@ -36,11 +43,13 @@ export default function Request() {
const hasRequests = !!requestData?.pages[0].total;

// 친구 요청창에 focus 들어올 때마다 친구목록 새로고침
useFocusEffect(() => {
if (!isFetchingNextPage) {
queryClient.invalidateQueries({ queryKey: ["friendRequests"] });
}
});
useFocusEffect(
useCallback(() => {
if (!isFetchingNextPage) {
queryClient.invalidateQueries({ queryKey: ["friendRequests"] });
}
}, [isFetchingNextPage, queryClient]),
);

// 친구 요청이 추가되면 쿼리 다시 패치하도록 정보 구독
useEffect(() => {
Expand All @@ -60,6 +69,20 @@ export default function Request() {
};
}, [queryClient.invalidateQueries]);

const handleScrollToTop = useCallback(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
refetch();
}, [refetch]);

useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
"SCROLL_REQUEST_TO_TOP",
handleScrollToTop,
);

return () => subscription.remove();
}, [handleScrollToTop]);

// 에러 스크린
if (error) {
return <ErrorScreen errorMessage={error.message} />;
Expand All @@ -73,6 +96,7 @@ export default function Request() {
return (
<SafeAreaView edges={[]} className="flex-1 bg-white">
<FlatList
ref={flatListRef}
className="w-full grow px-8"
data={hasRequests ? requestData.pages.flatMap((page) => page.data) : []}
keyExtractor={(request) => String(request.requestId)}
Expand Down
19 changes: 18 additions & 1 deletion app/(protected)/(tabs)/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import useRefresh from "@/hooks/useRefresh";
import { getPostLikes, getPosts } from "@/utils/supabase";
import { useRouter } from "expo-router";
import * as SecureStore from "expo-secure-store";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
DeviceEventEmitter,
Dimensions,
FlatList,
Image,
Expand All @@ -33,6 +34,7 @@ export default function Home() {
const [selectedPostId, setSelectedPostId] = useState<number | null>(null);
const [selectedAuthorId, setSelectedAuthorId] = useState<string | null>(null);
const [isLikedModalVisible, setIsLikedModalVisible] = useState(false);
const flatListRef = useRef<FlatList>(null);
const { openModal } = useModal();

const router = useRouter();
Expand Down Expand Up @@ -91,9 +93,24 @@ export default function Home() {
handleLoadId();
}, []);

const handleScrollToTop = useCallback(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
onRefresh();
}, [onRefresh]);

useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
"SCROLL_HOME_TO_TOP",
handleScrollToTop,
);

return () => subscription.remove();
}, [handleScrollToTop]);

return (
<SafeAreaView edges={[]} className="flex-1 items-center justify-center">
<FlatList
ref={flatListRef}
data={data?.pages.flatMap((page) => page.data) ?? []}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{ gap: 10 }}
Expand Down
2 changes: 2 additions & 0 deletions app/(protected)/(tabs)/mypage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function MyPage() {
data: posts,
isLoading: isPostsLoading,
isError: isPostsError,
refetch,
} = useFetchData(
["userPosts"],
() => getMyPosts(),
Expand All @@ -43,6 +44,7 @@ export default function MyPage() {
}
/>
<PostGrid
refetch={refetch}
posts={
posts
? posts.map((post) => ({ ...post, id: post.id.toString() }))
Expand Down
21 changes: 20 additions & 1 deletion components/PostGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import images from "@/constants/images";
import { useRouter } from "expo-router";
import { useCallback, useEffect, useRef } from "react";
import {
DeviceEventEmitter,
Dimensions,
FlatList,
Image,
Expand All @@ -14,12 +16,28 @@ interface Post {
}

interface PostGridProps {
refetch: () => void;
posts: Post[] | null;
isError?: boolean;
}

export default function PostGrid({ posts, isError }: PostGridProps) {
export default function PostGrid({ refetch, posts, isError }: PostGridProps) {
const router = useRouter();
const flatListRef = useRef<FlatList>(null);

const handleScrollToTop = useCallback(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
refetch();
}, [refetch]);

useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
"SCROLL_MY_PAGE_TO_TOP",
handleScrollToTop,
);

return () => subscription.remove();
}, [handleScrollToTop]);

if (isError) {
return (
Expand Down Expand Up @@ -52,6 +70,7 @@ export default function PostGrid({ posts, isError }: PostGridProps) {
return (
<View className="mt-[32px] h-full bg-gray-5">
<FlatList
ref={flatListRef}
data={posts}
renderItem={({ item }) => {
const size = Dimensions.get("window").width / 3;
Expand Down
21 changes: 20 additions & 1 deletion components/SearchLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import colors from "@/constants/colors";
import type { UserProfile } from "@/types/User.interface";
import { useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
DeviceEventEmitter,
FlatList,
type ListRenderItemInfo,
View,
Expand All @@ -11,6 +12,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
import SearchBar from "./SearchBar";

interface SearchLayoutProps {
refetch?: () => void;
data: UserProfile[]; // 추후 검색 사용 범위 넓어지면 변경 가능
isFetchingNextPage?: boolean;
onChangeKeyword: (newKeyword: string) => void;
Expand All @@ -20,6 +22,7 @@ interface SearchLayoutProps {
}

export function SearchLayout<T>({
refetch,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

제네릭 타입 T가 사용되지 않고 있습니다.

컴포넌트에 제네릭 타입 T가 선언되어 있지만 실제로 사용되지 않고 있습니다. 또한 UserProfile 타입이 하드코딩되어 있어 재사용성이 제한됩니다.

다음과 같이 개선하는 것을 제안드립니다:

- export function SearchLayout<T>({
+ export function SearchLayout<T extends { id: string }>({
  refetch,
- data: UserProfile[],
+ data: T[],
  ...
- renderItem: (itemInfo: ListRenderItemInfo<UserProfile>) => React.ReactElement;
+ renderItem: (itemInfo: ListRenderItemInfo<T>) => React.ReactElement;

Committable suggestion skipped: line range outside the PR's diff.

data,
isFetchingNextPage,
onChangeKeyword,
Expand All @@ -28,10 +31,26 @@ export function SearchLayout<T>({
emptyComponent,
}: SearchLayoutProps) {
const [keyword, setKeyword] = useState("");
const flatListRef = useRef<FlatList>(null);

const handleScrollToTop = useCallback(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
refetch?.();
}, [refetch]);

useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
"SCROLL_FRIEND_TO_TOP",
handleScrollToTop,
);

return () => subscription.remove();
}, [handleScrollToTop]);

return (
<SafeAreaView edges={[]} className="flex-1 bg-white">
<FlatList
ref={flatListRef}
className="w-full grow px-6"
data={data}
keyExtractor={(elem) => elem.id}
Expand Down