Show media modal on public pages (#6801)
This commit is contained in:
		
				
					committed by
					
						 Eugen Rochko
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							1c15329cce
						
					
				
				
					commit
					ff7941e652
				
			| @@ -14,10 +14,6 @@ const messages = defineMessages({ | |||||||
|  |  | ||||||
| class Item extends React.PureComponent { | class Item extends React.PureComponent { | ||||||
|  |  | ||||||
|   static contextTypes = { |  | ||||||
|     router: PropTypes.object, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     attachment: ImmutablePropTypes.map.isRequired, |     attachment: ImmutablePropTypes.map.isRequired, | ||||||
|     standalone: PropTypes.bool, |     standalone: PropTypes.bool, | ||||||
| @@ -53,7 +49,7 @@ class Item extends React.PureComponent { | |||||||
|   handleClick = (e) => { |   handleClick = (e) => { | ||||||
|     const { index, onClick } = this.props; |     const { index, onClick } = this.props; | ||||||
|  |  | ||||||
|     if (this.context.router && e.button === 0) { |     if (e.button === 0) { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       onClick(index); |       onClick(index); | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										84
									
								
								app/javascript/mastodon/components/modal_root.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								app/javascript/mastodon/components/modal_root.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  |  | ||||||
|  | export default class ModalRoot extends React.PureComponent { | ||||||
|  |  | ||||||
|  |   static propTypes = { | ||||||
|  |     children: PropTypes.node, | ||||||
|  |     onClose: PropTypes.func.isRequired, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   state = { | ||||||
|  |     revealed: !!this.props.children, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   activeElement = this.state.revealed ? document.activeElement : null; | ||||||
|  |  | ||||||
|  |   handleKeyUp = (e) => { | ||||||
|  |     if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) | ||||||
|  |          && !!this.props.children) { | ||||||
|  |       this.props.onClose(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentDidMount () { | ||||||
|  |     window.addEventListener('keyup', this.handleKeyUp, false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentWillReceiveProps (nextProps) { | ||||||
|  |     if (!!nextProps.children && !this.props.children) { | ||||||
|  |       this.activeElement = document.activeElement; | ||||||
|  |  | ||||||
|  |       this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); | ||||||
|  |     } else if (!nextProps.children) { | ||||||
|  |       this.setState({ revealed: false }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentDidUpdate (prevProps) { | ||||||
|  |     if (!this.props.children && !!prevProps.children) { | ||||||
|  |       this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); | ||||||
|  |       this.activeElement.focus(); | ||||||
|  |       this.activeElement = null; | ||||||
|  |     } | ||||||
|  |     if (this.props.children) { | ||||||
|  |       requestAnimationFrame(() => { | ||||||
|  |         this.setState({ revealed: true }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentWillUnmount () { | ||||||
|  |     window.removeEventListener('keyup', this.handleKeyUp); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getSiblings = () => { | ||||||
|  |     return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setRef = ref => { | ||||||
|  |     this.node = ref; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render () { | ||||||
|  |     const { children, onClose } = this.props; | ||||||
|  |     const { revealed } = this.state; | ||||||
|  |     const visible = !!children; | ||||||
|  |  | ||||||
|  |     if (!visible) { | ||||||
|  |       return ( | ||||||
|  |         <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} /> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> | ||||||
|  |         <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> | ||||||
|  |           <div role='presentation' className='modal-root__overlay' onClick={onClose} /> | ||||||
|  |           <div role='dialog' className='modal-root__container'>{children}</div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,68 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import ReactDOM from 'react-dom'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { IntlProvider, addLocaleData } from 'react-intl'; | ||||||
|  | import { getLocale } from '../locales'; | ||||||
|  | import MediaGallery from '../components/media_gallery'; | ||||||
|  | import ModalRoot from '../components/modal_root'; | ||||||
|  | import MediaModal from '../features/ui/components/media_modal'; | ||||||
|  | import { fromJS } from 'immutable'; | ||||||
|  |  | ||||||
|  | const { localeData, messages } = getLocale(); | ||||||
|  | addLocaleData(localeData); | ||||||
|  |  | ||||||
|  | export default class MediaGalleriesContainer extends React.PureComponent { | ||||||
|  |  | ||||||
|  |   static propTypes = { | ||||||
|  |     locale: PropTypes.string.isRequired, | ||||||
|  |     galleries: PropTypes.object.isRequired, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   state = { | ||||||
|  |     media: null, | ||||||
|  |     index: null, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   handleOpenMedia = (media, index) => { | ||||||
|  |     document.body.classList.add('media-gallery-standalone__body'); | ||||||
|  |     this.setState({ media, index }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleCloseMedia = () => { | ||||||
|  |     document.body.classList.remove('media-gallery-standalone__body'); | ||||||
|  |     this.setState({ media: null, index: null }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render () { | ||||||
|  |     const { locale, galleries } = this.props; | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       <IntlProvider locale={locale} messages={messages}> | ||||||
|  |         <React.Fragment> | ||||||
|  |           {[].map.call(galleries, gallery => { | ||||||
|  |             const { media, ...props } = JSON.parse(gallery.getAttribute('data-props')); | ||||||
|  |  | ||||||
|  |             return ReactDOM.createPortal( | ||||||
|  |               <MediaGallery | ||||||
|  |                 {...props} | ||||||
|  |                 media={fromJS(media)} | ||||||
|  |                 onOpenMedia={this.handleOpenMedia} | ||||||
|  |               />, | ||||||
|  |               gallery | ||||||
|  |             ); | ||||||
|  |           })} | ||||||
|  |           <ModalRoot onClose={this.handleCloseMedia}> | ||||||
|  |             {this.state.media === null || this.state.index === null ? null : ( | ||||||
|  |               <MediaModal | ||||||
|  |                 media={this.state.media} | ||||||
|  |                 index={this.state.index} | ||||||
|  |                 onClose={this.handleCloseMedia} | ||||||
|  |               /> | ||||||
|  |             )} | ||||||
|  |           </ModalRoot> | ||||||
|  |         </React.Fragment> | ||||||
|  |       </IntlProvider> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| import React from 'react'; |  | ||||||
| import PropTypes from 'prop-types'; |  | ||||||
| import { IntlProvider, addLocaleData } from 'react-intl'; |  | ||||||
| import { getLocale } from '../locales'; |  | ||||||
| import MediaGallery from '../components/media_gallery'; |  | ||||||
| import { fromJS } from 'immutable'; |  | ||||||
|  |  | ||||||
| const { localeData, messages } = getLocale(); |  | ||||||
| addLocaleData(localeData); |  | ||||||
|  |  | ||||||
| export default class MediaGalleryContainer extends React.PureComponent { |  | ||||||
|  |  | ||||||
|   static propTypes = { |  | ||||||
|     locale: PropTypes.string.isRequired, |  | ||||||
|     media: PropTypes.array.isRequired, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   handleOpenMedia = () => {} |  | ||||||
|  |  | ||||||
|   render () { |  | ||||||
|     const { locale, media, ...props } = this.props; |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|       <IntlProvider locale={locale} messages={messages}> |  | ||||||
|         <MediaGallery |  | ||||||
|           {...props} |  | ||||||
|           media={fromJS(media)} |  | ||||||
|           onOpenMedia={this.handleOpenMedia} |  | ||||||
|         /> |  | ||||||
|       </IntlProvider> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
|  | import Base from '../../../components/modal_root'; | ||||||
| import BundleContainer from '../containers/bundle_container'; | import BundleContainer from '../containers/bundle_container'; | ||||||
| import BundleModalError from './bundle_modal_error'; | import BundleModalError from './bundle_modal_error'; | ||||||
| import ModalLoading from './modal_loading'; | import ModalLoading from './modal_loading'; | ||||||
| @@ -39,56 +40,6 @@ export default class ModalRoot extends React.PureComponent { | |||||||
|     onClose: PropTypes.func.isRequired, |     onClose: PropTypes.func.isRequired, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   state = { |  | ||||||
|     revealed: false, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   handleKeyUp = (e) => { |  | ||||||
|     if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) |  | ||||||
|          && !!this.props.type) { |  | ||||||
|       this.props.onClose(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   componentDidMount () { |  | ||||||
|     window.addEventListener('keyup', this.handleKeyUp, false); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   componentWillReceiveProps (nextProps) { |  | ||||||
|     if (!!nextProps.type && !this.props.type) { |  | ||||||
|       this.activeElement = document.activeElement; |  | ||||||
|  |  | ||||||
|       this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); |  | ||||||
|     } else if (!nextProps.type) { |  | ||||||
|       this.setState({ revealed: false }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   componentDidUpdate (prevProps) { |  | ||||||
|     if (!this.props.type && !!prevProps.type) { |  | ||||||
|       this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); |  | ||||||
|       this.activeElement.focus(); |  | ||||||
|       this.activeElement = null; |  | ||||||
|     } |  | ||||||
|     if (this.props.type) { |  | ||||||
|       requestAnimationFrame(() => { |  | ||||||
|         this.setState({ revealed: true }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   componentWillUnmount () { |  | ||||||
|     window.removeEventListener('keyup', this.handleKeyUp); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getSiblings = () => { |  | ||||||
|     return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   setRef = ref => { |  | ||||||
|     this.node = ref; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   renderLoading = modalId => () => { |   renderLoading = modalId => () => { | ||||||
|     return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; |     return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; | ||||||
|   } |   } | ||||||
| @@ -101,28 +52,16 @@ export default class ModalRoot extends React.PureComponent { | |||||||
|  |  | ||||||
|   render () { |   render () { | ||||||
|     const { type, props, onClose } = this.props; |     const { type, props, onClose } = this.props; | ||||||
|     const { revealed } = this.state; |  | ||||||
|     const visible = !!type; |     const visible = !!type; | ||||||
|  |  | ||||||
|     if (!visible) { |  | ||||||
|       return ( |  | ||||||
|         <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} /> |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> |       <Base onClose={onClose}> | ||||||
|         <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> |         {visible && ( | ||||||
|           <div role='presentation' className='modal-root__overlay' onClick={onClose} /> |           <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> | ||||||
|           <div role='dialog' className='modal-root__container'> |             {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} | ||||||
|             {visible && ( |           </BundleContainer> | ||||||
|               <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> |         )} | ||||||
|                 {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} |       </Base> | ||||||
|               </BundleContainer> |  | ||||||
|             )} |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,7 +25,6 @@ function main() { | |||||||
|   const { getLocale } = require('../mastodon/locales'); |   const { getLocale } = require('../mastodon/locales'); | ||||||
|   const { localeData } = getLocale(); |   const { localeData } = getLocale(); | ||||||
|   const VideoContainer = require('../mastodon/containers/video_container').default; |   const VideoContainer = require('../mastodon/containers/video_container').default; | ||||||
|   const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default; |  | ||||||
|   const CardContainer = require('../mastodon/containers/card_container').default; |   const CardContainer = require('../mastodon/containers/card_container').default; | ||||||
|   const React = require('react'); |   const React = require('react'); | ||||||
|   const ReactDOM = require('react-dom'); |   const ReactDOM = require('react-dom'); | ||||||
| @@ -76,15 +75,20 @@ function main() { | |||||||
|       ReactDOM.render(<VideoContainer locale={locale} {...props} />, content); |       ReactDOM.render(<VideoContainer locale={locale} {...props} />, content); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => { |  | ||||||
|       const props = JSON.parse(content.getAttribute('data-props')); |  | ||||||
|       ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { |     [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { | ||||||
|       const props = JSON.parse(content.getAttribute('data-props')); |       const props = JSON.parse(content.getAttribute('data-props')); | ||||||
|       ReactDOM.render(<CardContainer locale={locale} {...props} />, content); |       ReactDOM.render(<CardContainer locale={locale} {...props} />, content); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]'); | ||||||
|  |  | ||||||
|  |     if (mediaGalleries.length > 0) { | ||||||
|  |       const MediaGalleriesContainer = require('../mastodon/containers/media_galleries_container').default; | ||||||
|  |       const content = document.createElement('div'); | ||||||
|  |  | ||||||
|  |       ReactDOM.render(<MediaGalleriesContainer locale={locale} galleries={mediaGalleries} />, content); | ||||||
|  |       document.body.appendChild(content); | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   delegate(document, '.webapp-btn', 'click', ({ target, button }) => { |   delegate(document, '.webapp-btn', 'click', ({ target, button }) => { | ||||||
|   | |||||||
| @@ -3375,13 +3375,14 @@ a.status-card { | |||||||
| } | } | ||||||
|  |  | ||||||
| .modal-root { | .modal-root { | ||||||
|  |   position: relative; | ||||||
|   transition: opacity 0.3s linear; |   transition: opacity 0.3s linear; | ||||||
|   will-change: opacity; |   will-change: opacity; | ||||||
|   z-index: 9999; |   z-index: 9999; | ||||||
| } | } | ||||||
|  |  | ||||||
| .modal-root__overlay { | .modal-root__overlay { | ||||||
|   position: absolute; |   position: fixed; | ||||||
|   top: 0; |   top: 0; | ||||||
|   left: 0; |   left: 0; | ||||||
|   right: 0; |   right: 0; | ||||||
| @@ -3390,7 +3391,7 @@ a.status-card { | |||||||
| } | } | ||||||
|  |  | ||||||
| .modal-root__container { | .modal-root__container { | ||||||
|   position: absolute; |   position: fixed; | ||||||
|   top: 0; |   top: 0; | ||||||
|   left: 0; |   left: 0; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   | |||||||
| @@ -60,6 +60,10 @@ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .media-gallery-standalone__body { | ||||||
|  |   overflow: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
| .account-header { | .account-header { | ||||||
|   width: 400px; |   width: 400px; | ||||||
|   margin: 0 auto; |   margin: 0 auto; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user