mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
refactor(native): extract shared RecentlyViewedRowContent and polish iOS search list
Extract the thumbnail + text content into a shared `RecentlyViewedRowContent` component so both the iOS SwiftUI list and the Android/default `RecentlyViewedRow` render identical row content without duplication. Rework the iOS recently-viewed list header to use an `RNHostView`-backed React Native view instead of SwiftUI `Section`/`HStack`/`Button`, giving it the app's display font, a history icon, and a native `Pressable` clear button. Switch each list row to the same `RNHostView` pattern so rows render with the app's background color and consistent insets. Replace SF Symbols type icons with Tabler icons (`IconMovie`, `IconDeviceTv`, `IconUser`) to match the rest of the app.
This commit is contained in:
@@ -1,51 +1,34 @@
|
||||
import { Host, List, RNHostView, VStack } from "@expo/ui/swift-ui";
|
||||
import {
|
||||
Button,
|
||||
Host,
|
||||
HStack,
|
||||
Image,
|
||||
List,
|
||||
Section,
|
||||
Spacer,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@expo/ui/swift-ui";
|
||||
import {
|
||||
font,
|
||||
foregroundStyle,
|
||||
deleteDisabled,
|
||||
listRowBackground,
|
||||
listRowInsets,
|
||||
listRowSeparator,
|
||||
listStyle,
|
||||
onTapGesture,
|
||||
scrollContentBackground,
|
||||
} from "@expo/ui/swift-ui/modifiers";
|
||||
import { IconSearch } from "@tabler/icons-react-native";
|
||||
import { IconHistory, IconSearch } from "@tabler/icons-react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { Alert, View } from "react-native";
|
||||
import { Alert, Pressable, View } from "react-native";
|
||||
import Animated, { FadeIn } from "react-native-reanimated";
|
||||
import type { SFSymbol } from "sf-symbols-typescript";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
import { Text as RNText } from "@/components/ui/text";
|
||||
import { RecentlyViewedRowContent } from "@/components/search/recently-viewed-row-content";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import {
|
||||
type RecentlyViewedItem,
|
||||
useRecentlyViewed,
|
||||
} from "@/lib/recently-viewed";
|
||||
import * as Haptics from "@/utils/haptics";
|
||||
|
||||
const SF_SYMBOL: Record<RecentlyViewedItem["type"], SFSymbol> = {
|
||||
movie: "film",
|
||||
tv: "tv",
|
||||
person: "person.fill",
|
||||
};
|
||||
|
||||
const TYPE_LABEL: Record<RecentlyViewedItem["type"], string> = {
|
||||
movie: "Movie",
|
||||
tv: "TV Show",
|
||||
person: "Person",
|
||||
};
|
||||
|
||||
export function RecentlyViewedList() {
|
||||
const { navigate } = useRouter();
|
||||
const { items, removeItem, clearAll } = useRecentlyViewed();
|
||||
const mutedForeground = useCSSVariable("--color-muted-foreground") as string;
|
||||
const [mutedForeground, primaryColor] = useCSSVariable([
|
||||
"--color-muted-foreground",
|
||||
"--color-primary",
|
||||
]) as [string, string];
|
||||
|
||||
const handlePress = useCallback(
|
||||
(item: RecentlyViewedItem) => {
|
||||
@@ -92,9 +75,9 @@ export function RecentlyViewedList() {
|
||||
className="flex-1 items-center justify-center"
|
||||
>
|
||||
<IconSearch size={64} color={mutedForeground} />
|
||||
<RNText className="mt-3 text-[15px] text-muted-foreground">
|
||||
<Text className="mt-3 text-[15px] text-muted-foreground">
|
||||
Search for movies, shows, or people
|
||||
</RNText>
|
||||
</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
@@ -105,76 +88,50 @@ export function RecentlyViewedList() {
|
||||
<List
|
||||
modifiers={[listStyle("plain"), scrollContentBackground("hidden")]}
|
||||
>
|
||||
<Section
|
||||
header={
|
||||
<HStack>
|
||||
<Image systemName="clock.arrow.circlepath" size={14} />
|
||||
<Text modifiers={[font({ weight: "semibold", size: 14 })]}>
|
||||
Recently Viewed
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Button
|
||||
label="Clear"
|
||||
onPress={handleClear}
|
||||
modifiers={[
|
||||
font({ size: 14 }),
|
||||
foregroundStyle({
|
||||
type: "hierarchical",
|
||||
style: "secondary",
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</HStack>
|
||||
}
|
||||
<VStack
|
||||
modifiers={[
|
||||
listRowBackground("clear"),
|
||||
listRowInsets({ leading: 0, trailing: 0 }),
|
||||
listRowSeparator("hidden"),
|
||||
deleteDisabled(),
|
||||
]}
|
||||
>
|
||||
<List.ForEach onDelete={handleDelete}>
|
||||
{items.map((item) => (
|
||||
<HStack
|
||||
key={item.id}
|
||||
spacing={12}
|
||||
alignment="center"
|
||||
modifiers={[onTapGesture(() => handlePress(item))]}
|
||||
<RNHostView matchContents>
|
||||
<View className="flex-row items-center justify-between bg-background px-2">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<IconHistory size={20} color={primaryColor} />
|
||||
<Text className="font-display text-[20px] text-foreground tracking-tight">
|
||||
Recently Viewed
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={handleClear}
|
||||
hitSlop={8}
|
||||
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
|
||||
>
|
||||
<Image
|
||||
systemName={SF_SYMBOL[item.type]}
|
||||
size={20}
|
||||
modifiers={[
|
||||
foregroundStyle({
|
||||
type: "hierarchical",
|
||||
style: "secondary",
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
<VStack alignment="leading" spacing={2}>
|
||||
<Text
|
||||
modifiers={[
|
||||
font({ weight: "medium", size: 16 }),
|
||||
foregroundStyle({
|
||||
type: "hierarchical",
|
||||
style: "primary",
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text
|
||||
modifiers={[
|
||||
font({ size: 13 }),
|
||||
foregroundStyle({
|
||||
type: "hierarchical",
|
||||
style: "secondary",
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{TYPE_LABEL[item.type]}
|
||||
{item.subtitle ? ` · ${item.subtitle}` : ""}
|
||||
</Text>
|
||||
</VStack>
|
||||
<Spacer />
|
||||
</HStack>
|
||||
))}
|
||||
</List.ForEach>
|
||||
</Section>
|
||||
<Text className="text-[13px] text-primary">Clear</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</RNHostView>
|
||||
</VStack>
|
||||
<List.ForEach onDelete={handleDelete}>
|
||||
{items.map((item) => (
|
||||
<VStack
|
||||
key={item.id}
|
||||
modifiers={[
|
||||
listRowBackground("clear"),
|
||||
listRowInsets({ leading: 0, trailing: 0 }),
|
||||
onTapGesture(() => handlePress(item)),
|
||||
]}
|
||||
>
|
||||
<RNHostView matchContents>
|
||||
<View className="bg-background px-2">
|
||||
<RecentlyViewedRowContent item={item} />
|
||||
</View>
|
||||
</RNHostView>
|
||||
</VStack>
|
||||
))}
|
||||
</List.ForEach>
|
||||
</List>
|
||||
</Host>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { IconDeviceTv, IconMovie, IconUser } from "@tabler/icons-react-native";
|
||||
import { View } from "react-native";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { RecentlyViewedItem } from "@/lib/recently-viewed";
|
||||
|
||||
const TypeIcon = {
|
||||
movie: IconMovie,
|
||||
tv: IconDeviceTv,
|
||||
person: IconUser,
|
||||
} as const;
|
||||
|
||||
const TypeLabel = {
|
||||
movie: "Movie",
|
||||
tv: "TV",
|
||||
person: "Person",
|
||||
} as const;
|
||||
|
||||
export function RecentlyViewedRowContent({
|
||||
item,
|
||||
}: {
|
||||
item: RecentlyViewedItem;
|
||||
}) {
|
||||
const mutedForeground = useCSSVariable("--color-muted-foreground") as string;
|
||||
const Icon = TypeIcon[item.type];
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center">
|
||||
<View
|
||||
className="mr-3 overflow-hidden bg-secondary"
|
||||
style={{
|
||||
width: 48,
|
||||
height: item.type === "person" ? 48 : 72,
|
||||
borderRadius: item.type === "person" ? 24 : 10,
|
||||
borderCurve: item.type === "person" ? undefined : "continuous",
|
||||
}}
|
||||
>
|
||||
{item.imagePath ? (
|
||||
<Image
|
||||
source={{ uri: item.imagePath }}
|
||||
className="h-full w-full"
|
||||
contentFit="cover"
|
||||
recyclingKey={`rv-${item.id}`}
|
||||
transition={200}
|
||||
/>
|
||||
) : (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Icon size={20} color={mutedForeground} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className="font-sans-medium text-[15px] text-foreground"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<View className="mt-1 flex-row items-center gap-2">
|
||||
<View className="rounded-full bg-secondary px-2 py-0.5">
|
||||
<Text className="text-[10px] text-muted-foreground">
|
||||
{TypeLabel[item.type]}
|
||||
</Text>
|
||||
</View>
|
||||
{item.subtitle ? (
|
||||
<Text className="text-muted-foreground text-xs">
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,9 @@
|
||||
import { IconDeviceTv, IconMovie, IconUser } from "@tabler/icons-react-native";
|
||||
import { memo } from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { Pressable } from "react-native";
|
||||
import { RecentlyViewedRowContent } from "@/components/search/recently-viewed-row-content";
|
||||
import { SwipeableRow } from "@/components/ui/swipeable-row";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { RecentlyViewedItem } from "@/lib/recently-viewed";
|
||||
|
||||
const TypeIcon = {
|
||||
movie: IconMovie,
|
||||
tv: IconDeviceTv,
|
||||
person: IconUser,
|
||||
} as const;
|
||||
|
||||
const TypeLabel = {
|
||||
movie: "Movie",
|
||||
tv: "TV",
|
||||
person: "Person",
|
||||
} as const;
|
||||
|
||||
export const RecentlyViewedRow = memo(function RecentlyViewedRow({
|
||||
item,
|
||||
onPress,
|
||||
@@ -28,10 +13,11 @@ export const RecentlyViewedRow = memo(function RecentlyViewedRow({
|
||||
onPress: (item: RecentlyViewedItem) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}) {
|
||||
const mutedForeground = useCSSVariable("--color-muted-foreground") as string;
|
||||
const Icon = TypeIcon[item.type];
|
||||
|
||||
const accessibilityLabel = [item.title, TypeLabel[item.type], item.subtitle]
|
||||
const accessibilityLabel = [
|
||||
item.title,
|
||||
item.type === "tv" ? "TV" : item.type === "movie" ? "Movie" : "Person",
|
||||
item.subtitle,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
@@ -41,59 +27,14 @@ export const RecentlyViewedRow = memo(function RecentlyViewedRow({
|
||||
onPress={() => onPress(item)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
className="flex-row items-center bg-background px-4 py-3"
|
||||
className="bg-background px-4 py-3"
|
||||
style={({ pressed }) => ({
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: "rgba(255,255,255,0.08)",
|
||||
opacity: pressed ? 0.7 : 1,
|
||||
})}
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<View
|
||||
className="mr-3 overflow-hidden bg-secondary"
|
||||
style={{
|
||||
width: item.type === "person" ? 48 : 48,
|
||||
height: item.type === "person" ? 48 : 72,
|
||||
borderRadius: item.type === "person" ? 24 : 10,
|
||||
borderCurve: item.type === "person" ? undefined : "continuous",
|
||||
}}
|
||||
>
|
||||
{item.imagePath ? (
|
||||
<Image
|
||||
source={{ uri: item.imagePath }}
|
||||
className="h-full w-full"
|
||||
contentFit="cover"
|
||||
recyclingKey={`rv-${item.id}`}
|
||||
transition={200}
|
||||
/>
|
||||
) : (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Icon size={20} color={mutedForeground} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Text content */}
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className="font-sans-medium text-[15px] text-foreground"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<View className="mt-1 flex-row items-center gap-2">
|
||||
<View className="rounded-full bg-secondary px-2 py-0.5">
|
||||
<Text className="text-[10px] text-muted-foreground">
|
||||
{TypeLabel[item.type]}
|
||||
</Text>
|
||||
</View>
|
||||
{item.subtitle ? (
|
||||
<Text className="text-muted-foreground text-xs">
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<RecentlyViewedRowContent item={item} />
|
||||
</Pressable>
|
||||
</SwipeableRow>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user