import { giphyBlue, giphyGreen, giphyPurple, giphyRed, giphyYellow } from '@giphy/js-brand' import { IGif, ImageAllTypes, IUser } from '@giphy/js-types' import { getAltText, getBestRendition, getGifHeight, Logger } from '@giphy/js-util' import { css, cx } from '@emotion/css' import { h } from 'preact' import { useContext, useEffect, useRef, useState } from 'preact/hooks' import * as pingback from '../util/pingback' import AttributionOverlay from './attribution/overlay' import VerifiedBadge from './attribution/verified-badge' import { PingbackContext } from './pingback-context-manager' const gifCss = css` display: block; &:focus { outline: unset; } img { display: block; } .${VerifiedBadge.className} { g { fill: white; } } ` export const GRID_COLORS = [giphyBlue, giphyGreen, giphyPurple, giphyRed, giphyYellow] export const getColor = () => GRID_COLORS[Math.round(Math.random() * (GRID_COLORS.length - 1))] const hoverTimeoutDelay = 200 const Container = (props: any) => (props.href ? :
) export type EventProps = { // fired on desktop when hovered for onGifHover?: (gif: IGif, e: Event) => void // fired every time the gif is show onGifVisible?: (gif: IGif, e: Event) => void // fired once after the gif loads and when it's completely in view onGifSeen?: (gif: IGif, boundingClientRect: ClientRect | DOMRect) => void // fired when the gif is clicked onGifClick?: (gif: IGif, e: Event) => void // fired when the gif is right clicked onGifRightClick?: (gif: IGif, e: Event) => void // fired when the gif is selected and a key is pressed onGifKeyPress?: (gif: IGif, e: Event) => void } function useMutableRef(initialValue?: T) { const [ref] = useState<{ current: T | undefined }>({ current: initialValue }) return ref } export type GifOverlayProps = { gif: IGif isHovered: boolean } type GifProps = { gif: IGif width: number height?: number backgroundColor?: string className?: string user?: Partial hideAttribution?: boolean noLink?: boolean borderRadius?: number tabIndex?: number } export type Props = GifProps & EventProps const placeholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' const noop = () => {} const Gif = ({ gif, width, height: forcedHeight, onGifRightClick = noop, className, onGifClick = noop, onGifKeyPress = noop, onGifSeen = noop, onGifVisible = noop, user = {}, backgroundColor, hideAttribution = false, noLink = false, borderRadius = 4, tabIndex, }: Props) => { // only fire seen once per gif id const [hasFiredSeen, setHasFiredSeen] = useState(false) // classname to target animations on image load const [loadedClassname, setLoadedClassName] = useState('') // hovered is for the gif overlay const [isHovered, setHovered] = useState(false) // only show the gif if it's on the screen const [shouldShowMedia, setShouldShowMedia] = useState(false) // the background color shouldn't change unless it comes from a prop or we have a sticker const defaultBgColor = useRef(getColor()) // the a tag the media is rendered into const container = useRef(null) // intersection observer with no threshold const showGifObserver = useMutableRef() // intersection observer with a threshold of 1 (full element is on screen) const fullGifObserver = useMutableRef() // fire hover pingback after this timeout const hoverTimeout = useMutableRef() // fire onseen ref (changes per gif, so need a ref) const sendOnSeen = useRef<(_: IntersectionObserverEntry) => void>(noop) // custom pingback const { attributes } = useContext(PingbackContext) const onMouseOver = (e: Event) => { clearTimeout(hoverTimeout.current!) setHovered(true) hoverTimeout.current = window.setTimeout(() => { pingback.onGifHover(gif, user?.id, e.target as HTMLElement, attributes) }, hoverTimeoutDelay) } const onMouseLeave = () => { clearTimeout(hoverTimeout.current!) setHovered(false) } const onClick = (e: Event) => { // fire pingback pingback.onGifClick(gif, user?.id, e.target as HTMLElement, attributes) onGifClick(gif, e) } const onKeyPress = (e: Event) => { onGifKeyPress(gif, e) } // using a ref in case `gif` changes sendOnSeen.current = (entry: IntersectionObserverEntry) => { // flag so we don't observe any more setHasFiredSeen(true) Logger.debug(`GIF ${gif.id} seen. ${gif.title}`) // fire pingback pingback.onGifSeen(gif, user?.id, entry.boundingClientRect, attributes) // fire custom onGifSeen onGifSeen?.(gif, entry.boundingClientRect) // disconnect if (fullGifObserver.current) { fullGifObserver.current.disconnect() } } const onImageLoad = (e: Event) => { if (!fullGifObserver.current) { fullGifObserver.current = new IntersectionObserver( ([entry]: IntersectionObserverEntry[]) => { if (entry.isIntersecting) { sendOnSeen.current(entry) } }, { threshold: [0.99] } ) } if (!hasFiredSeen && container.current && fullGifObserver.current) { // observe img for full gif view fullGifObserver.current.observe(container.current) } onGifVisible(gif, e) // gif is visible, perhaps just partially setLoadedClassName(Gif.imgLoadedClassName) } useEffect(() => { if (fullGifObserver.current) { fullGifObserver.current.disconnect() } setHasFiredSeen(false) }, [gif.id]) useEffect(() => { showGifObserver.current = new IntersectionObserver(([entry]: IntersectionObserverEntry[]) => { const { isIntersecting } = entry // show the gif if the container is on the screen setShouldShowMedia(isIntersecting) // remove the fullGifObserver if we go off the screen // we may have already disconnected if the hasFiredSeen happened if (!isIntersecting && fullGifObserver.current) { fullGifObserver.current.disconnect() } }) showGifObserver.current.observe(container.current!) return () => { if (showGifObserver.current) showGifObserver.current.disconnect() if (fullGifObserver.current) fullGifObserver.current.disconnect() if (hoverTimeout.current) clearTimeout(hoverTimeout.current) } }, []) const height = forcedHeight || getGifHeight(gif, width) const bestRendition = getBestRendition(gif.images, width, height) const rendition = gif.images[bestRendition.renditionName] as ImageAllTypes const background = backgroundColor || // <- specified background prop // sticker has black if no backgroundColor is specified (gif.is_sticker ? `url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4AQMAAACSSKldAAAABlBMVEUhIiIWFhYoSqvJAAAAGElEQVQY02MAAv7///8PWxqIPwDZw5UGABtgwz2xhFKxAAAAAElFTkSuQmCC') 0 0` : defaultBgColor.current) const borderRadiusCss = borderRadius ? css` border-radius: ${borderRadius}px; overflow: hidden; ` : '' return ( onGifRightClick(gif, e)} onKeyPress={onKeyPress} tabIndex={tabIndex} >
{getAltText(gif)} {}} /> {shouldShowMedia ? (
{!hideAttribution && }
) : null}
) } Gif.className = 'giphy-gif' Gif.imgClassName = 'giphy-gif-img' Gif.imgLoadedClassName = 'giphy-img-loaded' export default Gif