fix: bug
This commit is contained in:
@@ -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<string | null>(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={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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={
|
||||
<RefreshControl
|
||||
refreshing={loading && messages.length > 0}
|
||||
onRefresh={refetch}
|
||||
tintColor="#FFFFFF"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{messages.length > 0 ? (
|
||||
<>
|
||||
|
||||
@@ -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() {
|
||||
</View>
|
||||
|
||||
{/* 作品九宫格 */}
|
||||
{loading && !refreshing ? (
|
||||
{loading ? (
|
||||
<MySkeleton />
|
||||
) : (
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#FFFFFF"
|
||||
colors={['#FFFFFF']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View style={styles.galleryGrid}>
|
||||
{generations.map((item, index) => (
|
||||
|
||||
@@ -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={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={handleLoadMore}
|
||||
onEndReachedThreshold={0.1}
|
||||
ListFooterComponent={loading ? <PaginationLoader color="#FFE500" /> : null}
|
||||
|
||||
@@ -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={<RefreshControl refreshing={loading} onRefresh={handleRefresh} />}
|
||||
onEndReached={handleLoadMore}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListFooterComponent={loadingMore ? <PaginationLoader testID="pagination-loader" /> : null}
|
||||
|
||||
@@ -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() {
|
||||
<SearchResultsGrid
|
||||
results={searchResults}
|
||||
loading={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
|
||||
onEndReached={handleLoadMore}
|
||||
ListFooterComponent={loadingMore ? <PaginationLoader /> : null}
|
||||
/>
|
||||
|
||||
@@ -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={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={refresh} />
|
||||
}
|
||||
onEndReached={loadMore}
|
||||
ListFooterComponent={
|
||||
hasMore ? <PaginationLoader /> : undefined
|
||||
|
||||
@@ -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(
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={mockOnRefresh} />
|
||||
}
|
||||
>
|
||||
<></>
|
||||
</ScrollView>
|
||||
)
|
||||
expect(UNSAFE_root).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should render with refreshing true', () => {
|
||||
const mockOnRefresh = jest.fn()
|
||||
const { UNSAFE_root } = render(
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={true} onRefresh={mockOnRefresh} />
|
||||
}
|
||||
>
|
||||
<></>
|
||||
</ScrollView>
|
||||
)
|
||||
expect(UNSAFE_root).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should accept refreshing prop', () => {
|
||||
const mockOnRefresh = jest.fn()
|
||||
const { UNSAFE_root } = render(
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={true} onRefresh={mockOnRefresh} />
|
||||
}
|
||||
>
|
||||
<></>
|
||||
</ScrollView>
|
||||
)
|
||||
expect(UNSAFE_root).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should accept onRefresh callback prop', () => {
|
||||
const mockOnRefresh = jest.fn()
|
||||
const { UNSAFE_root } = render(
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={mockOnRefresh} />
|
||||
}
|
||||
>
|
||||
<></>
|
||||
</ScrollView>
|
||||
)
|
||||
expect(UNSAFE_root).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 (
|
||||
<RNRefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor="#F5F5F5"
|
||||
colors={['#F5F5F5']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={refreshControl}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={400}
|
||||
>
|
||||
|
||||
@@ -32,7 +32,6 @@ interface WorksGalleryProps {
|
||||
onCategoryChange: (category: Category) => void
|
||||
groupedWorks: Record<string, WorkItem[]>
|
||||
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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user