mirror of https://github.com/vector-im/riot-web
				
				
				
			Factor out all shared logic between MStickerBody and MImageBody
The benefits of this: - One code path for determining spinner/placeholder and it's position for loading images/stickers. This includes spinner used in e2e decryption of images. - Very small definition for MStickerBody, only overriding the minimal differences is has from MImageBody. The disadvantages: - Slightly more complicated MImageBody, but hopefully not less readable.pull/21833/head
							parent
							
								
									015093b371
								
							
						
					
					
						commit
						836dc8b0ef
					
				|  | @ -37,11 +37,12 @@ limitations under the License. | |||
| } | ||||
| 
 | ||||
| .mx_MImageBody_thumbnail_spinner { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     width: 100%; | ||||
|     position: absolute; | ||||
|     left: 50%; | ||||
|     top: 50%; | ||||
| } | ||||
| 
 | ||||
| .mx_MImageBody_thumbnail_spinner img { | ||||
|     margin: auto; | ||||
| // Inner img and TintableSvg should be centered around 0, 0 | ||||
| .mx_MImageBody_thumbnail_spinner > * { | ||||
|     transform: translate(-50%, -50%); | ||||
| } | ||||
|  |  | |||
|  | @ -14,33 +14,11 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| .mx_MStickerBody { | ||||
|   display: block; | ||||
|   margin-right: 34px; | ||||
|   min-height: 110px; | ||||
|   padding: 20px 0; | ||||
| .mx_MStickerBody_wrapper { | ||||
|     padding: 20px 0px; | ||||
| } | ||||
| 
 | ||||
| .mx_MStickerBody_image_container { | ||||
|   display: inline-block; | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .mx_MStickerBody_image { | ||||
|   max-width: 100%; | ||||
|   opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .mx_MStickerBody_image_visible { | ||||
|   opacity: 1; | ||||
| } | ||||
| 
 | ||||
| .mx_MStickerBody_placeholder { | ||||
|   position: absolute; | ||||
|   opacity: 1; | ||||
| } | ||||
| 
 | ||||
| .mx_MStickerBody_placeholder_invisible { | ||||
|   transition: 500ms; | ||||
|   opacity: 0; | ||||
| .mx_MStickerBody_tooltip { | ||||
|     position: absolute; | ||||
|     top: 50%; | ||||
| } | ||||
|  |  | |||
|  | @ -62,6 +62,8 @@ export default class extends React.Component { | |||
|             decryptedBlob: null, | ||||
|             error: null, | ||||
|             imgError: false, | ||||
|             imgLoaded: false, | ||||
|             hover: false, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|  | @ -116,6 +118,8 @@ export default class extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     onImageEnter(e) { | ||||
|         this.setState({ hover: true }); | ||||
| 
 | ||||
|         if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { | ||||
|             return; | ||||
|         } | ||||
|  | @ -124,6 +128,8 @@ export default class extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     onImageLeave(e) { | ||||
|         this.setState({ hover: false }); | ||||
| 
 | ||||
|         if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { | ||||
|             return; | ||||
|         } | ||||
|  | @ -139,6 +145,7 @@ export default class extends React.Component { | |||
| 
 | ||||
|     onImageLoad() { | ||||
|         this.props.onWidgetLoad(); | ||||
|         this.setState({ imgLoaded: true }); | ||||
|     } | ||||
| 
 | ||||
|     _getContentUrl() { | ||||
|  | @ -237,14 +244,22 @@ export default class extends React.Component { | |||
|         const maxWidth = content.info.w * maxHeight / content.info.h; | ||||
| 
 | ||||
|         let img = null; | ||||
|         let placeholder = null; | ||||
| 
 | ||||
|         // e2e image hasn't been decrypted yet
 | ||||
|         if (content.file !== undefined && this.state.decryptedUrl === null) { | ||||
|             img = <div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image"> | ||||
|                 <img src="img/spinner.gif" alt={content.body} width="32" height="32" /> | ||||
|             </div>; | ||||
|         } else if (thumbUrl && !this.state.imgError) { | ||||
|             placeholder = <img src="img/spinner.gif" alt={content.body} width="32" height="32" />; | ||||
|         } else if (!this.state.imgLoaded) { | ||||
|             // Deliberately, getSpinner is left unimplemented here, MStickerBody overides
 | ||||
|             placeholder = this.getPlaceholder(); | ||||
|         } | ||||
| 
 | ||||
|         const showPlaceholder = Boolean(placeholder); | ||||
| 
 | ||||
|         if (thumbUrl && !this.state.imgError) { | ||||
|             // Restrict the width of the thumbnail here, otherwise it will fill the container
 | ||||
|             // which has the same width as the timeline
 | ||||
|             // mx_MImageBody_thumbnail resizes img to exactly container size
 | ||||
|             img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image" | ||||
|                 style={{ "max-width": maxWidth + "px" }} | ||||
|                 alt={content.body} | ||||
|  | @ -253,23 +268,54 @@ export default class extends React.Component { | |||
|                 onMouseEnter={this.onImageEnter} | ||||
|                 onMouseLeave={this.onImageLeave} />; | ||||
|         } | ||||
|         const thumbnail = img ? | ||||
|             <a href={contentUrl} onClick={this.onClick}> | ||||
|                 <div className="mx_MImageBody_thumbnail_container" style={{ "max-height": maxHeight + "px" }} > | ||||
|                     { /* Calculate aspect ratio, using %padding will size _container correctly */ } | ||||
|                     <div style={{ paddingBottom: (100 * content.info.h / content.info.w) + '%' }}></div> | ||||
| 
 | ||||
|                     { /* mx_MImageBody_thumbnail resizes img to exactly container size */ } | ||||
|         const thumbnail = ( | ||||
|             <div className="mx_MImageBody_thumbnail_container" style={{ "max-height": maxHeight + "px" }} > | ||||
|                 { /* Calculate aspect ratio, using %padding will size _container correctly */ } | ||||
|                 <div style={{ paddingBottom: (100 * content.info.h / content.info.w) + '%' }}></div> | ||||
| 
 | ||||
|                 <div className="mx_MImageBody_thumbnail" style={{ | ||||
|                     "display": showPlaceholder ? undefined : 'none', | ||||
|                     // Constrain width here so that spinner appears central to the loaded thumbnail
 | ||||
|                     "max-width": content.info.w + "px", | ||||
|                 }}> | ||||
|                     <div className="mx_MImageBody_thumbnail_spinner"> | ||||
|                         { placeholder } | ||||
|                     </div> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <div style={{display: !showPlaceholder ? undefined : 'none'}}> | ||||
|                     { img } | ||||
|                 </div> | ||||
|             </a> : null; | ||||
| 
 | ||||
|         return ( | ||||
|             <span className="mx_MImageBody" ref="body"> | ||||
|                 { thumbnail } | ||||
|                 <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} /> | ||||
|             </span> | ||||
|       ); | ||||
|                 { this.state.hover && this.getTooltip() } | ||||
|             </div> | ||||
|         ); | ||||
| 
 | ||||
|         return this.wrapImage(contentUrl, thumbnail); | ||||
|     } | ||||
| 
 | ||||
|     // Overidden by MStickerBody
 | ||||
|     wrapImage(contentUrl, children) { | ||||
|         return <a href={contentUrl} onClick={this.onClick}> | ||||
|             {children} | ||||
|         </a>; | ||||
|     } | ||||
| 
 | ||||
|     // Overidden by MStickerBody
 | ||||
|     getPlaceholder() { | ||||
|         // MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     // Overidden by MStickerBody
 | ||||
|     getTooltip() { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     // Overidden by MStickerBody
 | ||||
|     getFileBody() { | ||||
|         return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />; | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|  | @ -284,7 +330,6 @@ export default class extends React.Component { | |||
|             ); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const contentUrl = this._getContentUrl(); | ||||
|         let thumbUrl; | ||||
|         if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { | ||||
|  | @ -293,6 +338,12 @@ export default class extends React.Component { | |||
|           thumbUrl = this._getThumbUrl(); | ||||
|         } | ||||
| 
 | ||||
|         return this._messageContent(contentUrl, thumbUrl, content); | ||||
|         const thumbnail = this._messageContent(contentUrl, thumbUrl, content); | ||||
|         const fileBody = this.getFileBody(); | ||||
| 
 | ||||
|         return <span className="mx_MImageBody" ref="body"> | ||||
|             { thumbnail } | ||||
|             { fileBody } | ||||
|         </span>; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -18,141 +18,39 @@ limitations under the License. | |||
| 
 | ||||
| import MImageBody from './MImageBody'; | ||||
| import sdk from '../../../index'; | ||||
| import TintableSVG from '../elements/TintableSvg'; | ||||
| 
 | ||||
| export default class MStickerBody extends MImageBody { | ||||
|     displayName: 'MStickerBody' | ||||
| 
 | ||||
|     constructor(props) { | ||||
|       super(props); | ||||
| 
 | ||||
|       this._onMouseEnter = this._onMouseEnter.bind(this); | ||||
|       this._onMouseLeave = this._onMouseLeave.bind(this); | ||||
|       this._onImageLoad = this._onImageLoad.bind(this); | ||||
|     } | ||||
| 
 | ||||
|     _onMouseEnter() { | ||||
|         this.setState({showTooltip: true}); | ||||
|     } | ||||
| 
 | ||||
|     _onMouseLeave() { | ||||
|         this.setState({showTooltip: false}); | ||||
|     } | ||||
| 
 | ||||
|     _onImageLoad() { | ||||
|         this.setState({ | ||||
|             placeholderClasses: 'mx_MStickerBody_placeholder_invisible', | ||||
|         }); | ||||
|         const hidePlaceholderTimer = setTimeout(() => { | ||||
|             this.setState({ | ||||
|                 placeholderVisible: false, | ||||
|                 imageClasses: 'mx_MStickerBody_image_visible', | ||||
|             }); | ||||
|         }, 500); | ||||
|         this.setState({hidePlaceholderTimer}); | ||||
|         if (this.props.onWidgetLoad) { | ||||
|             this.props.onWidgetLoad(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _afterComponentDidMount() { | ||||
|         if (this.refs.image.complete) { | ||||
|             // Image already loaded
 | ||||
|             this.setState({ | ||||
|                 placeholderVisible: false, | ||||
|                 placeholderClasses: '.mx_MStickerBody_placeholder_invisible', | ||||
|                 imageClasses: 'mx_MStickerBody_image_visible', | ||||
|             }); | ||||
|         } else { | ||||
|             // Image not already loaded
 | ||||
|             this.setState({ | ||||
|                 placeholderVisible: true, | ||||
|                 placeholderClasses: '', | ||||
|                 imageClasses: '', | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _afterComponentWillUnmount() { | ||||
|         if (this.state.hidePlaceholderTimer) { | ||||
|             clearTimeout(this.state.hidePlaceholderTimer); | ||||
|             this.setState({hidePlaceholderTimer: null}); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _messageContent(contentUrl, thumbUrl, content) { | ||||
|         let tooltip; | ||||
|         const tooltipBody = ( | ||||
|             this.props.mxEvent && | ||||
|             this.props.mxEvent.getContent() && | ||||
|             this.props.mxEvent.getContent().body) ? | ||||
|             this.props.mxEvent.getContent().body : null; | ||||
|         if (this.state.showTooltip && tooltipBody) { | ||||
|             const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); | ||||
|             tooltip = <RoomTooltip | ||||
|                 className='mx_RoleButton_tooltip' | ||||
|                 label={tooltipBody} />; | ||||
|         } | ||||
| 
 | ||||
|         const gutterSize = 0; | ||||
|         let placeholderSize = 75; | ||||
|         let placeholderFixupHeight = '100px'; | ||||
|         let placeholderTop = 0; | ||||
|         let placeholderLeft = 0; | ||||
| 
 | ||||
|         if (content.info) { | ||||
|             placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px'; | ||||
|             placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px'; | ||||
|             placeholderFixupHeight = content.info.h + 'px'; | ||||
|         } | ||||
| 
 | ||||
|         // The pixel size of sticker images is generally larger than their intended display
 | ||||
|         // size so they render at native reolution on HiDPI displays. We therefore need to
 | ||||
|         // explicity set the size so they render at the intended size.
 | ||||
|         const imageStyle = { | ||||
|             height: content.info.h, | ||||
|             // leave the browser the calculate the width automatically
 | ||||
|         }; | ||||
| 
 | ||||
|         placeholderSize = placeholderSize + 'px'; | ||||
| 
 | ||||
|         // Body 'ref' required by MImageBody
 | ||||
|         return ( | ||||
|             <span className='mx_MStickerBody' ref='body' | ||||
|                 style={{ | ||||
|                     height: placeholderFixupHeight, | ||||
|                 }}> | ||||
|                 <div className={'mx_MStickerBody_image_container'}> | ||||
|                   { this.state.placeholderVisible && | ||||
|                     <div | ||||
|                         className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses} | ||||
|                         style={{ | ||||
|                             top: placeholderTop, | ||||
|                             left: placeholderLeft, | ||||
|                         }} | ||||
|                     > | ||||
|                         <TintableSVG | ||||
|                             src={'img/icons-show-stickers.svg'} | ||||
|                             width={placeholderSize} | ||||
|                             height={placeholderSize} /> | ||||
|                     </div> } | ||||
|                     <img | ||||
|                         className={'mx_MStickerBody_image ' + this.state.imageClasses} | ||||
|                         src={contentUrl} | ||||
|                         style={imageStyle} | ||||
|                         ref='image' | ||||
|                         alt={content.body} | ||||
|                         onLoad={this._onImageLoad} | ||||
|                         onMouseEnter={this._onMouseEnter} | ||||
|                         onMouseLeave={this._onMouseLeave} | ||||
|                     /> | ||||
|                     { tooltip } | ||||
|                 </div> | ||||
|             </span> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     // Empty to prevent default behaviour of MImageBody
 | ||||
|     onClick() { | ||||
|     } | ||||
| 
 | ||||
|     // MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
 | ||||
|     // which is added by mx_MStickerBody_wrapper
 | ||||
|     wrapImage(contentUrl, children) { | ||||
|         return <div className="mx_MStickerBody_wrapper"> { children } </div>; | ||||
|     } | ||||
| 
 | ||||
|     // Placeholder to show in place of the sticker image if
 | ||||
|     // img onLoad hasn't fired yet.
 | ||||
|     getPlaceholder() { | ||||
|         const TintableSVG = sdk.getComponent('elements.TintableSvg'); | ||||
|         return <TintableSVG src="img/icons-show-stickers.svg" width="75" height="75" />; | ||||
|     } | ||||
| 
 | ||||
|     // Tooltip to show on mouse over
 | ||||
|     getTooltip() { | ||||
|         const content = this.props.mxEvent && this.props.mxEvent.getContent(); | ||||
| 
 | ||||
|         if (!content || !content.body || !content.info || !content.info.w) return null; | ||||
| 
 | ||||
|         const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); | ||||
|         return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip"> | ||||
|             <RoomTooltip label={content.body} /> | ||||
|         </div>; | ||||
|     } | ||||
| 
 | ||||
|     // Don't show "Download this_file.png ..."
 | ||||
|     getFileBody() { | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Luke Barnard
						Luke Barnard