diff --git a/components/ConversationFlashList/ConversationFlashList.constants.ts b/components/ConversationFlashList/ConversationFlashList.constants.ts new file mode 100644 index 000000000..ebd92b868 --- /dev/null +++ b/components/ConversationFlashList/ConversationFlashList.constants.ts @@ -0,0 +1,6 @@ +import { Platform } from "react-native"; + +// iOS has it's own bounce and search bar, so we need to set a different threshold +// Android does not have a bounce, so this will never really get hit. +export const CONVERSATION_FLASH_LIST_REFRESH_THRESHOLD = + Platform.OS === "ios" ? -190 : 0; diff --git a/components/ConversationFlashList.tsx b/components/ConversationFlashList/ConversationFlashList.tsx similarity index 68% rename from components/ConversationFlashList.tsx rename to components/ConversationFlashList/ConversationFlashList.tsx index cdfd0fc2e..722e15397 100644 --- a/components/ConversationFlashList.tsx +++ b/components/ConversationFlashList/ConversationFlashList.tsx @@ -3,22 +3,30 @@ import { FlashList } from "@shopify/flash-list"; import { backgroundColor } from "@styles/colors"; import { ConversationListContext } from "@utils/conversationList"; import { useCallback, useEffect, useRef } from "react"; -import { Platform, StyleSheet, View, useColorScheme } from "react-native"; +import { + NativeScrollEvent, + NativeSyntheticEvent, + Platform, + StyleSheet, + View, + useColorScheme, +} from "react-native"; -import HiddenRequestsButton from "./ConversationList/HiddenRequestsButton"; -import { V3GroupConversationListItem } from "./V3GroupConversationListItem"; -import { useChatStore, useCurrentAccount } from "../data/store/accountsStore"; -import { useSelect } from "../data/store/storeHelpers"; -import { NavigationParamList } from "../screens/Navigation/Navigation"; -import { ConversationFlatListHiddenRequestItem } from "../utils/conversation"; -import { FlatListItemType } from "../features/conversation-list/ConversationList.types"; +import HiddenRequestsButton from "../ConversationList/HiddenRequestsButton"; +import { V3GroupConversationListItem } from "../V3GroupConversationListItem"; +import { useChatStore, useCurrentAccount } from "@data/store/accountsStore"; +import { useSelect } from "@data/store/storeHelpers"; +import { NavigationParamList } from "@screens/Navigation/Navigation"; +import { ConversationFlatListHiddenRequestItem } from "@utils/conversation"; +import { FlatListItemType } from "@features/conversation-list/ConversationList.types"; import { unwrapConversationContainer } from "@utils/groupUtils/conversationContainerHelpers"; import { ConversationVersion } from "@xmtp/react-native-sdk"; import { DmWithCodecsType, GroupWithCodecsType, } from "@/utils/xmtpRN/client.types"; -import { V3DMListItem } from "./V3DMListItem"; +import { V3DMListItem } from "../V3DMListItem"; +import { CONVERSATION_FLASH_LIST_REFRESH_THRESHOLD } from "./ConversationFlashList.constants"; type Props = { onScroll?: () => void; @@ -26,7 +34,7 @@ type Props = { itemsForSearchQuery?: string; ListHeaderComponent?: React.ReactElement | null; ListFooterComponent?: React.ReactElement | null; - refetch?: () => void; + refetch?: () => Promise; isRefetching?: boolean; } & NativeStackScreenProps< NavigationParamList, @@ -61,6 +69,7 @@ export default function ConversationFlashList({ ); const userAddress = useCurrentAccount() as string; const listRef = useRef | undefined>(); + const refreshingRef = useRef(false); const renderItem = useCallback(({ item }: { item: FlatListItemType }) => { if ("lastMessage" in item) { @@ -87,6 +96,34 @@ export default function ConversationFlashList({ return null; }, []); + const handleRefresh = useCallback(async () => { + if (refreshingRef.current) return; + refreshingRef.current = true; + try { + console.log("refetching from pull"); + await refetch?.(); + } catch (error) { + console.error(error); + } finally { + refreshingRef.current = false; + } + }, [refetch]); + + const onScrollList = useCallback( + (e: NativeSyntheticEvent) => { + if (refreshingRef.current) return; + // On Android the list does not bounce, so this will only get hit + // on iOS when the user scrolls up + if ( + e.nativeEvent.contentOffset.y < + CONVERSATION_FLASH_LIST_REFRESH_THRESHOLD + ) { + handleRefresh(); + } + }, + [handleRefresh] + ); + return ( 0} contentInsetAdjustmentBehavior="automatic" data={items} diff --git a/features/blocked-chats/ConversationBlockedListNav.tsx b/features/blocked-chats/ConversationBlockedListNav.tsx index db8908a39..85196587a 100644 --- a/features/blocked-chats/ConversationBlockedListNav.tsx +++ b/features/blocked-chats/ConversationBlockedListNav.tsx @@ -13,9 +13,9 @@ import { NativeStack, navigationAnimation, NavigationParamList, -} from "../../screens/Navigation/Navigation"; -import AndroidBackAction from "../../components/AndroidBackAction"; -import ConversationFlashList from "../../components/ConversationFlashList"; +} from "@screens/Navigation/Navigation"; +import AndroidBackAction from "@components/AndroidBackAction"; +import ConversationFlashList from "@/components/ConversationFlashList/ConversationFlashList"; import { translate } from "@i18n/index"; import { useAllBlockedChats } from "./useAllBlockedChats"; diff --git a/features/conversation/conversation-list.contstants.ts b/features/conversation/conversation-list.contstants.ts new file mode 100644 index 000000000..55f251b33 --- /dev/null +++ b/features/conversation/conversation-list.contstants.ts @@ -0,0 +1,6 @@ +import { Platform } from "react-native"; + +// On iOS the list has a bounce, so we need to set a different threshold +// to trigger the refresh. +export const CONVERSATION_LIST_REFRESH_THRESHOLD = + Platform.OS === "ios" ? -120 : 0; diff --git a/features/conversation/conversation.tsx b/features/conversation/conversation.tsx index 580451339..4998d30ec 100644 --- a/features/conversation/conversation.tsx +++ b/features/conversation/conversation.tsx @@ -74,6 +74,12 @@ import { ConversationStoreProvider, useCurrentConversationTopic, } from "./conversation.store-context"; +import { CONVERSATION_LIST_REFRESH_THRESHOLD } from "./conversation-list.contstants"; +import { + NativeScrollEvent, + NativeSyntheticEvent, + Platform, +} from "react-native"; export const Conversation = memo(function Conversation(props: { topic: ConversationTopic; @@ -186,6 +192,8 @@ const Messages = memo(function Messages(props: { const { data: currentAccountInboxId } = useCurrentAccountInboxId(); const topic = useCurrentConversationTopic()!; + const refreshingRef = useRef(false); + const { data: messages, isLoading: messagesLoading, @@ -223,11 +231,33 @@ const Messages = memo(function Messages(props: { } }, [isUnread, messagesLoading, toggleReadStatus]); + const handleRefresh = useCallback(async () => { + try { + refreshingRef.current = true; + await refetch(); + } catch (e) { + console.error(e); + } finally { + refreshingRef.current = false; + } + }, [refetch]); + + const onScroll = useCallback( + (e: NativeSyntheticEvent) => { + if (refreshingRef.current && !isRefetchingMessages) return; + if (e.nativeEvent.contentOffset.y < CONVERSATION_LIST_REFRESH_THRESHOLD) { + handleRefresh(); + } + }, + [handleRefresh, isRefetchingMessages] + ); + return ( diff --git a/screens/ConversationList.tsx b/screens/ConversationList.tsx index bc9f20d79..1dcf41f02 100644 --- a/screens/ConversationList.tsx +++ b/screens/ConversationList.tsx @@ -18,7 +18,7 @@ import { gestureHandlerRootHOC } from "react-native-gesture-handler"; import { SearchBarCommands } from "react-native-screens"; import ChatNullState from "../components/ConversationList/ChatNullState"; -import ConversationFlashList from "../components/ConversationFlashList"; +import ConversationFlashList from "../components/ConversationFlashList/ConversationFlashList"; import NewConversationButton from "../components/ConversationList/NewConversationButton"; import RequestsButton from "../components/ConversationList/RequestsButton"; import EphemeralAccountBanner from "../components/EphemeralAccountBanner"; diff --git a/screens/Navigation/ConversationListNav.tsx b/screens/Navigation/ConversationListNav.tsx index 09842b66e..caacac841 100644 --- a/screens/Navigation/ConversationListNav.tsx +++ b/screens/Navigation/ConversationListNav.tsx @@ -108,9 +108,6 @@ export default function ConversationListNav() { placeholder={searchPlaceholder()} onChangeText={onChangeSearch} value={searchQuery} - icon={({ color }) => ( - - )} mode="bar" autoCapitalize="none" autoFocus={false} diff --git a/screens/Navigation/ConversationRequestsListNav.ios.tsx b/screens/Navigation/ConversationRequestsListNav.ios.tsx index 881024eab..2462bdb78 100644 --- a/screens/Navigation/ConversationRequestsListNav.ios.tsx +++ b/screens/Navigation/ConversationRequestsListNav.ios.tsx @@ -20,7 +20,7 @@ import { } from "./Navigation"; import ActivityIndicator from "../../components/ActivityIndicator/ActivityIndicator"; import Button from "../../components/Button/Button"; -import ConversationFlashList from "../../components/ConversationFlashList"; +import ConversationFlashList from "@/components/ConversationFlashList/ConversationFlashList"; import { showActionSheetWithOptions } from "../../components/StateHandlers/ActionSheetStateHandler"; import { useCurrentAccount } from "../../data/store/accountsStore"; import { consentToAddressesOnProtocolByAccount } from "../../utils/xmtpRN/contacts"; diff --git a/screens/Navigation/ConversationRequestsListNav.tsx b/screens/Navigation/ConversationRequestsListNav.tsx index 2ba5a7730..d039dec23 100644 --- a/screens/Navigation/ConversationRequestsListNav.tsx +++ b/screens/Navigation/ConversationRequestsListNav.tsx @@ -16,14 +16,14 @@ import { navigationAnimation, NavigationParamList, } from "./Navigation"; -import AndroidBackAction from "../../components/AndroidBackAction"; -import Button from "../../components/Button/Button"; -import ConversationFlashList from "../../components/ConversationFlashList"; -import { showActionSheetWithOptions } from "../../components/StateHandlers/ActionSheetStateHandler"; -import { useCurrentAccount } from "../../data/store/accountsStore"; -import { consentToAddressesOnProtocolByAccount } from "../../utils/xmtpRN/contacts"; -import { useRequestItems } from "../../features/conversation-requests-list/useRequestItems"; -import { FlatListItemType } from "../../features/conversation-list/ConversationList.types"; +import AndroidBackAction from "@components/AndroidBackAction"; +import Button from "@components/Button/Button"; +import ConversationFlashList from "@/components/ConversationFlashList/ConversationFlashList"; +import { showActionSheetWithOptions } from "@components/StateHandlers/ActionSheetStateHandler"; +import { useCurrentAccount } from "@data/store/accountsStore"; +import { consentToAddressesOnProtocolByAccount } from "@utils/xmtpRN/contacts"; +import { useRequestItems } from "@features/conversation-requests-list/useRequestItems"; +import { FlatListItemType } from "@features/conversation-list/ConversationList.types"; // TODO: Remove iOS-specific code due to the existence of a .ios file // TODO: Alternatively, implement an Android equivalent for the segmented controller