]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/components/media_gallery.js
In detail status view, display attachment uncropped if there's only one (#5054)
[mastodon.git] / app / javascript / mastodon / components / media_gallery.js
1 import React from 'react';
2 import ImmutablePropTypes from 'react-immutable-proptypes';
3 import PropTypes from 'prop-types';
4 import IconButton from './icon_button';
5 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
6 import { isIOS } from '../is_mobile';
7 import classNames from 'classnames';
8 import sizeMe from 'react-sizeme';
9
10 const messages = defineMessages({
11 toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
12 });
13
14 class Item extends React.PureComponent {
15
16 static contextTypes = {
17 router: PropTypes.object,
18 };
19
20 static propTypes = {
21 attachment: ImmutablePropTypes.map.isRequired,
22 standalone: PropTypes.bool,
23 index: PropTypes.number.isRequired,
24 size: PropTypes.number.isRequired,
25 onClick: PropTypes.func.isRequired,
26 autoPlayGif: PropTypes.bool,
27 };
28
29 static defaultProps = {
30 autoPlayGif: false,
31 standalone: false,
32 index: 0,
33 size: 1,
34 };
35
36 handleMouseEnter = (e) => {
37 if (this.hoverToPlay()) {
38 e.target.play();
39 }
40 }
41
42 handleMouseLeave = (e) => {
43 if (this.hoverToPlay()) {
44 e.target.pause();
45 e.target.currentTime = 0;
46 }
47 }
48
49 hoverToPlay () {
50 const { attachment, autoPlayGif } = this.props;
51 return !autoPlayGif && attachment.get('type') === 'gifv';
52 }
53
54 handleClick = (e) => {
55 const { index, onClick } = this.props;
56
57 if (this.context.router && e.button === 0) {
58 e.preventDefault();
59 onClick(index);
60 }
61
62 e.stopPropagation();
63 }
64
65 render () {
66 const { attachment, index, size, standalone } = this.props;
67
68 let width = 50;
69 let height = 100;
70 let top = 'auto';
71 let left = 'auto';
72 let bottom = 'auto';
73 let right = 'auto';
74
75 if (size === 1) {
76 width = 100;
77 }
78
79 if (size === 4 || (size === 3 && index > 0)) {
80 height = 50;
81 }
82
83 if (size === 2) {
84 if (index === 0) {
85 right = '2px';
86 } else {
87 left = '2px';
88 }
89 } else if (size === 3) {
90 if (index === 0) {
91 right = '2px';
92 } else if (index > 0) {
93 left = '2px';
94 }
95
96 if (index === 1) {
97 bottom = '2px';
98 } else if (index > 1) {
99 top = '2px';
100 }
101 } else if (size === 4) {
102 if (index === 0 || index === 2) {
103 right = '2px';
104 }
105
106 if (index === 1 || index === 3) {
107 left = '2px';
108 }
109
110 if (index < 2) {
111 bottom = '2px';
112 } else {
113 top = '2px';
114 }
115 }
116
117 let thumbnail = '';
118
119 if (attachment.get('type') === 'image') {
120 const previewUrl = attachment.get('preview_url');
121 const previewWidth = attachment.getIn(['meta', 'small', 'width']);
122
123 const originalUrl = attachment.get('url');
124 const originalWidth = attachment.getIn(['meta', 'original', 'width']);
125
126 const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
127
128 const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
129 const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
130
131 thumbnail = (
132 <a
133 className='media-gallery__item-thumbnail'
134 href={attachment.get('remote_url') || originalUrl}
135 onClick={this.handleClick}
136 target='_blank'
137 >
138 <img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' />
139 </a>
140 );
141 } else if (attachment.get('type') === 'gifv') {
142 const autoPlay = !isIOS() && this.props.autoPlayGif;
143
144 thumbnail = (
145 <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
146 <video
147 className='media-gallery__item-gifv-thumbnail'
148 role='application'
149 src={attachment.get('url')}
150 onClick={this.handleClick}
151 onMouseEnter={this.handleMouseEnter}
152 onMouseLeave={this.handleMouseLeave}
153 autoPlay={autoPlay}
154 loop
155 muted
156 />
157
158 <span className='media-gallery__gifv__label'>GIF</span>
159 </div>
160 );
161 }
162
163 const style = standalone ? {} : { left, top, right, bottom, width: `${width}%`, height: `${height}%` };
164
165 return (
166 <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={style}>
167 {thumbnail}
168 </div>
169 );
170 }
171
172 }
173
174 @injectIntl
175 @sizeMe({})
176 export default class MediaGallery extends React.PureComponent {
177
178 static propTypes = {
179 sensitive: PropTypes.bool,
180 standalone: PropTypes.bool,
181 media: ImmutablePropTypes.list.isRequired,
182 size: PropTypes.object,
183 height: PropTypes.number.isRequired,
184 onOpenMedia: PropTypes.func.isRequired,
185 intl: PropTypes.object.isRequired,
186 autoPlayGif: PropTypes.bool,
187 };
188
189 static defaultProps = {
190 autoPlayGif: false,
191 standalone: false,
192 };
193
194 state = {
195 visible: !this.props.sensitive,
196 };
197
198 componentWillReceiveProps (nextProps) {
199 if (nextProps.sensitive !== this.props.sensitive) {
200 this.setState({ visible: !nextProps.sensitive });
201 }
202 }
203
204 handleOpen = () => {
205 this.setState({ visible: !this.state.visible });
206 }
207
208 handleClick = (index) => {
209 this.props.onOpenMedia(this.props.media, index);
210 }
211
212 render () {
213 const { media, intl, sensitive, height, standalone, size } = this.props;
214
215 let children;
216
217 const standaloneEligible = standalone && size.width && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
218 const style = {};
219
220 if (standaloneEligible) {
221 style.height = size.width / media.getIn([0, 'meta', 'small', 'aspect']);
222 } else {
223 style.height = height;
224 }
225
226 if (!this.state.visible) {
227 let warning;
228
229 if (sensitive) {
230 warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
231 } else {
232 warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
233 }
234
235 children = (
236 <button className='media-spoiler' onClick={this.handleOpen} style={style}>
237 <span className='media-spoiler__warning'>{warning}</span>
238 <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
239 </button>
240 );
241 } else {
242 const size = media.take(4).size;
243
244 if (standaloneEligible) {
245 children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} autoPlayGif={this.props.autoPlayGif} />;
246 } else {
247 children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
248 }
249 }
250
251 return (
252 <div className='media-gallery' style={style}>
253 <div className={classNames('spoiler-button', { 'spoiler-button--visible': this.state.visible })}>
254 <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
255 </div>
256
257 {children}
258 </div>
259 );
260 }
261
262 }
This page took 0.208275 seconds and 4 git commands to generate.