diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 4acf2b2..3bf7f08 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -21,7 +21,6 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { DownArrowIcon, PointsIcon, SearchIcon } from '@/components/icon'; import ErrorState from '@/components/ErrorState'; import LoadingState from '@/components/LoadingState'; -import RefreshControl from '@/components/RefreshControl'; import { useActivates } from '@/hooks/use-activates'; import { useCategories } from '@/hooks/use-categories'; import { CategoryTemplate } from '@repo/sdk'; @@ -84,7 +83,6 @@ export default function HomeScreen() { const params = useLocalSearchParams() const [activeTab, setActiveTab] = useState(0) const [selectedCategoryId, setSelectedCategoryId] = useState(null) - const [refreshing, setRefreshing] = useState(false) const { load: loadCategories, data: categoriesData, loading: categoriesLoading, error: categoriesError } = useCategories() const { load, data: activatesData, error } = useActivates() @@ -94,12 +92,6 @@ export default function HomeScreen() { loadCategories() }, []) - const handleRefresh = async () => { - setRefreshing(true) - await Promise.all([load(), loadCategories()]) - setRefreshing(false) - } - useEffect(() => { // 当分类数据加载完成后,默认选中第一个分类 if (categoriesData?.categories && categoriesData.categories.length > 0 && !selectedCategoryId) { @@ -144,14 +136,6 @@ export default function HomeScreen() { // 过滤掉视频类型的模板,只显示图片 const previewUrl = (template as any).webpPreviewUrl || template.previewUrl || template.coverImageUrl || `` const isVideo = previewUrl.includes('.mp4') || previewUrl.includes('.mov') || previewUrl.includes('.webm') - console.log(`[HOME PAGE] Filtering ${template.title}:`, { - webpPreviewUrl: (template as any).webpPreviewUrl, - previewUrl: template.previewUrl, - coverImageUrl: template.coverImageUrl, - finalPreviewUrl: previewUrl, - isVideo, - shouldKeep: !isVideo - }) return !isVideo }) : [] @@ -297,9 +281,6 @@ export default function HomeScreen() { style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} - refreshControl={ - - } onScroll={(event) => { const scrollY = event.nativeEvent.contentOffset.y if (scrollY >= tabsPositionRef.current) { @@ -488,6 +469,7 @@ const styles = StyleSheet.create({ backgroundColor: '#090A0B', }, scrollContent: { + flexGrow: 1, backgroundColor: '#090A0B', }, heroSection: { @@ -615,10 +597,8 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, gridContainer: { - flexDirection: 'row', - flexWrap: 'wrap', + flex: 1, paddingHorizontal: 8, - justifyContent: 'space-between', }, card: { marginBottom: 12, diff --git a/app/(tabs)/message.tsx b/app/(tabs)/message.tsx index d9fc17a..5f6c046 100644 --- a/app/(tabs)/message.tsx +++ b/app/(tabs)/message.tsx @@ -6,7 +6,6 @@ import { ScrollView, StatusBar as RNStatusBar, Pressable, - RefreshControl, NativeScrollEvent, NativeSyntheticEvent, } from 'react-native' @@ -276,13 +275,6 @@ export default function MessageScreen() { showsVerticalScrollIndicator={false} onScroll={handleScroll} scrollEventThrottle={400} - refreshControl={ - 0} - onRefresh={refetch} - tintColor="#FFFFFF" - /> - } > {messages.length > 0 ? ( <> diff --git a/app/(tabs)/my.tsx b/app/(tabs)/my.tsx index f0410c8..fc1a6b0 100644 --- a/app/(tabs)/my.tsx +++ b/app/(tabs)/my.tsx @@ -7,7 +7,6 @@ import { Dimensions, Pressable, StatusBar as RNStatusBar, - RefreshControl, } from 'react-native' import { StatusBar } from 'expo-status-bar' import { SafeAreaView } from 'react-native-safe-area-context' @@ -38,7 +37,6 @@ export default function My() { const { t, i18n } = useTranslation() const [editDrawerVisible, setEditDrawerVisible] = useState(false) const [profileName, setProfileName] = useState('乔乔乔乔') - const [refreshing, setRefreshing] = useState(false) // 使用 useTemplateGenerations hook 获取用户作品列表 const { @@ -54,13 +52,6 @@ export default function My() { loadGenerations({ page: 1, limit: 50 }) }, []) - // 处理下拉刷新 - const handleRefresh = useCallback(async () => { - setRefreshing(true) - await refetch({ page: 1, limit: 50 }) - setRefreshing(false) - }, [refetch]) - // 处理设置菜单选择 const handleSettingsSelect = async (value: string) => { if (value === 'changePassword') { @@ -179,21 +170,13 @@ export default function My() { {/* 作品九宫格 */} - {loading && !refreshing ? ( + {loading ? ( ) : ( - } > {generations.map((item, index) => ( diff --git a/app/(tabs)/video.tsx b/app/(tabs)/video.tsx index 14f30ea..b2e6ef3 100644 --- a/app/(tabs)/video.tsx +++ b/app/(tabs)/video.tsx @@ -19,7 +19,6 @@ import { useRouter } from 'expo-router' import { useTemplates, type TemplateDetail } from '@/hooks' import LoadingState from '@/components/LoadingState' import ErrorState from '@/components/ErrorState' -import RefreshControl from '@/components/RefreshControl' import PaginationLoader from '@/components/PaginationLoader' const { width: screenWidth, height: screenHeight } = Dimensions.get('window') @@ -124,16 +123,13 @@ export default function VideoScreen() { page: 1, limit: PAGE_SIZE, }) - const [refreshing, setRefreshing] = useState(false) useEffect(() => { execute() }, [execute]) const handleRefresh = useCallback(async () => { - setRefreshing(true) await refetch() - setRefreshing(false) }, [refetch]) const handleLoadMore = useCallback(() => { @@ -220,12 +216,6 @@ export default function VideoScreen() { snapToInterval={videoHeight} snapToAlignment="start" decelerationRate="fast" - refreshControl={ - - } onEndReached={handleLoadMore} onEndReachedThreshold={0.1} ListFooterComponent={loading ? : null} diff --git a/app/generationRecord.tsx b/app/generationRecord.tsx index 75ef02d..e00ca03 100644 --- a/app/generationRecord.tsx +++ b/app/generationRecord.tsx @@ -19,7 +19,6 @@ import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog' import LoadingState from '@/components/LoadingState' import ErrorState from '@/components/ErrorState' import PaginationLoader from '@/components/PaginationLoader' -import RefreshControl from '@/components/RefreshControl' import { useTemplateGenerations, type TemplateGeneration } from '@/hooks' const { width: screenWidth } = Dimensions.get('window') @@ -154,7 +153,6 @@ export default function GenerationRecordScreen() { keyExtractor={(item) => item.id} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} - refreshControl={} onEndReached={handleLoadMore} onEndReachedThreshold={0.5} ListFooterComponent={loadingMore ? : null} diff --git a/app/searchResults.tsx b/app/searchResults.tsx index 262294f..6933da7 100644 --- a/app/searchResults.tsx +++ b/app/searchResults.tsx @@ -11,7 +11,6 @@ import { useRouter, useLocalSearchParams } from 'expo-router' import SearchResultsGrid from '@/components/SearchResultsGrid' import SearchBar from '@/components/SearchBar' -import RefreshControl from '@/components/RefreshControl' import LoadingState from '@/components/LoadingState' import ErrorState from '@/components/ErrorState' import PaginationLoader from '@/components/PaginationLoader' @@ -21,7 +20,6 @@ export default function SearchResultsScreen() { const router = useRouter() const params = useLocalSearchParams() const [searchText, setSearchText] = useState((params.q as string) || '') - const [refreshing, setRefreshing] = useState(false) const { templates, loading, loadingMore, error, execute, refetch, loadMore, hasMore } = useTemplates() const { addToHistory } = useSearchHistory() @@ -38,11 +36,9 @@ export default function SearchResultsScreen() { }, [params.q, execute, addToHistory]) const handleRefresh = async () => { - setRefreshing(true) if (searchText) { await refetch() } - setRefreshing(false) } const handleLoadMore = () => { @@ -164,7 +160,6 @@ export default function SearchResultsScreen() { } onEndReached={handleLoadMore} ListFooterComponent={loadingMore ? : null} /> diff --git a/app/worksList.tsx b/app/worksList.tsx index f3d926a..c422b4c 100644 --- a/app/worksList.tsx +++ b/app/worksList.tsx @@ -13,7 +13,6 @@ import { useTranslation } from 'react-i18next' import { LeftArrowIcon, SearchIcon } from '@/components/icon' import WorksGallery, { type Category, type WorkItem } from '@/components/WorksGallery' -import RefreshControl from '@/components/RefreshControl' import LoadingState from '@/components/LoadingState' import ErrorState from '@/components/ErrorState' import PaginationLoader from '@/components/PaginationLoader' @@ -95,9 +94,6 @@ export default function WorksListScreen() { params: { id: id.toString() }, }) }} - refreshControl={ - - } onEndReached={loadMore} ListFooterComponent={ hasMore ? : undefined diff --git a/components/RefreshControl.test.tsx b/components/RefreshControl.test.tsx deleted file mode 100644 index f803f62..0000000 --- a/components/RefreshControl.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react' -import { render } from '@testing-library/react-native' -import { ScrollView } from 'react-native' -import RefreshControl from './RefreshControl' - -describe('RefreshControl Component', () => { - describe('Basic Rendering', () => { - it('should render with refreshing false', () => { - const mockOnRefresh = jest.fn() - const { UNSAFE_root } = render( - - } - > - <> - - ) - expect(UNSAFE_root).toBeTruthy() - }) - - it('should render with refreshing true', () => { - const mockOnRefresh = jest.fn() - const { UNSAFE_root } = render( - - } - > - <> - - ) - expect(UNSAFE_root).toBeTruthy() - }) - }) - - describe('Props', () => { - it('should accept refreshing prop', () => { - const mockOnRefresh = jest.fn() - const { UNSAFE_root } = render( - - } - > - <> - - ) - expect(UNSAFE_root).toBeTruthy() - }) - - it('should accept onRefresh callback prop', () => { - const mockOnRefresh = jest.fn() - const { UNSAFE_root } = render( - - } - > - <> - - ) - expect(UNSAFE_root).toBeTruthy() - }) - }) -}) diff --git a/components/RefreshControl.tsx b/components/RefreshControl.tsx deleted file mode 100644 index 113883a..0000000 --- a/components/RefreshControl.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import { RefreshControl as RNRefreshControl } from 'react-native' - -interface RefreshControlProps { - refreshing: boolean - onRefresh: () => void -} - -export default function RefreshControl({ - refreshing, - onRefresh, -}: RefreshControlProps) { - return ( - - ) -} diff --git a/components/SearchResultsGrid.tsx b/components/SearchResultsGrid.tsx index 0ada988..026bbe6 100644 --- a/components/SearchResultsGrid.tsx +++ b/components/SearchResultsGrid.tsx @@ -30,7 +30,6 @@ interface TemplateSearchResultItem { interface SearchResultsGridProps { results: TemplateSearchResultItem[] loading?: boolean - refreshControl?: React.ReactElement onEndReached?: () => void ListFooterComponent?: React.ReactElement | null } @@ -44,7 +43,7 @@ const calculateCardHeight = (width: number, aspectRatio?: number): number => { return width * 1.2 } -export default function SearchResultsGrid({ results, loading, refreshControl, onEndReached, ListFooterComponent }: SearchResultsGridProps) { +export default function SearchResultsGrid({ results, loading, onEndReached, ListFooterComponent }: SearchResultsGridProps) { const { t } = useTranslation() const router = useRouter() const [gridWidth, setGridWidth] = useState(screenWidth) @@ -88,7 +87,6 @@ export default function SearchResultsGrid({ results, loading, refreshControl, on diff --git a/components/WorksGallery.tsx b/components/WorksGallery.tsx index cc5b72b..22f569e 100644 --- a/components/WorksGallery.tsx +++ b/components/WorksGallery.tsx @@ -32,7 +32,6 @@ interface WorksGalleryProps { onCategoryChange: (category: Category) => void groupedWorks: Record onWorkPress: (id: number) => void - refreshControl?: React.ReactElement onEndReached?: () => void ListFooterComponent?: React.ReactElement } @@ -43,7 +42,6 @@ export default function WorksGallery({ onCategoryChange, groupedWorks, onWorkPress, - refreshControl, onEndReached, ListFooterComponent, }: WorksGalleryProps) { @@ -118,7 +116,6 @@ export default function WorksGallery({ style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} - refreshControl={refreshControl} onScroll={(e) => { const { layoutMeasurement, contentOffset, contentSize } = e.nativeEvent const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 100 diff --git a/jest.setup.js b/jest.setup.js index 756edf7..777f3f3 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,3 +1,42 @@ +// Mock react-native BEFORE any other imports to avoid flow type issues +jest.mock('react-native', () => { + const React = require('react') + return { + ...jest.requireActual('react-native'), + RefreshControl: 'RefreshControl', + ScrollView: 'ScrollView', + FlatList: 'FlatList', + View: 'View', + Text: 'Text', + Image: 'Image', + Pressable: 'Pressable', + Platform: { OS: 'web' }, + Animated: { + View: 'Animated.View', + Text: 'Animated.Text', + Image: 'Animated.Image', + Value: class MockValue { + constructor(v) { this._value = v } + setValue(v) { this._value = v } + __getValue() { return this._value } + interpolate() { return this } + }, + timing: () => ({ start: jest.fn() }), + spring: () => ({ start: jest.fn() }), + event: () => jest.fn(), + }, + PanResponder: { + create: () => ({ panHandlers: {} }), + }, + ActivityIndicator: 'ActivityIndicator', + StyleSheet: { create: (styles) => styles }, + Dimensions: { get: () => ({ width: 375, height: 812 }) }, + StatusBar: 'StatusBar', + SafeAreaView: 'SafeAreaView', + useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), + } +}) + require('@testing-library/jest-native/extend-expect') // Initialize globals for react-native-reanimated @@ -138,6 +177,16 @@ jest.mock('@repo/sdk', () => ({ CategoryController: class MockCategoryController {}, })) +// Mock react-native to avoid flow type issues +jest.mock('react-native', () => { + const RN = jest.requireActual('react-native') + return { + ...RN, + // Ensure RefreshControl is properly mocked for tests + RefreshControl: 'RefreshControl', + } +}) + // Mock lib/auth to avoid TypeScript compilation errors jest.mock('@/lib/auth', () => ({ OWNER_ID: 'test-owner-id', diff --git a/message-adaptation-plan.md b/message-adaptation-plan.md index 203e024..aefa5b7 100644 --- a/message-adaptation-plan.md +++ b/message-adaptation-plan.md @@ -345,4 +345,24 @@ type UnreadAnnouncementCountResult = { | 错误 | 尝试 | 解决方案 | |------|------|----------| -| 无 | - | 所有阶段都顺利完成,没有遇到阻塞性错误 | +| API验证错误:type参数不接受数组 | 发送type: ["SYSTEM", "ACTIVITY"] | 修改为单个type值,使用客户端过滤实现Tab功能 | + +### 错误详情 + +**错误信息**: +```json +{ + "code": "VALIDATION_ERROR", + "message": "[body.type] Invalid option: expected one of \"SYSTEM\"|\"ACTIVITY\"|\"BILLING\"|\"MARKETING\"" +} +``` + +**原因**: SDK的MessageController.list() API只接受单个MessageType值,不支持数组形式的type参数。 + +**解决方案**: +1. 修改hooks/use-messages.ts中的type参数类型从数组改为单个值 +2. 在app/(tabs)/message.tsx中使用客户端过滤(filterMessagesByTab函数) +3. 获取所有消息后,根据activeTab在客户端过滤显示 +4. 移除useEffect对activeTab的依赖,避免Tab切换时重新请求API + +**提交**: fix: change type parameter from array to single value for API compatibility diff --git a/package.json b/package.json index ac59406..89bbf46 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,6 @@ "test:coverage": "./node_modules/.bin/jest --coverage" }, "dependencies": { - "@repo/core": "1.0.3", - "@repo/sdk": "1.0.9", "@better-auth/expo": "1.3.34", "@expo/vector-icons": "^15.0.3", "@gorhom/bottom-sheet": "^5.2.8", @@ -29,6 +27,8 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", + "@repo/core": "1.0.3", + "@repo/sdk": "1.0.9", "@shopify/flash-list": "^2.0.0", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.5.3",