feat: integrate social buttons in video page
- Add VideoSocialButton to VideoItem component - Connect like/favorite actions with existing hooks - Use Zustand store for state management - Extend templateSocialStore to support favoriteCount - Update tests for social button integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,106 @@ jest.mock('@/components/LoadingState', () => 'LoadingState')
|
|||||||
jest.mock('@/components/ErrorState', () => 'ErrorState')
|
jest.mock('@/components/ErrorState', () => 'ErrorState')
|
||||||
jest.mock('@/components/PaginationLoader', () => 'PaginationLoader')
|
jest.mock('@/components/PaginationLoader', () => 'PaginationLoader')
|
||||||
|
|
||||||
|
// Mock VideoSocialButton
|
||||||
|
jest.mock('@/components/blocks/ui/VideoSocialButton', () => ({
|
||||||
|
VideoSocialButton: ({ testID, liked, favorited, likeCount, favoriteCount }: any) => {
|
||||||
|
const React = require('react')
|
||||||
|
const { View, Text } = require('react-native')
|
||||||
|
return React.createElement(
|
||||||
|
View,
|
||||||
|
{ testID },
|
||||||
|
liked !== undefined && React.createElement(Text, { testID: `${testID}-like-button` }, liked ? 'liked' : 'not liked'),
|
||||||
|
favorited !== undefined && React.createElement(Text, { testID: `${testID}-favorite-button` }, favorited ? 'favorited' : 'not favorited'),
|
||||||
|
likeCount !== undefined && React.createElement(Text, {}, likeCount.toString()),
|
||||||
|
favoriteCount !== undefined && React.createElement(Text, {}, favoriteCount.toString())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock LikeButton and FavoriteButton
|
||||||
|
jest.mock('@/components/blocks/ui/LikeButton', () => ({
|
||||||
|
LikeButton: ({ testID, count }: any) => {
|
||||||
|
const React = require('react')
|
||||||
|
const { Text } = require('react-native')
|
||||||
|
return React.createElement(Text, { testID }, count !== undefined ? count.toString() : 'LikeButton')
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/components/blocks/ui/FavoriteButton', () => ({
|
||||||
|
FavoriteButton: ({ testID, count }: any) => {
|
||||||
|
const React = require('react')
|
||||||
|
const { Text } = require('react-native')
|
||||||
|
return React.createElement(Text, { testID }, count !== undefined ? count.toString() : 'FavoriteButton')
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock hooks
|
||||||
|
jest.mock('@/hooks/use-template-like', () => ({
|
||||||
|
useTemplateLike: jest.fn(() => ({
|
||||||
|
liked: false,
|
||||||
|
loading: false,
|
||||||
|
like: jest.fn(),
|
||||||
|
unlike: jest.fn(),
|
||||||
|
checkLiked: jest.fn(),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/hooks/use-template-favorite', () => ({
|
||||||
|
useTemplateFavorite: jest.fn(() => ({
|
||||||
|
favorited: false,
|
||||||
|
loading: false,
|
||||||
|
favorite: jest.fn(),
|
||||||
|
unfavorite: jest.fn(),
|
||||||
|
checkFavorited: jest.fn(),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/stores/templateSocialStore', () => {
|
||||||
|
const actualStore = jest.requireActual('@/stores/templateSocialStore')
|
||||||
|
return {
|
||||||
|
...actualStore,
|
||||||
|
useTemplateSocialStore: jest.fn((selector) => {
|
||||||
|
const state = {
|
||||||
|
likedMap: {},
|
||||||
|
favoritedMap: {},
|
||||||
|
likeCountMap: {},
|
||||||
|
favoriteCountMap: {},
|
||||||
|
setLikedStates: jest.fn(),
|
||||||
|
setFavoritedStates: jest.fn(),
|
||||||
|
setLikeCountStates: jest.fn(),
|
||||||
|
setFavoriteCountStates: jest.fn(),
|
||||||
|
setLiked: jest.fn(),
|
||||||
|
setFavorited: jest.fn(),
|
||||||
|
setLikeCount: jest.fn(),
|
||||||
|
setFavoriteCount: jest.fn(),
|
||||||
|
incrementLikeCount: jest.fn(),
|
||||||
|
decrementLikeCount: jest.fn(),
|
||||||
|
getLiked: jest.fn(),
|
||||||
|
getFavorited: jest.fn(),
|
||||||
|
getLikeCount: jest.fn(),
|
||||||
|
getFavoriteCount: jest.fn(),
|
||||||
|
isLiked: jest.fn(() => false),
|
||||||
|
isFavorited: jest.fn(() => false),
|
||||||
|
clear: jest.fn(),
|
||||||
|
}
|
||||||
|
if (typeof selector === 'function') {
|
||||||
|
return selector(state)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}),
|
||||||
|
templateSocialStore: {
|
||||||
|
useLiked: jest.fn(() => false),
|
||||||
|
useFavorited: jest.fn(() => false),
|
||||||
|
useLikeCount: jest.fn(() => undefined),
|
||||||
|
useFavoriteCount: jest.fn(() => undefined),
|
||||||
|
},
|
||||||
|
useTemplateLiked: jest.fn(() => false),
|
||||||
|
useTemplateFavorited: jest.fn(() => false),
|
||||||
|
useTemplateLikeCount: jest.fn(() => undefined),
|
||||||
|
useTemplateFavoriteCount: jest.fn(() => undefined),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Mock react-native RefreshControl (directly from react-native)
|
// Mock react-native RefreshControl (directly from react-native)
|
||||||
jest.mock('react-native', () =>
|
jest.mock('react-native', () =>
|
||||||
Object.assign({}, jest.requireActual('react-native'), {
|
Object.assign({}, jest.requireActual('react-native'), {
|
||||||
@@ -194,6 +294,61 @@ describe('Video Screen', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('VideoItem 社交按钮集成', () => {
|
||||||
|
const mockItem = createMockTemplateDetail() as TemplateDetail
|
||||||
|
const mockVideoHeight = 600
|
||||||
|
|
||||||
|
it('应该渲染社交按钮', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(getByTestId('video-social-button')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该显示点赞数量', () => {
|
||||||
|
const itemWithLikes = createMockTemplateDetail({
|
||||||
|
likeCount: 150,
|
||||||
|
}) as TemplateDetail
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<VideoItem item={itemWithLikes} videoHeight={mockVideoHeight} />
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(getByText('150')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该显示收藏数量', () => {
|
||||||
|
const itemWithFavorites = createMockTemplateDetail({
|
||||||
|
favoriteCount: 80,
|
||||||
|
}) as TemplateDetail
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<VideoItem item={itemWithFavorites} videoHeight={mockVideoHeight} />
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(getByText('80')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该有点赞交互功能', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
||||||
|
)
|
||||||
|
|
||||||
|
const likeButton = getByTestId('video-social-button-like-button')
|
||||||
|
expect(likeButton).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该有收藏交互功能', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<VideoItem item={mockItem} videoHeight={mockVideoHeight} />
|
||||||
|
)
|
||||||
|
|
||||||
|
const favoriteButton = getByTestId('video-social-button-favorite-button')
|
||||||
|
expect(favoriteButton).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('VideoScreen Component', () => {
|
describe('VideoScreen Component', () => {
|
||||||
it('should show loading state when templates are loading', () => {
|
it('should show loading state when templates are loading', () => {
|
||||||
mockUseTemplates.mockReturnValue({
|
mockUseTemplates.mockReturnValue({
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { SameStyleIcon, WhiteStarIcon } from '@/components/icon'
|
import { SameStyleIcon, WhiteStarIcon } from '@/components/icon'
|
||||||
import { useRouter } from 'expo-router'
|
import { useRouter } from 'expo-router'
|
||||||
import { useTemplates, type TemplateDetail } from '@/hooks'
|
import { useTemplates, type TemplateDetail } from '@/hooks'
|
||||||
|
import { VideoSocialButton } from '@/components/blocks/ui/VideoSocialButton'
|
||||||
|
import { useTemplateLike } from '@/hooks/use-template-like'
|
||||||
|
import { useTemplateFavorite } from '@/hooks/use-template-favorite'
|
||||||
|
import {
|
||||||
|
useTemplateSocialStore,
|
||||||
|
useTemplateLiked,
|
||||||
|
useTemplateFavorited,
|
||||||
|
useTemplateLikeCount,
|
||||||
|
useTemplateFavoriteCount,
|
||||||
|
} from '@/stores/templateSocialStore'
|
||||||
import LoadingState from '@/components/LoadingState'
|
import LoadingState from '@/components/LoadingState'
|
||||||
import ErrorState from '@/components/ErrorState'
|
import ErrorState from '@/components/ErrorState'
|
||||||
import PaginationLoader from '@/components/PaginationLoader'
|
import PaginationLoader from '@/components/PaginationLoader'
|
||||||
@@ -67,6 +77,16 @@ export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; vi
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null)
|
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null)
|
||||||
|
|
||||||
|
// 从 store 获取状态
|
||||||
|
const liked = useTemplateLiked(item.id) ?? false
|
||||||
|
const favorited = useTemplateFavorited(item.id) ?? false
|
||||||
|
const likeCount = useTemplateLikeCount(item.id) ?? item.likeCount
|
||||||
|
const favoriteCount = useTemplateFavoriteCount(item.id) ?? item.favoriteCount
|
||||||
|
|
||||||
|
// 使用 hooks 获取操作函数
|
||||||
|
const { like: onLike, unlike: onUnlike, loading: likeLoading } = useTemplateLike(item.id)
|
||||||
|
const { favorite: onFavorite, unfavorite: onUnfavorite, loading: favoriteLoading } = useTemplateFavorite(item.id)
|
||||||
|
|
||||||
const handleImageLoad = useCallback((event: ImageLoadEventData) => {
|
const handleImageLoad = useCallback((event: ImageLoadEventData) => {
|
||||||
const { source } = event
|
const { source } = event
|
||||||
if (source?.width && source?.height) {
|
if (source?.width && source?.height) {
|
||||||
@@ -109,6 +129,20 @@ export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; vi
|
|||||||
<SameStyleIcon />
|
<SameStyleIcon />
|
||||||
<Text style={styles.actionButtonText}>{t('video.makeSame')}</Text>
|
<Text style={styles.actionButtonText}>{t('video.makeSame')}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{/* 社交按钮 */}
|
||||||
|
<VideoSocialButton
|
||||||
|
templateId={item.id}
|
||||||
|
liked={liked}
|
||||||
|
favorited={favorited}
|
||||||
|
likeCount={likeCount}
|
||||||
|
favoriteCount={favoriteCount}
|
||||||
|
loading={likeLoading || favoriteLoading}
|
||||||
|
onLike={onLike}
|
||||||
|
onUnlike={onUnlike}
|
||||||
|
onFavorite={onFavorite}
|
||||||
|
onUnfavorite={onUnfavorite}
|
||||||
|
testID="video-social-button"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ interface TemplateSocialState {
|
|||||||
// 点赞数量缓存: templateId -> number
|
// 点赞数量缓存: templateId -> number
|
||||||
likeCountMap: Record<string, number>
|
likeCountMap: Record<string, number>
|
||||||
|
|
||||||
|
// 收藏数量缓存: templateId -> number
|
||||||
|
favoriteCountMap: Record<string, number>
|
||||||
|
|
||||||
// 批量更新点赞状态
|
// 批量更新点赞状态
|
||||||
setLikedStates: (states: Record<string, boolean>) => void
|
setLikedStates: (states: Record<string, boolean>) => void
|
||||||
|
|
||||||
@@ -20,6 +23,9 @@ interface TemplateSocialState {
|
|||||||
// 批量更新点赞数量
|
// 批量更新点赞数量
|
||||||
setLikeCountStates: (states: Record<string, number>) => void
|
setLikeCountStates: (states: Record<string, number>) => void
|
||||||
|
|
||||||
|
// 批量更新收藏数量
|
||||||
|
setFavoriteCountStates: (states: Record<string, number>) => void
|
||||||
|
|
||||||
// 更新单个点赞状态
|
// 更新单个点赞状态
|
||||||
setLiked: (templateId: string, liked: boolean) => void
|
setLiked: (templateId: string, liked: boolean) => void
|
||||||
|
|
||||||
@@ -29,6 +35,9 @@ interface TemplateSocialState {
|
|||||||
// 更新单个点赞数量
|
// 更新单个点赞数量
|
||||||
setLikeCount: (templateId: string, count: number) => void
|
setLikeCount: (templateId: string, count: number) => void
|
||||||
|
|
||||||
|
// 更新单个收藏数量
|
||||||
|
setFavoriteCount: (templateId: string, count: number) => void
|
||||||
|
|
||||||
// 增加点赞数量(乐观更新)
|
// 增加点赞数量(乐观更新)
|
||||||
incrementLikeCount: (templateId: string) => void
|
incrementLikeCount: (templateId: string) => void
|
||||||
|
|
||||||
@@ -44,6 +53,9 @@ interface TemplateSocialState {
|
|||||||
// 获取点赞数量(如果不存在返回 undefined)
|
// 获取点赞数量(如果不存在返回 undefined)
|
||||||
getLikeCount: (templateId: string) => number | undefined
|
getLikeCount: (templateId: string) => number | undefined
|
||||||
|
|
||||||
|
// 获取收藏数量(如果不存在返回 undefined)
|
||||||
|
getFavoriteCount: (templateId: string) => number | undefined
|
||||||
|
|
||||||
// 获取点赞状态(如果不存在返回 false)
|
// 获取点赞状态(如果不存在返回 false)
|
||||||
isLiked: (templateId: string) => boolean
|
isLiked: (templateId: string) => boolean
|
||||||
|
|
||||||
@@ -61,6 +73,7 @@ export const useTemplateSocialStore = create<TemplateSocialState>()(
|
|||||||
likedMap: {},
|
likedMap: {},
|
||||||
favoritedMap: {},
|
favoritedMap: {},
|
||||||
likeCountMap: {},
|
likeCountMap: {},
|
||||||
|
favoriteCountMap: {},
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
setLikedStates: (states) => {
|
setLikedStates: (states) => {
|
||||||
@@ -81,6 +94,12 @@ export const useTemplateSocialStore = create<TemplateSocialState>()(
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFavoriteCountStates: (states) => {
|
||||||
|
set((state) => ({
|
||||||
|
favoriteCountMap: { ...state.favoriteCountMap, ...states },
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
setLiked: (templateId, liked) => {
|
setLiked: (templateId, liked) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
likedMap: { ...state.likedMap, [templateId]: liked },
|
likedMap: { ...state.likedMap, [templateId]: liked },
|
||||||
@@ -99,6 +118,12 @@ export const useTemplateSocialStore = create<TemplateSocialState>()(
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFavoriteCount: (templateId, count) => {
|
||||||
|
set((state) => ({
|
||||||
|
favoriteCountMap: { ...state.favoriteCountMap, [templateId]: count },
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
incrementLikeCount: (templateId) => {
|
incrementLikeCount: (templateId) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
likeCountMap: {
|
likeCountMap: {
|
||||||
@@ -132,6 +157,10 @@ export const useTemplateSocialStore = create<TemplateSocialState>()(
|
|||||||
return get().likeCountMap[templateId]
|
return get().likeCountMap[templateId]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFavoriteCount: (templateId) => {
|
||||||
|
return get().favoriteCountMap[templateId]
|
||||||
|
},
|
||||||
|
|
||||||
isLiked: (templateId) => {
|
isLiked: (templateId) => {
|
||||||
return get().likedMap[templateId] || false
|
return get().likedMap[templateId] || false
|
||||||
},
|
},
|
||||||
@@ -145,6 +174,7 @@ export const useTemplateSocialStore = create<TemplateSocialState>()(
|
|||||||
likedMap: {},
|
likedMap: {},
|
||||||
favoritedMap: {},
|
favoritedMap: {},
|
||||||
likeCountMap: {},
|
likeCountMap: {},
|
||||||
|
favoriteCountMap: {},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -171,3 +201,12 @@ export function useTemplateLikeCount(templateId: string | undefined) {
|
|||||||
templateId ? state.likeCountMap[templateId] : undefined
|
templateId ? state.likeCountMap[templateId] : undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTemplateFavoriteCount(templateId: string | undefined) {
|
||||||
|
return useTemplateSocialStore((state) =>
|
||||||
|
templateId ? state.favoriteCountMap[templateId] : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为了方便使用,导出 templateSocialStore 别名
|
||||||
|
export const templateSocialStore = useTemplateSocialStore
|
||||||
|
|||||||
Reference in New Issue
Block a user