Skip to content

Commit

Permalink
feat(rn): implement search list
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Jan 13, 2025
1 parent 91f5340 commit 01122c9
Show file tree
Hide file tree
Showing 18 changed files with 622 additions and 248 deletions.
30 changes: 28 additions & 2 deletions apps/mobile/src/components/ui/icon/fallback-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getBackgroundGradient, isCJKChar } from "@follow/utils"
import { Image } from "expo-image"
import { LinearGradient } from "expo-linear-gradient"
import { useMemo } from "react"
import { useMemo, useState } from "react"
import type { StyleProp, ViewStyle } from "react-native"
import { StyleSheet, Text } from "react-native"
import { StyleSheet, Text, View } from "react-native"

export const FallbackIcon = ({
title,
Expand Down Expand Up @@ -39,6 +40,31 @@ export const FallbackIcon = ({
)
}

export const IconWithFallback = (props: {
url?: string | undefined | null
size: number
title?: string
className?: string
style?: StyleProp<ViewStyle>
}) => {
const { url, size, title = "", className, style } = props
const [hasError, setHasError] = useState(false)

if (!url || hasError) {
return <FallbackIcon title={title} size={size} className={className} style={style} />
}

return (
<View className={className} style={style}>
<Image
source={{ uri: url }}
style={[{ width: size, height: size }]}
onError={() => setHasError(true)}
/>
</View>
)
}

const styles = StyleSheet.create({
container: {
display: "flex",
Expand Down
2 changes: 0 additions & 2 deletions apps/mobile/src/modules/discover/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ export enum SearchType {
Feed = "feed",
List = "list",
User = "user",
RSSHub = "rsshub",
}

export const SearchTabs = [
{ name: "Feed", value: SearchType.Feed },
{ name: "List", value: SearchType.List },
{ name: "User", value: SearchType.User },
{ name: "RSSHub", value: SearchType.RSSHub },
]
43 changes: 12 additions & 31 deletions apps/mobile/src/modules/discover/search-tabs/SearchFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { FeedViewType } from "@follow/constants"
import { withOpacity } from "@follow/utils"
import { useQuery } from "@tanstack/react-query"
import { Image } from "expo-image"
import { router } from "expo-router"
import { useAtomValue } from "jotai"
import { memo } from "react"
import type { ListRenderItem } from "react-native"
import { Text, useWindowDimensions, View } from "react-native"
import { ScrollView } from "react-native-gesture-handler"
import Animated, { FadeInUp } from "react-native-reanimated"

import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { LoadingIndicator } from "@/src/components/ui/loading"
import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
import { SadCuteReIcon } from "@/src/icons/sad_cute_re"
import { apiClient } from "@/src/lib/api-fetch"
import { useSubscriptionByFeedId } from "@/src/store/subscription/hooks"
import { useColor } from "@/src/theme/colors"

import { useSearchPageContext } from "../ctx"
import { BaseSearchPageFlatList, BaseSearchPageRootView, BaseSearchPageScrollView } from "./__base"
import { BaseSearchPageFlatList, ItemSeparator, RenderScrollComponent } from "./__base"
import { useDataSkeleton } from "./hooks"

type SearchResultItem = Awaited<ReturnType<typeof apiClient.discover.$post>>["data"][number]

Expand All @@ -39,50 +36,34 @@ export const SearchFeed = () => {
enabled: !!searchValue,
})

const textColor = useColor("text")

if (isLoading) {
return (
<BaseSearchPageRootView className="items-center justify-center">
<View className="-mt-72" />
<LoadingIndicator color={withOpacity(textColor, 0.7)} size={32} />
</BaseSearchPageRootView>
)
}

if (data?.data.length === 0) {
return (
<BaseSearchPageRootView className="items-center justify-center">
<View className="-mt-72" />
<SadCuteReIcon height={32} width={32} color={withOpacity(textColor, 0.5)} />
<Text className="text-text/50 mt-2">No results found</Text>
</BaseSearchPageRootView>
)
}
const skeleton = useDataSkeleton(isLoading, data)
if (skeleton) return skeleton

return (
<BaseSearchPageFlatList
refreshing={isLoading}
onRefresh={refetch}
keyExtractor={keyExtractor}
renderScrollComponent={(props) => <BaseSearchPageScrollView {...props} />}
contentContainerClassName={"-mt-4"}
renderScrollComponent={RenderScrollComponent}
data={data?.data}
renderItem={renderItem}
ItemSeparatorComponent={ItemSeparator}
/>
)
}
const keyExtractor = (item: SearchResultItem) => item.feed?.id ?? Math.random().toString()

const renderItem = ({ item }: { item: SearchResultItem }) => (
const renderItem: ListRenderItem<SearchResultItem> = ({ item }) => (
<SearchFeedItem key={item.feed?.id} item={item} />
)

const SearchFeedItem = memo(({ item }: { item: SearchResultItem }) => {
const SearchFeedItem = ({ item }: { item: SearchResultItem }) => {
const isSubscribed = useSubscriptionByFeedId(item.feed?.id ?? "")
return (
<Animated.View entering={FadeInUp}>
<ItemPressable
className="py-2"
className="py-6"
onPress={() => {
if (item.feed?.id) {
router.push(`/follow?id=${item.feed.id}`)
Expand Down Expand Up @@ -146,7 +127,7 @@ const SearchFeedItem = memo(({ item }: { item: SearchResultItem }) => {
</ItemPressable>
</Animated.View>
)
})
}
const formatter = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
Expand Down
105 changes: 100 additions & 5 deletions apps/mobile/src/modules/discover/search-tabs/SearchList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,111 @@
import { useQuery } from "@tanstack/react-query"
import { Image } from "expo-image"
import { router } from "expo-router"
import { useAtomValue } from "jotai"
import { Text } from "react-native"
import { memo } from "react"
import { Text, View } from "react-native"
import Animated, { FadeInUp } from "react-native-reanimated"

import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon"
import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
import { apiClient } from "@/src/lib/api-fetch"
import { useSubscriptionByListId } from "@/src/store/subscription/hooks"

import { useSearchPageContext } from "../ctx"
import { BaseSearchPageScrollView } from "./__base"
import { BaseSearchPageFlatList, ItemSeparator, RenderScrollComponent } from "./__base"
import { useDataSkeleton } from "./hooks"

type SearchResultItem = Awaited<ReturnType<typeof apiClient.discover.$post>>["data"][number]

export const SearchList = () => {
const { searchValueAtom } = useSearchPageContext()
const searchValue = useAtomValue(searchValueAtom)

const { data, isLoading, refetch } = useQuery({
queryKey: ["searchFeed", searchValue],
queryFn: () => {
return apiClient.discover.$post({
json: {
keyword: searchValue,
target: "lists",
},
})
},
enabled: !!searchValue,
})

const skeleton = useDataSkeleton(isLoading, data)
if (skeleton) return skeleton

return (
<BaseSearchPageScrollView>
<Text className="text-text">{searchValue}</Text>
</BaseSearchPageScrollView>
<BaseSearchPageFlatList
refreshing={isLoading}
onRefresh={refetch}
keyExtractor={keyExtractor}
renderScrollComponent={RenderScrollComponent}
data={data?.data}
renderItem={renderItem}
ItemSeparatorComponent={ItemSeparator}
/>
)
}

const keyExtractor = (item: SearchResultItem) => item.list?.id ?? Math.random().toString()

const renderItem = ({ item }: { item: SearchResultItem }) => (
<SearchListCard key={item.list?.id} item={item} />
)

const SearchListCard = memo(({ item }: { item: SearchResultItem }) => {
const isSubscribed = useSubscriptionByListId(item.list?.id ?? "")
return (
<Animated.View entering={FadeInUp}>
<ItemPressable
className="py-2"
onPress={() => {
if (item.list?.id) {
router.push(`/follow?id=${item.list.id}&type=list`)
}
}}
>
{/* Headline */}
<View className="flex-row items-center gap-2 pl-4 pr-2">
<View className="size-[32px] overflow-hidden rounded-lg">
{item.list?.image ? (
<Image
source={item.list.image}
className="size-full"
contentFit="cover"
transition={1000}
/>
) : (
!!item.list?.title && <FallbackIcon title={item.list.title} size={32} />
)}
</View>
<View className="flex-1">
<Text
className="text-text text-lg font-semibold"
ellipsizeMode="middle"
numberOfLines={1}
>
{item.list?.title}
</Text>
{!!item.list?.description && (
<Text className="text-text/60" ellipsizeMode="tail" numberOfLines={1}>
{item.list?.description}
</Text>
)}
</View>
{/* Subscribe */}
{isSubscribed && (
<View className="ml-auto">
<View className="bg-gray-5/60 rounded-full px-2 py-1">
<Text className="text-gray-2 text-sm font-medium">Subscribed</Text>
</View>
</View>
)}
</View>
</ItemPressable>
</Animated.View>
)
})
11 changes: 0 additions & 11 deletions apps/mobile/src/modules/discover/search-tabs/SearchRSSHub.tsx

This file was deleted.

12 changes: 10 additions & 2 deletions apps/mobile/src/modules/discover/search-tabs/__base.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cn } from "@follow/utils/src/utils"
import { forwardRef } from "react"
import type { ScrollViewProps } from "react-native"
import { RefreshControl, ScrollView, useWindowDimensions, View } from "react-native"
import { RefreshControl, ScrollView, StyleSheet, useWindowDimensions, View } from "react-native"
import type { FlatListPropsWithLayout } from "react-native-reanimated"
import Animated, { LinearTransition } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
Expand Down Expand Up @@ -70,10 +70,18 @@ export function BaseSearchPageFlatList<T>({
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
progressViewOffset={offsetTop}
progressViewOffset={searchBarHeight + insets.top}
/>
}
{...props}
/>
)
}
const itemSeparator = (
<View style={{ height: StyleSheet.hairlineWidth }} className="bg-opaque-separator ml-16 flex-1" />
)
export const ItemSeparator = () => itemSeparator

export const RenderScrollComponent = (props: ScrollViewProps) => (
<BaseSearchPageScrollView {...props} />
)
35 changes: 35 additions & 0 deletions apps/mobile/src/modules/discover/search-tabs/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { withOpacity } from "@follow/utils"
import { useMemo } from "react"
import { Text, View } from "react-native"

import { LoadingIndicator } from "@/src/components/ui/loading"
import { SadCuteReIcon } from "@/src/icons/sad_cute_re"
import { useColor } from "@/src/theme/colors"

import { BaseSearchPageRootView } from "./__base"

export const useDataSkeleton = (isLoading: boolean, data: any) => {
const textColor = useColor("text")
return useMemo(() => {
if (isLoading) {
return (
<BaseSearchPageRootView className="items-center justify-center">
<View className="-mt-72" />
<LoadingIndicator color={withOpacity(textColor, 0.7)} size={32} />
</BaseSearchPageRootView>
)
}

if (data?.data.length === 0) {
return (
<BaseSearchPageRootView className="items-center justify-center">
<View className="-mt-72" />
<SadCuteReIcon height={32} width={32} color={withOpacity(textColor, 0.5)} />
<Text className="text-text/50 mt-2">No results found</Text>
</BaseSearchPageRootView>
)
}

return null
}, [isLoading, data, textColor])
}
2 changes: 1 addition & 1 deletion apps/mobile/src/modules/discover/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const SearchHeader: FC<{
className="relative"
onLayout={onLayout}
>
{/* <BlurEffect /> */}
<BlurEffect />
<View style={styles.header}>
<ComposeSearchBar />
</View>
Expand Down
Loading

0 comments on commit 01122c9

Please sign in to comment.