InfiniteVideoGrid.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import React, { useEffect, useState } from 'react'
  2. import styled from '@emotion/styled'
  3. import { debounce } from 'lodash'
  4. import { useLazyQuery } from '@apollo/client'
  5. import { sizes, typography } from '../../theme'
  6. import { VideoPreviewBase } from '../VideoPreview'
  7. import Grid from '../Grid'
  8. import VideoPreview from '@/components/VideoPreviewWithNavigation'
  9. import { GET_NEWEST_VIDEOS } from '@/api/queries'
  10. import { GetNewestVideos, GetNewestVideosVariables } from '@/api/queries/__generated__/GetNewestVideos'
  11. type InfiniteVideoGridProps = {
  12. title?: string
  13. categoryId?: string
  14. skipCount?: number
  15. ready?: boolean
  16. className?: string
  17. }
  18. const INITIAL_ROWS = 4
  19. const INITIAL_VIDEOS_PER_ROW = 4
  20. const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
  21. title,
  22. categoryId = '',
  23. skipCount = 0,
  24. ready = true,
  25. className,
  26. }) => {
  27. const [videosPerRow, setVideosPerRow] = useState(INITIAL_VIDEOS_PER_ROW)
  28. const [targetRowsCountByCategory, setTargetRowsCountByCategory] = useState<Record<string, number>>({
  29. [categoryId]: INITIAL_ROWS,
  30. })
  31. const [cachedCategoryId, setCachedCategoryId] = useState<string>(categoryId)
  32. const targetRowsCount = targetRowsCountByCategory[cachedCategoryId]
  33. const targetDisplayedVideosCount = targetRowsCount * videosPerRow
  34. const targetLoadedVideosCount = targetDisplayedVideosCount + skipCount
  35. const [fetchVideos, { loading, data, error, fetchMore, called, refetch }] = useLazyQuery<
  36. GetNewestVideos,
  37. GetNewestVideosVariables
  38. >(GET_NEWEST_VIDEOS, {
  39. notifyOnNetworkStatusChange: true,
  40. })
  41. if (error) {
  42. throw error
  43. }
  44. const loadedVideosCount = data?.videosConnection.edges.length || 0
  45. const allVideosLoaded = data ? !data.videosConnection.pageInfo.hasNextPage : false
  46. const endCursor = data?.videosConnection.pageInfo.endCursor
  47. useEffect(() => {
  48. if (ready && !called) {
  49. fetchVideos({ variables: { first: targetLoadedVideosCount, categoryId } })
  50. }
  51. }, [ready, called, categoryId, targetLoadedVideosCount, fetchVideos])
  52. useEffect(() => {
  53. if (categoryId === cachedCategoryId) {
  54. return
  55. }
  56. setCachedCategoryId(categoryId)
  57. const categoryRowsSet = !!targetRowsCountByCategory[categoryId]
  58. const categoryRowsCount = categoryRowsSet ? targetRowsCountByCategory[categoryId] : INITIAL_ROWS
  59. if (!categoryRowsSet) {
  60. setTargetRowsCountByCategory((prevState) => ({
  61. ...prevState,
  62. [categoryId]: categoryRowsCount,
  63. }))
  64. }
  65. if (!called || !refetch) {
  66. return
  67. }
  68. refetch({ first: categoryRowsCount * videosPerRow + skipCount, categoryId })
  69. }, [categoryId, cachedCategoryId, targetRowsCountByCategory, called, refetch, videosPerRow, skipCount])
  70. useEffect(() => {
  71. if (loading || !fetchMore || allVideosLoaded) {
  72. return
  73. }
  74. if (targetLoadedVideosCount > loadedVideosCount) {
  75. const videosToLoadCount = targetLoadedVideosCount - loadedVideosCount
  76. fetchMore({ variables: { first: videosToLoadCount, after: endCursor, categoryId } })
  77. }
  78. }, [loading, loadedVideosCount, targetLoadedVideosCount, allVideosLoaded, fetchMore, endCursor, categoryId])
  79. useEffect(() => {
  80. const scrollHandler = debounce(() => {
  81. const scrolledToBottom =
  82. window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight
  83. if (scrolledToBottom && ready && !loading && !allVideosLoaded) {
  84. setTargetRowsCountByCategory((prevState) => ({
  85. ...prevState,
  86. [cachedCategoryId]: targetRowsCount + 2,
  87. }))
  88. }
  89. }, 100)
  90. window.addEventListener('scroll', scrollHandler)
  91. return () => {
  92. window.removeEventListener('scroll', scrollHandler)
  93. }
  94. }, [targetRowsCount, ready, loading, allVideosLoaded, cachedCategoryId])
  95. const displayedEdges = data?.videosConnection.edges.slice(skipCount, targetLoadedVideosCount) || []
  96. const displayedVideos = displayedEdges.map((edge) => edge.node)
  97. const targetDisplayedItemsCount = data
  98. ? Math.min(targetDisplayedVideosCount, data.videosConnection.totalCount - skipCount)
  99. : targetDisplayedVideosCount
  100. const placeholdersCount = targetDisplayedItemsCount - displayedVideos.length
  101. const gridContent = (
  102. <>
  103. {displayedVideos.map((v) => (
  104. <StyledVideoPreview
  105. id={v.id}
  106. channelId={v.channel.id}
  107. title={v.title}
  108. channelName={v.channel.handle}
  109. channelAvatarURL={v.channel.avatarPhotoUrl}
  110. createdAt={v.createdAt}
  111. views={v.views}
  112. posterURL={v.thumbnailUrl}
  113. key={v.id}
  114. />
  115. ))}
  116. {Array.from({ length: placeholdersCount }, (_, idx) => (
  117. <StyledVideoPreviewBase key={idx} />
  118. ))}
  119. </>
  120. )
  121. if (displayedVideos.length <= 0 && placeholdersCount <= 0) {
  122. return null
  123. }
  124. return (
  125. <section className={className}>
  126. {title && <Title>{title}</Title>}
  127. <Grid onResize={(sizes) => setVideosPerRow(sizes.length)}>{gridContent}</Grid>
  128. </section>
  129. )
  130. }
  131. const Title = styled.h4`
  132. margin: 0 0 ${sizes(4)};
  133. font-size: ${typography.sizes.h5};
  134. `
  135. const StyledVideoPreview = styled(VideoPreview)`
  136. margin: 0 auto;
  137. width: 100%;
  138. `
  139. const StyledVideoPreviewBase = styled(VideoPreviewBase)`
  140. margin: 0 auto;
  141. width: 100%;
  142. `
  143. export default InfiniteVideoGrid