-
-
Notifications
You must be signed in to change notification settings - Fork 484
Render an empty div for game cards until they inter the viewport #2460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,24 @@ const GameCard = ({ | |
isRecent = false, | ||
gameInfo: gameInfoFromProps | ||
}: Card) => { | ||
const [visible, setVisible] = useState(false) | ||
|
||
useEffect(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. start as invisible, make visible when the |
||
// render an empty div until the card enters the viewport | ||
// check GameList for the other side of this detection | ||
const callback = (e: CustomEvent<{ appNames: string[] }>) => { | ||
if (e.detail.appNames.includes(gameInfoFromProps.app_name)) { | ||
setVisible(true) | ||
} | ||
} | ||
|
||
window.addEventListener('visible-cards', callback) | ||
|
||
return () => { | ||
window.removeEventListener('visible-cards', callback) | ||
} | ||
}, []) | ||
|
||
const [gameInfo, setGameInfo] = useState<GameInfo | SideloadGame>( | ||
gameInfoFromProps | ||
) | ||
|
@@ -80,7 +98,8 @@ const GameCard = ({ | |
favouriteGames, | ||
allTilesInColor, | ||
showDialogModal, | ||
setIsSettingsModalOpen | ||
setIsSettingsModalOpen, | ||
activeController | ||
} = useContext(ContextProvider) | ||
|
||
const { | ||
|
@@ -318,6 +337,7 @@ const GameCard = ({ | |
const instClass = isInstalled ? 'installed' : '' | ||
const hiddenClass = isHiddenGame ? 'hidden' : '' | ||
const notAvailableClass = notAvailable ? 'notAvailable' : '' | ||
const gamepadClass = activeController ? 'gamepad' : '' | ||
const imgClasses = `gameImg ${isInstalled ? 'installed' : ''} ${ | ||
allTilesInColor ? 'allTilesInColor' : '' | ||
}` | ||
|
@@ -327,13 +347,21 @@ const GameCard = ({ | |
|
||
const wrapperClasses = `${ | ||
grid ? 'gameCard' : 'gameListItem' | ||
} ${instClass} ${hiddenClass} ${notAvailableClass}` | ||
|
||
const { activeController } = useContext(ContextProvider) | ||
} ${instClass} ${hiddenClass} ${notAvailableClass} ${gamepadClass}` | ||
|
||
const showUpdateButton = | ||
hasUpdate && !isUpdating && !isQueued && !notAvailable | ||
|
||
if (!visible) { | ||
return ( | ||
<div | ||
className={wrapperClasses} | ||
data-app-name={appName} | ||
data-invisible={true} | ||
></div> | ||
) | ||
} | ||
|
||
return ( | ||
<div> | ||
{showUninstallModal && ( | ||
|
@@ -344,7 +372,7 @@ const GameCard = ({ | |
/> | ||
)} | ||
<ContextMenu items={items}> | ||
<div className={wrapperClasses}> | ||
<div className={wrapperClasses} data-app-name={appName}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using this data attribute for the intersection observer |
||
{haveStatus && <span className="gameCardStatus">{label}</span>} | ||
<Link | ||
to={`/gamepage/${runner}/${appName}`} | ||
|
@@ -394,11 +422,7 @@ const GameCard = ({ | |
</span> | ||
</Link> | ||
<> | ||
<span | ||
className={classNames('icons', { | ||
gamepad: activeController | ||
})} | ||
> | ||
<span className="icons"> | ||
{showUpdateButton && ( | ||
<SvgButton | ||
className="updateIcon" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, { useContext, useEffect, useState } from 'react' | ||
import React, { useContext, useEffect } from 'react' | ||
import { GameInfo, Runner, SideloadGame } from 'common/types' | ||
import cx from 'classnames' | ||
import GameCard from '../GameCard' | ||
|
@@ -26,62 +26,51 @@ const GamesList = ({ | |
onlyInstalled = false, | ||
isRecent = false | ||
}: Props): JSX.Element => { | ||
const { gameUpdates, showNonAvailable } = useContext(ContextProvider) | ||
const { gameUpdates } = useContext(ContextProvider) | ||
const { t } = useTranslation() | ||
const [gameCards, setGameCards] = useState<JSX.Element[]>([]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this useState and the useEffect changing it was causing extra re-renders I moved this back to the return jsx instead of using a useState+useEffect |
||
|
||
useEffect(() => { | ||
let mounted = true | ||
|
||
const createGameCards = async () => { | ||
if (!library.length) { | ||
return | ||
if (library.length) { | ||
const options = { | ||
root: document.querySelector('.listing'), | ||
rootMargin: '500px', | ||
threshold: 0 | ||
} | ||
const resolvedLibrary = library.map(async (gameInfo) => { | ||
const { app_name, is_installed, runner } = gameInfo | ||
|
||
let is_dlc = false | ||
if (gameInfo.runner !== 'sideload') { | ||
is_dlc = gameInfo.install.is_dlc ?? false | ||
} | ||
const callback: IntersectionObserverCallback = (entries, observer) => { | ||
const entered: string[] = [] | ||
entries.forEach((entry) => { | ||
if (entry.intersectionRatio > 0) { | ||
// when a card is intersecting the viewport | ||
const appName = (entry.target as HTMLDivElement).dataset | ||
.appName as string | ||
|
||
if (is_dlc) { | ||
return null | ||
} | ||
if (!is_installed && onlyInstalled) { | ||
return null | ||
} | ||
// store this appName for later | ||
entered.push(appName) | ||
// stop observing this element | ||
observer.unobserve(entry.target) | ||
} | ||
}) | ||
|
||
const hasUpdate = is_installed && gameUpdates?.includes(app_name) | ||
return ( | ||
<GameCard | ||
key={app_name} | ||
hasUpdate={hasUpdate} | ||
buttonClick={() => { | ||
if (gameInfo.runner !== 'sideload') | ||
handleGameCardClick(app_name, runner, gameInfo) | ||
}} | ||
forceCard={layout === 'grid'} | ||
isRecent={isRecent} | ||
gameInfo={gameInfo} | ||
/> | ||
// dispatch an event with the newley visible cards | ||
// check GameCard for the other side of this detection | ||
window.dispatchEvent( | ||
new CustomEvent('visible-cards', { detail: { appNames: entered } }) | ||
) | ||
}) | ||
const gameCardElements = (await Promise.all( | ||
resolvedLibrary | ||
)) as JSX.Element[] | ||
|
||
if (mounted) { | ||
setGameCards(gameCardElements) | ||
} | ||
} | ||
|
||
createGameCards() | ||
const observer = new IntersectionObserver(callback, options) | ||
|
||
document.querySelectorAll('[data-invisible]').forEach((card) => { | ||
observer.observe(card) | ||
}) | ||
|
||
return () => { | ||
mounted = false | ||
return () => { | ||
observer.disconnect() | ||
} | ||
} | ||
}, [library, onlyInstalled, layout, gameUpdates, isRecent, showNonAvailable]) | ||
return () => ({}) | ||
}, [library]) | ||
|
||
return ( | ||
<div | ||
|
@@ -100,7 +89,37 @@ const GamesList = ({ | |
<span>{t('wine.actions', 'Action')}</span> | ||
</div> | ||
)} | ||
{!!library.length && gameCards} | ||
{!!library.length && | ||
library.map((gameInfo) => { | ||
const { app_name, is_installed, runner } = gameInfo | ||
|
||
let is_dlc = false | ||
if (gameInfo.runner !== 'sideload') { | ||
is_dlc = gameInfo.install.is_dlc ?? false | ||
} | ||
|
||
if (is_dlc) { | ||
return null | ||
} | ||
if (!is_installed && onlyInstalled) { | ||
return null | ||
} | ||
|
||
const hasUpdate = is_installed && gameUpdates?.includes(app_name) | ||
return ( | ||
<GameCard | ||
key={app_name} | ||
hasUpdate={hasUpdate} | ||
buttonClick={() => { | ||
if (gameInfo.runner !== 'sideload') | ||
handleGameCardClick(app_name, runner, gameInfo) | ||
}} | ||
forceCard={layout === 'grid'} | ||
isRecent={isRecent} | ||
gameInfo={gameInfo} | ||
/> | ||
) | ||
})} | ||
</div> | ||
) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these are the current ratios, but I'm hardcoding them so the empty divs use the correct space