@@ -6,13 +6,17 @@ import FeedOptionsSwitch from './feed-options-switch';
66import UserProfile from './user-profile' ;
77import { useNouter } from '../services/nouter' ;
88import PaginatedView from './paginated-view' ;
9- import { useMemo , useState } from 'react' ;
9+ import { useEffect , useMemo , useState } from 'react' ;
1010import { VisualContainer } from './post/attachments/visual/container' ;
1111import { isPostNSFW } from './select-utils' ;
1212import { htmlSafe } from '../utils' ;
1313import { Helmet } from 'react-helmet' ;
14+ import { NEEDMORE_EVENT , MOREITEMS_EVENT } from '../services/lightbox-events' ;
15+ import { useLightboxItems } from './post/attachments/visual/hooks' ;
1416
1517const tokenizeHashtags = hashtags ( ) ;
18+ const lightboxOptions = { loop : false , pagination : true } ;
19+ const PAGE_SIZE = 30 ;
1620
1721// Persists showNSFW state across remounts within the same userMedia route.
1822// Resets when switching to a different user.
@@ -35,6 +39,7 @@ export default function UserMedia() {
3539 setShowNSFWState ( value ) ;
3640 } ;
3741 const { attachments, hasNSFW } = useMediaAttachments ( foundUser , showNSFW ) ;
42+ useLightboxPagination ( attachments ) ;
3843
3944 const nameForTitle = useMemo (
4045 ( ) =>
@@ -76,7 +81,12 @@ export default function UserMedia() {
7681 { canViewAccountContent ? (
7782 attachments . length > 0 ? (
7883 < PaginatedView >
79- < VisualContainer attachments = { attachments } isNSFW = { false } isExpanded />
84+ < VisualContainer
85+ attachments = { attachments }
86+ isNSFW = { false }
87+ isExpanded
88+ lightboxOptions = { lightboxOptions }
89+ />
8090 </ PaginatedView >
8191 ) : (
8292 < div className = "box-body" >
@@ -94,6 +104,58 @@ export default function UserMedia() {
94104 ) ;
95105}
96106
107+ // Enables infinite scroll in the lightbox on the UserMedia page.
108+ //
109+ // The lightbox (lightbox-actual.js) and this page communicate via two custom
110+ // DOM events, without direct dependency on each other:
111+ //
112+ // 1. When the user approaches the last slide in the lightbox, it dispatches
113+ // NEEDMORE_EVENT. This hook listens for it and navigates to the next page
114+ // using `navigate({ replace: true })`. The `replace` flag swaps the lightbox
115+ // history marker with the new page URL (see lightbox-actual.js for details).
116+ //
117+ // 2. Once React processes the new page data and `useMediaAttachments` produces
118+ // a new list of attachments, this hook dispatches MOREITEMS_EVENT with the
119+ // lightbox-formatted items and the `isLastPage` flag. The lightbox receives
120+ // the event, deduplicates items by `pid`, pushes new ones into its
121+ // `dataSource` array, and restores the history marker via `pushState`.
122+ //
123+ // This way the lightbox never touches React/Redux, and this page never imports
124+ // PhotoSwipe — they only share event name constants from lightbox-events.js.
125+ function useLightboxPagination ( attachments ) {
126+ const { navigate, location } = useNouter ( ) ;
127+ const isLastPage = useSelector ( ( state ) => state . feedViewState . isLastPage ) ;
128+ const lightboxItems = useLightboxItems ( attachments ) ;
129+
130+ // Listen for "need more" requests from the lightbox
131+ useEffect ( ( ) => {
132+ let loading = false ;
133+ const handler = ( ) => {
134+ if ( loading || isLastPage ) {
135+ return ;
136+ }
137+ loading = true ;
138+ const offset = ( + location . query . offset || 0 ) + PAGE_SIZE ;
139+ navigate (
140+ { pathname : location . pathname , query : { ...location . query , offset } } ,
141+ { replace : true } ,
142+ ) ;
143+ } ;
144+ document . addEventListener ( NEEDMORE_EVENT , handler ) ;
145+ return ( ) => document . removeEventListener ( NEEDMORE_EVENT , handler ) ;
146+ } , [ navigate , location , isLastPage ] ) ;
147+
148+ // Dispatch new items to the lightbox when attachments change
149+ useEffect ( ( ) => {
150+ if ( lightboxItems . length === 0 ) {
151+ return ;
152+ }
153+ document . dispatchEvent (
154+ new CustomEvent ( MOREITEMS_EVENT , { detail : { items : lightboxItems , isLastPage } } ) ,
155+ ) ;
156+ } , [ lightboxItems , isLastPage ] ) ;
157+ }
158+
97159// Same logic as in UserProfileHead: don't show media for private/banned accounts
98160function useCanViewAccountContent ( user ) {
99161 const currentUser = useSelector ( ( state ) => state . user ) ;
0 commit comments