This commit is contained in:
imeepos
2026-01-21 16:25:26 +08:00
parent 54867492a7
commit 4c01d8e9e7
14 changed files with 76 additions and 165 deletions

View File

@@ -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,

View File

@@ -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 ? (
<>

View File

@@ -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) => (

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
/>

View File

@@ -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

View File

@@ -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()
})
})
})

View File

@@ -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']}
/>
)
}

View File

@@ -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}
>

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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",