InfiniteVideoGrid.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 { typography, sizes } 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, fetchMore, called, refetch }] = useLazyQuery<
  36. GetNewestVideos,
  37. GetNewestVideosVariables
  38. >(GET_NEWEST_VIDEOS, {
  39. notifyOnNetworkStatusChange: true,
  40. })
  41. const loadedVideosCount = data?.videosConnection.edges.length || 0
  42. const allVideosLoaded = data ? !data.videosConnection.pageInfo.hasNextPage : false
  43. const endCursor = data?.videosConnection.pageInfo.endCursor
  44. useEffect(() => {
  45. if (ready && !called) {
  46. fetchVideos({ variables: { first: targetLoadedVideosCount, categoryId } })
  47. }
  48. }, [ready, called, categoryId, targetLoadedVideosCount, fetchVideos])
  49. useEffect(() => {
  50. if (categoryId === cachedCategoryId) {
  51. return
  52. }
  53. setCachedCategoryId(categoryId)
  54. const categoryRowsSet = !!targetRowsCountByCategory[categoryId]
  55. const categoryRowsCount = categoryRowsSet ? targetRowsCountByCategory[categoryId] : INITIAL_ROWS
  56. if (!categoryRowsSet) {
  57. setTargetRowsCountByCategory((prevState) => ({
  58. ...prevState,
  59. [categoryId]: categoryRowsCount,
  60. }))
  61. }
  62. if (!called || !refetch) {
  63. return
  64. }
  65. refetch({ first: categoryRowsCount * videosPerRow + skipCount, categoryId })
  66. }, [categoryId, cachedCategoryId, targetRowsCountByCategory, called, refetch, videosPerRow, skipCount])
  67. useEffect(() => {
  68. if (loading || !fetchMore || allVideosLoaded) {
  69. return
  70. }
  71. if (targetLoadedVideosCount > loadedVideosCount) {
  72. const videosToLoadCount = targetLoadedVideosCount - loadedVideosCount
  73. fetchMore({ variables: { first: videosToLoadCount, after: endCursor, categoryId } })
  74. }
  75. }, [loading, loadedVideosCount, targetLoadedVideosCount, allVideosLoaded, fetchMore, endCursor, categoryId])
  76. useEffect(() => {
  77. const scrollHandler = debounce(() => {
  78. const scrolledToBottom =
  79. window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight
  80. if (scrolledToBottom && ready && !loading && !allVideosLoaded) {
  81. setTargetRowsCountByCategory((prevState) => ({
  82. ...prevState,
  83. [cachedCategoryId]: targetRowsCount + 2,
  84. }))
  85. }
  86. }, 100)
  87. window.addEventListener('scroll', scrollHandler)
  88. return () => {
  89. window.removeEventListener('scroll', scrollHandler)
  90. }
  91. }, [targetRowsCount, ready, loading, allVideosLoaded, cachedCategoryId])
  92. const displayedEdges = data?.videosConnection.edges.slice(skipCount, targetLoadedVideosCount) || []
  93. const displayedVideos = displayedEdges.map((edge) => edge.node)
  94. const targetDisplayedItemsCount = data
  95. ? Math.min(targetDisplayedVideosCount, data.videosConnection.totalCount - skipCount)
  96. : targetDisplayedVideosCount
  97. const placeholdersCount = targetDisplayedItemsCount - displayedVideos.length
  98. const gridContent = (
  99. <>
  100. {displayedVideos.map((v) => (
  101. <StyledVideoPreview
  102. id={v.id}
  103. channelId={v.channel.id}
  104. title={v.title}
  105. channelName={v.channel.handle}
  106. channelAvatarURL={v.channel.avatarPhotoURL}
  107. createdAt={v.publishedOnJoystreamAt}
  108. views={v.views}
  109. posterURL={v.thumbnailURL}
  110. key={v.id}
  111. />
  112. ))}
  113. {Array.from({ length: placeholdersCount }, (_, idx) => (
  114. <StyledVideoPreviewBase key={idx} />
  115. ))}
  116. </>
  117. )
  118. return (
  119. <section className={className}>
  120. {title && <Title>{title}</Title>}
  121. <Grid onResize={(sizes) => setVideosPerRow(sizes.length)}>{gridContent}</Grid>
  122. </section>
  123. )
  124. }
  125. const Title = styled.h4`
  126. margin: 0 0 ${sizes.b4};
  127. font-size: ${typography.sizes.h5};
  128. `
  129. const StyledVideoPreview = styled(VideoPreview)`
  130. margin: 0 auto;
  131. width: 100%;
  132. `
  133. const StyledVideoPreviewBase = styled(VideoPreviewBase)`
  134. margin: 0 auto;
  135. width: 100%;
  136. `
  137. export default InfiniteVideoGrid