Skip to content

Order carousel as images appear in items / markdown #2239

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
30 changes: 26 additions & 4 deletions components/carousel.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,26 +105,48 @@ function CarouselOverflow ({ originalSrc, rel }) {

export function CarouselProvider ({ children }) {
const media = useRef(new Map())
const itemArray = useRef(new Map())
const itemCount = useRef(0)
const showModal = useShowModal()

const showCarousel = useCallback(({ src }) => {
const sortedMedia = Array.from(media.current.entries())
.sort(([, a], [, b]) => a.sortKey - b.sortKey)

showModal((close, setOptions) => {
return <Carousel close={close} mediaArr={Array.from(media.current.entries())} src={src} setOptions={setOptions} />
return <Carousel close={close} mediaArr={sortedMedia} src={src} setOptions={setOptions} />
}, {
fullScreen: true,
overflow: <CarouselOverflow {...media.current.get(src)} />
})
}, [showModal, media.current])

const addMedia = useCallback(({ src, originalSrc, rel }) => {
media.current.set(src, { src, originalSrc, rel })
const addMedia = useCallback(({ src, originalSrc, rel, itemId, imgIndex = 0 }) => {
const items = itemArray.current
const itemOrder = items.has(itemId) ? items.get(itemId).itemOrder : 0
const sortKey = itemOrder * 100 + imgIndex
media.current.set(src, { src, originalSrc, rel, sortKey })
}, [media.current])

const removeMedia = useCallback((src) => {
media.current.delete(src)
}, [media.current])

const value = useMemo(() => ({ showCarousel, addMedia, removeMedia }), [showCarousel, addMedia, removeMedia])
const addItem = useCallback((itemId) => {
const items = itemArray.current
if (!items.has(itemId)) {
itemCount.current += 1
items.set(itemId, { itemOrder: itemCount.current })
}
return items.get(itemId).itemOrder
}, [])

const value = useMemo(() => ({
showCarousel,
addMedia,
removeMedia,
addItem
}), [showCarousel, addMedia, removeMedia, addItem])
return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
}

Expand Down
6 changes: 6 additions & 0 deletions components/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import LinkToContext from './link-to-context'
import Boost from './boost-button'
import { gql, useApolloClient } from '@apollo/client'
import classNames from 'classnames'
import { useCarousel } from './carousel'

function Parent ({ item, rootText }) {
const root = useRoot()
Expand Down Expand Up @@ -114,6 +115,11 @@ export default function Comment ({

const { cache } = useApolloClient()

const carousel = useCarousel()
if (carousel) {
carousel.addItem(item.id)
}

useEffect(() => {
const comment = cache.readFragment({
id: `Item:${router.query.commentId}`,
Expand Down
30 changes: 25 additions & 5 deletions components/item-full.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@ import { numWithUnits } from '@/lib/format'
import { useQuoteReply } from './use-quote-reply'
import { UNKNOWN_LINK_REL } from '@/lib/constants'
import classNames from 'classnames'
import { CarouselProvider } from './carousel'
import { CarouselProvider, useCarousel } from './carousel'
import Embed from './embed'
import { useRouter } from 'next/router'

function BioItem ({ item, handleClick }) {
const { me } = useMe()
if (!item.text) {
return null
}

const carousel = useCarousel()
if (carousel) {
carousel.addItem(item.id)
}

return (
<>
<ItemText item={item} />
Expand All @@ -50,7 +56,7 @@ function BioItem ({ item, handleClick }) {
)
}

function ItemEmbed ({ url, imgproxyUrls }) {
function ItemEmbed ({ url, imgproxyUrls, itemId }) {
const provider = parseEmbedUrl(url)
if (provider) {
return (
Expand All @@ -65,7 +71,14 @@ function ItemEmbed ({ url, imgproxyUrls }) {
const srcSet = imgproxyUrls?.[url]
return (
<div className='mt-3'>
<MediaOrLink src={src} srcSet={srcSet} topLevel linkFallback={false} />
<MediaOrLink
src={src}
srcSet={srcSet}
topLevel
linkFallback={false}
itemId={itemId}
imgIndex={0}
/>
</div>
)
}
Expand Down Expand Up @@ -93,6 +106,11 @@ function TopLevelItem ({ item, noReply, ...props }) {
const ItemComponent = item.isJob ? ItemJob : Item
const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text })

const carousel = useCarousel()
if (carousel) {
carousel.addItem(item.id)
}

return (
<ItemComponent
item={item}
Expand All @@ -110,7 +128,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
>
<article className={classNames(styles.fullItemContainer, 'topLevel')} ref={textRef}>
{item.text && <ItemText item={item} />}
{item.url && !item.outlawed && <ItemEmbed url={item.url} imgproxyUrls={item.imgproxyUrls} />}
{item.url && !item.outlawed && <ItemEmbed url={item.url} imgproxyUrls={item.imgproxyUrls} itemId={item.id} />}
{item.poll && <Poll item={item} />}
{item.bounty &&
<div className='fw-bold mt-2'>
Expand Down Expand Up @@ -164,6 +182,8 @@ export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props
useEffect(() => {
commentsViewed(item)
}, [item.lastCommentAt])
const router = useRouter()
const carouselKey = `${item.id}--${router.query.sort || 'default'}`

return (
<>
Expand All @@ -174,7 +194,7 @@ export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props
</div>)
: <div />}
<RootProvider root={item.root || item}>
<CarouselProvider key={item.id}>
<CarouselProvider key={carouselKey}>
{item.parentId
? <Comment topLevel item={item} replyOpen includeParent noComments {...props} />
: (
Expand Down
10 changes: 8 additions & 2 deletions components/media-or-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ export default function MediaOrLink ({ linkFallback = true, ...props }) {

useEffect(() => {
if (!media.image) return
addMedia({ src: media.bestResSrc, originalSrc: media.originalSrc, rel: props.rel })
}, [media.image])
addMedia({
src: media.bestResSrc,
originalSrc: media.originalSrc,
rel: props.rel,
itemId: props.itemId,
imgIndex: props.imgIndex
})
}, [media.image, props.rel, props.itemId, props.imgIndex])

const handleClick = useCallback(() => showCarousel({ src: media.bestResSrc }),
[showCarousel, media.bestResSrc])
Expand Down
30 changes: 25 additions & 5 deletions components/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,17 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
}, [containerRef.current, setOverflowing])

const TextMediaOrLink = useCallback(props => {
return <MediaLink {...props} outlawed={outlawed} imgproxyUrls={imgproxyUrls} topLevel={topLevel} rel={rel} />
},
[outlawed, imgproxyUrls, topLevel, rel])
return (
<MediaLink
{...props}
outlawed={outlawed}
imgproxyUrls={imgproxyUrls}
topLevel={topLevel}
rel={rel}
itemId={itemId}
/>
)
}, [outlawed, imgproxyUrls, topLevel, rel, itemId])

const components = useMemo(() => ({
h1: ({ node, id, ...props }) => <h1 id={topLevel ? id : undefined} {...props} />,
Expand Down Expand Up @@ -142,6 +150,9 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
}), [outlawed, rel, TextMediaOrLink, topLevel])

const carousel = useCarousel()
if (carousel) {
carousel.addItem(itemId)
}

const markdownContent = useMemo(() => (
<ReactMarkdown
Expand Down Expand Up @@ -220,7 +231,7 @@ function Footnote ({ children, node, ...props }) {
}

function MediaLink ({
node, src, outlawed, imgproxyUrls, rel = UNKNOWN_LINK_REL, ...props
node, src, outlawed, imgproxyUrls, itemId, imgIndex, rel = UNKNOWN_LINK_REL, ...props
}) {
const url = IMGPROXY_URL_REGEXP.test(src) ? decodeProxyUrl(src) : src
// if outlawed, render the media link as text
Expand All @@ -230,7 +241,16 @@ function MediaLink ({

const srcSet = imgproxyUrls?.[url]

return <MediaOrLink srcSet={srcSet} src={src} rel={rel} {...props} />
return (
<MediaOrLink
srcSet={srcSet}
src={src}
rel={rel}
itemId={itemId}
imgIndex={imgIndex}
{...props}
/>
)
}

function Table ({ node, ...props }) {
Expand Down
16 changes: 15 additions & 1 deletion lib/rehype-sn.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ export default function rehypeSN (options = {}) {
const { stylers = [] } = options

return function transformer (tree) {
let imgIndex = 0

try {
visit(tree, (node, index, parent) => {
if (parent?.tagName === 'code') {
// don't process code blocks
return
}

// If node is img, increment and assign imgIndex
if (node.tagName === 'img') {
node.properties.imgIndex ??= ++imgIndex
}

// Handle inline code property
if (node.tagName === 'code') {
node.properties.inline = !(parent && parent.tagName === 'pre')
Expand Down Expand Up @@ -83,6 +90,8 @@ export default function rehypeSN (options = {}) {
node.properties = { ...embed, src: node.properties.href }
} else {
node.tagName = 'autolink'
// since autolinks could be an image, increment imgIndex
node.properties.imgIndex ??= ++imgIndex
}
}

Expand Down Expand Up @@ -209,6 +218,10 @@ export default function rehypeSN (options = {}) {
const allImages = adjacentNodes.flatMap(n =>
n.tagName === 'img' ? [n] : (Array.isArray(n.children) ? n.children.filter(child => child.tagName === 'img') : [])
)
// increment imgIndex for each of the images
allImages.forEach(n => {
n.properties.imgIndex ??= ++imgIndex
})
const collageNode = {
type: 'element',
tagName: 'p',
Expand All @@ -234,7 +247,8 @@ export default function rehypeSN (options = {}) {
node.children.every(child =>
(child.tagName === 'img') ||
(child.type === 'text' && typeof child.value === 'string' && !child.value.trim())
)
) &&
node.children.some(child => child.tagName === 'img')
}

function replaceMention (value, username) {
Expand Down