Merge branch 'develop' into audio-player
This commit is contained in:
commit
4e94c5d2ca
76 changed files with 392 additions and 344 deletions
|
@ -1,4 +1,3 @@
|
|||
NODE_ENV=development
|
||||
# BACKEND_URL="https://example.com"
|
||||
# PATRON_URL="https://patron.example.com"
|
||||
# PROXY_HTTPS_INSECURE=false
|
||||
|
|
|
@ -79,7 +79,7 @@ Try again.
|
|||
|
||||
You can also run Soapbox FE locally with a live production server as the backend.
|
||||
|
||||
> **Note:** Whether or not this works depends on your production server. It does not seem to work with Cloudflare.
|
||||
> **Note:** Whether or not this works depends on your production server. It does not seem to work with Cloudflare or VanwaNet.
|
||||
|
||||
To do so, just copy the env file:
|
||||
|
||||
|
@ -124,7 +124,7 @@ For https, be sure to also set `PROXY_HTTPS_INSECURE=true`.
|
|||
|
||||
Allows using an HTTPS backend if set to `true`.
|
||||
|
||||
This is needed if `BACKEND_URL` or `PATRON_URL` are set to an `https://` value.
|
||||
This is needed if `BACKEND_URL` is set to an `https://` value.
|
||||
[More info](https://stackoverflow.com/a/48624590/8811886).
|
||||
|
||||
**Default:** `false`
|
||||
|
|
|
@ -5,7 +5,7 @@ export const PATRON_FUNDING_FETCH_FAIL = 'PATRON_FUNDING_FETCH_FAIL';
|
|||
|
||||
export function fetchFunding() {
|
||||
return (dispatch, getState) => {
|
||||
api(getState).get('/patron/v1/funding').then(response => {
|
||||
api(getState).get('/api/patron/v1/instance').then(response => {
|
||||
dispatch(importFetchedFunding(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchFundingFail(error));
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import Rails from 'rails-ujs';
|
||||
|
||||
export function start() {
|
||||
require('font-awesome/css/font-awesome.css');
|
||||
require('fork-awesome/css/fork-awesome.css');
|
||||
require.context('../images/', true);
|
||||
|
||||
try {
|
||||
|
|
|
@ -2,30 +2,36 @@
|
|||
|
||||
exports[`<Avatar /> Autoplay renders an animated avatar 1`] = `
|
||||
<div
|
||||
className="account__avatar"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
className="account__avatar still-image"
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/animated/alice.gif)",
|
||||
"height": "100px",
|
||||
"width": "100px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
onLoad={[Function]}
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
||||
<div
|
||||
className="account__avatar"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
className="account__avatar still-image"
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/static/alice.jpg)",
|
||||
"height": "100px",
|
||||
"width": "100px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
onLoad={[Function]}
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -5,20 +5,24 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
|
|||
className="account__avatar-overlay"
|
||||
>
|
||||
<div
|
||||
className="account__avatar-overlay-base"
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/static/alice.jpg)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
className="account__avatar-overlay-base still-image"
|
||||
style={Object {}}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
onLoad={[Function]}
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="account__avatar-overlay-overlay"
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/static/eve.jpg)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
className="account__avatar-overlay-overlay still-image"
|
||||
style={Object {}}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
onLoad={[Function]}
|
||||
src="/animated/eve.gif"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { fromJS } from 'immutable';
|
||||
import { Avatar } from '../avatar';
|
||||
import { createComponent } from 'soapbox/test_helpers';
|
||||
import Avatar from '../avatar';
|
||||
|
||||
describe('<Avatar />', () => {
|
||||
const account = fromJS({
|
||||
|
@ -16,7 +16,7 @@ describe('<Avatar />', () => {
|
|||
|
||||
describe('Autoplay', () => {
|
||||
it('renders an animated avatar', () => {
|
||||
const component = renderer.create(<Avatar account={account} animate size={size} />);
|
||||
const component = createComponent(<Avatar account={account} animate size={size} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
@ -25,7 +25,7 @@ describe('<Avatar />', () => {
|
|||
|
||||
describe('Still', () => {
|
||||
it('renders a still avatar', () => {
|
||||
const component = renderer.create(<Avatar account={account} size={size} />);
|
||||
const component = createComponent(<Avatar account={account} size={size} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { fromJS } from 'immutable';
|
||||
import { AvatarOverlay } from '../avatar_overlay';
|
||||
import { createComponent } from 'soapbox/test_helpers';
|
||||
import AvatarOverlay from '../avatar_overlay';
|
||||
|
||||
describe('<AvatarOverlay', () => {
|
||||
const account = fromJS({
|
||||
|
@ -21,7 +21,7 @@ describe('<AvatarOverlay', () => {
|
|||
});
|
||||
|
||||
it('renders a overlay avatar', () => {
|
||||
const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
|
||||
const component = createComponent(<AvatarOverlay account={account} friend={friend} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
@ -153,10 +153,13 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
this.input.focus();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
static getDerivedStateFromProps(nextProps, state) {
|
||||
if (nextProps.suggestions && nextProps.suggestions.size > 0 && state.suggestionsHidden && state.focused) {
|
||||
return {
|
||||
suggestionsHidden: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setInput = (c) => {
|
||||
|
|
|
@ -159,10 +159,13 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
this.textarea.focus();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
static getDerivedStateFromProps(nextProps, state) {
|
||||
if (nextProps.suggestions && nextProps.suggestions.size > 0 && state.suggestionsHidden && state.focused) {
|
||||
return {
|
||||
suggestionsHidden: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setTextarea = (c) => {
|
||||
|
|
|
@ -1,54 +1,25 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import classNames from 'classnames';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
animate: getSettings(state).get('autoPlayGif'),
|
||||
});
|
||||
|
||||
export class Avatar extends React.PureComponent {
|
||||
export default class Avatar extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
size: PropTypes.number,
|
||||
style: PropTypes.object,
|
||||
inline: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
inline: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
hovering: false,
|
||||
};
|
||||
|
||||
handleMouseEnter = () => {
|
||||
if (this.props.animate) return;
|
||||
this.setState({ hovering: true });
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
if (this.props.animate) return;
|
||||
this.setState({ hovering: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, size, animate, inline } = this.props;
|
||||
const { account, size, inline } = this.props;
|
||||
if (!account) return null;
|
||||
const { hovering } = this.state;
|
||||
|
||||
const src = account.get('avatar');
|
||||
const staticSrc = account.get('avatar_static');
|
||||
|
||||
let className = 'account__avatar';
|
||||
|
||||
if (inline) {
|
||||
className = className + ' account__avatar-inline';
|
||||
}
|
||||
|
||||
// : TODO : remove inline and change all avatars to be sized using css
|
||||
const style = !size ? {} : {
|
||||
|
@ -56,22 +27,14 @@ export class Avatar extends React.PureComponent {
|
|||
height: `${size}px`,
|
||||
};
|
||||
|
||||
if (hovering || animate) {
|
||||
style.backgroundImage = `url(${src})`;
|
||||
} else {
|
||||
style.backgroundImage = `url(${staticSrc})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
<StillImage
|
||||
className={classNames('account__avatar', { 'account__avatar-inline': inline })}
|
||||
style={style}
|
||||
src={account.get('avatar')}
|
||||
alt=''
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Avatar);
|
||||
|
|
|
@ -1,24 +1,16 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
animate: getSettings(state).get('autoPlayGif'),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class AvatarComposite extends React.PureComponent {
|
||||
export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
animate: PropTypes.bool,
|
||||
size: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
renderItem(account, size, index) {
|
||||
const { animate } = this.props;
|
||||
|
||||
let width = 50;
|
||||
let height = 100;
|
||||
|
@ -76,12 +68,10 @@ class AvatarComposite extends React.PureComponent {
|
|||
bottom: bottom,
|
||||
width: `${width}%`,
|
||||
height: `${height}%`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={account.get('id')} style={style} />
|
||||
<StillImage key={account.get('id')} src={account.get('avatar')} style={style} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +1,23 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
animate: getSettings(state).get('autoPlayGif'),
|
||||
});
|
||||
|
||||
export class AvatarOverlay extends React.PureComponent {
|
||||
export default class AvatarOverlay extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
friend: ImmutablePropTypes.map.isRequired,
|
||||
animate: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { account, friend, animate } = this.props;
|
||||
|
||||
const baseStyle = {
|
||||
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
|
||||
};
|
||||
|
||||
const overlayStyle = {
|
||||
backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
|
||||
};
|
||||
const { account, friend } = this.props;
|
||||
|
||||
return (
|
||||
<div className='account__avatar-overlay'>
|
||||
<div className='account__avatar-overlay-base' style={baseStyle} />
|
||||
<div className='account__avatar-overlay-overlay' style={overlayStyle} />
|
||||
<StillImage src={account.get('avatar')} className='account__avatar-overlay-base' />
|
||||
<StillImage src={friend.get('avatar')} className='account__avatar-overlay-overlay' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AvatarOverlay);
|
||||
|
|
|
@ -143,7 +143,7 @@ class ColumnHeader extends React.PureComponent {
|
|||
</Link>
|
||||
|
||||
<Link to='/timeline/local' className={classNames('btn grouped', { 'active': 'local' === activeItem })}>
|
||||
<Icon id='site-icon' fixedWidth className='column-header__icon' />
|
||||
<Icon id='users' fixedWidth className='column-header__icon' />
|
||||
{siteTitle}
|
||||
</Link>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ export default class Icon extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { id, className, fixedWidth, ...other } = this.props;
|
||||
// Use the font awesome retweet icon, but change its alt
|
||||
// Use the Fork Awesome retweet icon, but change its alt
|
||||
// tag. There is a common adblocker rule which hides elements with
|
||||
// alt='retweet' unless the domain is twitter.com. This should
|
||||
// change what screenreaders call it as well.
|
||||
|
|
|
@ -12,6 +12,7 @@ import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maxi
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
|
@ -61,8 +62,7 @@ class Item extends React.PureComponent {
|
|||
|
||||
hoverToPlay() {
|
||||
const { attachment, autoPlayGif } = this.props;
|
||||
return !autoPlayGif &&
|
||||
(attachment.get('type') === 'gifv' || attachment.getIn(['pleroma', 'mime_type']) === 'image/gif');
|
||||
return !autoPlayGif && attachment.get('type') === 'gifv';
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
|
@ -73,7 +73,7 @@ class Item extends React.PureComponent {
|
|||
e.preventDefault();
|
||||
} else {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
if (!this.canvas && this.hoverToPlay()) {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.pause();
|
||||
e.target.currentTime = 0;
|
||||
}
|
||||
|
@ -113,23 +113,12 @@ class Item extends React.PureComponent {
|
|||
this.canvas = c;
|
||||
}
|
||||
|
||||
setImageRef = i => {
|
||||
this.image = i;
|
||||
}
|
||||
|
||||
handleImageLoad = () => {
|
||||
this.setState({ loaded: true });
|
||||
if (this.hoverToPlay()) {
|
||||
const image = this.image;
|
||||
const canvas = this.canvas;
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
canvas.getContext('2d').drawImage(image, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attachment, standalone, displayWidth, visible, dimensions, autoPlayGif } = this.props;
|
||||
const { attachment, standalone, visible, dimensions, autoPlayGif } = this.props;
|
||||
|
||||
let width = 100;
|
||||
let height = '100%';
|
||||
|
@ -163,39 +152,17 @@ class Item extends React.PureComponent {
|
|||
);
|
||||
} else if (attachment.get('type') === 'image') {
|
||||
const previewUrl = attachment.get('preview_url');
|
||||
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
||||
|
||||
const originalUrl = attachment.get('url');
|
||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||
|
||||
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
|
||||
|
||||
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
|
||||
const sizes = hasSize && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : null;
|
||||
|
||||
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
|
||||
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
thumbnail = (
|
||||
<a
|
||||
className={classNames('media-gallery__item-thumbnail', { 'media-gallery__item-thumbnail--play-on-hover': this.hoverToPlay() })}
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
src={previewUrl}
|
||||
srcSet={srcSet}
|
||||
sizes={sizes}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
ref={this.setImageRef}
|
||||
/>
|
||||
{this.hoverToPlay() && <canvas ref={this.setCanvasRef} style={{ objectPosition: `${x}% ${y}%` }} />}
|
||||
<StillImage src={previewUrl} alt={attachment.get('description')} />
|
||||
</a>
|
||||
);
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
|
@ -286,15 +253,21 @@ class MediaGallery extends React.PureComponent {
|
|||
state = {
|
||||
visible: this.props.visible !== undefined ? this.props.visible : (this.props.displayMedia !== 'hide_all' && !this.props.sensitive || this.props.displayMedia === 'show_all'),
|
||||
width: this.props.defaultWidth,
|
||||
media: this.props.media,
|
||||
displayMedia: this.props.displayMedia,
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { displayMedia } = this.props;
|
||||
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
|
||||
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
|
||||
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
||||
this.setState({ visible: nextProps.visible });
|
||||
static getDerivedStateFromProps(nextProps, state) {
|
||||
if (!is(nextProps.media, state.media) && nextProps.visible === undefined) {
|
||||
return {
|
||||
visible: state.displayMedia !== 'hide_all' && !nextProps.sensitive || state.displayMedia === 'show_all',
|
||||
};
|
||||
} else if (!is(nextProps.visible, state.visible) && nextProps.visible !== undefined) {
|
||||
return {
|
||||
visible: nextProps.visible,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
handleOpen = () => {
|
||||
|
|
|
@ -78,21 +78,18 @@ class ModalRoot extends React.PureComponent {
|
|||
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps, prevProps) {
|
||||
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 });
|
||||
}
|
||||
if (!nextProps.children && !!this.props.children) {
|
||||
this.activeElement = document.activeElement;
|
||||
this.activeElement.focus();
|
||||
this.activeElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!this.props.children && !!prevProps.children) {
|
||||
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ class RelativeTimestamp extends React.Component {
|
|||
this.state.now !== nextState.now;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (this.props.timestamp !== nextProps.timestamp) {
|
||||
this.setState({ now: Date.now() });
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ class RelativeTimestamp extends React.Component {
|
|||
this._scheduleNextUpdate(this.props, this.state);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
componentDidUpdate(nextProps, nextState) {
|
||||
this._scheduleNextUpdate(nextProps, nextState);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import { shortNumberFormat } from '../utils/numbers';
|
|||
import { isStaff } from '../utils/accounts';
|
||||
import { makeGetAccount } from '../selectors';
|
||||
import { logOut } from 'soapbox/actions/auth';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
followers: { id: 'account.followers', defaultMessage: 'Followers' },
|
||||
|
@ -39,11 +40,12 @@ const messages = defineMessages({
|
|||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const getAccount = makeGetAccount();
|
||||
const patron = state.getIn(['soapbox', 'extensions', 'patron'], ImmutableMap());
|
||||
|
||||
return {
|
||||
account: getAccount(state, me),
|
||||
sidebarOpen: state.get('sidebar').sidebarOpen,
|
||||
hasPatron: state.getIn(['soapbox', 'extensions', 'patron']),
|
||||
patronUrl: patron.get('enabled') && patron.get('baseUrl'),
|
||||
isStaff: isStaff(state.getIn(['accounts', me])),
|
||||
};
|
||||
};
|
||||
|
@ -75,7 +77,7 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { sidebarOpen, onClose, intl, account, onClickLogOut, hasPatron, isStaff } = this.props;
|
||||
const { sidebarOpen, onClose, intl, account, onClickLogOut, patronUrl, isStaff } = this.props;
|
||||
if (!account) return null;
|
||||
const acct = account.get('acct');
|
||||
|
||||
|
@ -127,11 +129,11 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon id='envelope' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.messages)}</span>
|
||||
</NavLink>
|
||||
{hasPatron ?
|
||||
<NavLink className='sidebar-menu-item' to='/donate' onClick={onClose}>
|
||||
{patronUrl ?
|
||||
<a className='sidebar-menu-item' href={patronUrl} onClick={onClose}>
|
||||
<Icon id='dollar' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
|
||||
</NavLink>
|
||||
</a>
|
||||
: ''}
|
||||
<NavLink className='sidebar-menu-item' to='/lists' onClick={onClose}>
|
||||
<Icon id='list' />
|
||||
|
|
63
app/soapbox/components/still_image.js
Normal file
63
app/soapbox/components/still_image.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class StillImage extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
alt: PropTypes.string,
|
||||
autoPlayGif: PropTypes.bool.isRequired,
|
||||
className: PropTypes.node,
|
||||
src: PropTypes.string.isRequired,
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
alt: '',
|
||||
className: '',
|
||||
style: {},
|
||||
}
|
||||
|
||||
hoverToPlay() {
|
||||
const { autoPlayGif, src } = this.props;
|
||||
return !autoPlayGif && (src.endsWith('.gif') || src.startsWith('blob:'));
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
}
|
||||
|
||||
setImageRef = i => {
|
||||
this.img = i;
|
||||
}
|
||||
|
||||
handleImageLoad = () => {
|
||||
if (this.hoverToPlay()) {
|
||||
const img = this.img;
|
||||
const canvas = this.canvas;
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { alt, className, src, style } = this.props;
|
||||
const hoverToPlay = this.hoverToPlay();
|
||||
|
||||
return (
|
||||
<div className={classNames(className, 'still-image', { 'still-image--play-on-hover': hoverToPlay })} style={style}>
|
||||
<img src={src} alt={alt} ref={this.setImageRef} onLoad={this.handleImageLoad} />
|
||||
{hoverToPlay && <canvas ref={this.setCanvasRef} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,7 +20,7 @@ class AboutPage extends ImmutablePureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.loadPageHtml();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { NavLink } from 'react-router-dom';
|
|||
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||
import ProfileInfoPanel from '../../ui/components/profile_info_panel';
|
||||
import { debounce } from 'lodash';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
|
@ -54,7 +54,6 @@ const mapStateToProps = state => {
|
|||
return {
|
||||
me,
|
||||
isStaff: isStaff(state.getIn(['accounts', me])),
|
||||
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
||||
version: parseVersion(state.getIn(['instance', 'version'])),
|
||||
};
|
||||
};
|
||||
|
@ -70,7 +69,6 @@ class Header extends ImmutablePureComponent {
|
|||
onBlock: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
autoPlayGif: PropTypes.bool,
|
||||
isStaff: PropTypes.bool.isRequired,
|
||||
version: PropTypes.object,
|
||||
};
|
||||
|
@ -91,7 +89,7 @@ class Header extends ImmutablePureComponent {
|
|||
return !location.pathname.match(/\/(followers|following|favorites|pins)\/?$/);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
|
@ -226,7 +224,7 @@ class Header extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { account, intl, username, me, autoPlayGif } = this.props;
|
||||
const { account, intl, username, me } = this.props;
|
||||
const { isSmallScreen } = this.state;
|
||||
|
||||
if (!account) {
|
||||
|
@ -252,8 +250,7 @@ class Header extends ImmutablePureComponent {
|
|||
const actionBtn = this.getActionBtn();
|
||||
const menu = this.makeMenu();
|
||||
|
||||
const headerImgSrc = autoPlayGif ? account.get('header') : account.get('header_static');
|
||||
const headerMissing = (headerImgSrc.indexOf('/headers/original/missing.png') > -1);
|
||||
const headerMissing = (account.get('header').indexOf('/headers/original/missing.png') > -1);
|
||||
|
||||
const avatarSize = isSmallScreen ? 90 : 200;
|
||||
|
||||
|
@ -264,7 +261,7 @@ class Header extends ImmutablePureComponent {
|
|||
{info}
|
||||
</div>
|
||||
|
||||
<img src={headerImgSrc} alt='' className='parallax' />
|
||||
<StillImage src={account.get('header')} alt='' className='parallax' />
|
||||
</div>
|
||||
|
||||
<div className='account__header__bar'>
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import { decode } from 'blurhash';
|
||||
import { isIOS } from 'soapbox/is_mobile';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
||||
|
@ -113,12 +114,10 @@ class MediaItem extends ImmutablePureComponent {
|
|||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
thumbnail = (
|
||||
<img
|
||||
<StillImage
|
||||
src={attachment.get('preview_url')}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
);
|
||||
} else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
|
||||
|
|
|
@ -97,7 +97,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId)) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(nextProps.accountId));
|
||||
|
|
|
@ -64,7 +64,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { params: { username }, accountId, withReplies, me } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
|
@ -81,7 +81,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
const { me } = nextProps;
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||
this.props.dispatch(fetchAccount(nextProps.accountId));
|
||||
|
|
|
@ -61,7 +61,7 @@ class CaptchaField extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.fetchCaptcha();
|
||||
this.startRefresh(); // Refresh periodically
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchBlocks());
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class ModifierPickerMenu extends React.PureComponent {
|
|||
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.active) {
|
||||
this.attachListeners();
|
||||
} else {
|
||||
|
|
|
@ -168,6 +168,18 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
placement: 'bottom',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||
];
|
||||
}
|
||||
|
||||
handleToggle = ({ target }) => {
|
||||
if (this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
|
@ -210,17 +222,6 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, intl } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
|
|
@ -33,7 +33,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchDomainBlocks());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { acctFull } from 'soapbox/utils/accounts';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const ProfilePreview = ({ account }) => (
|
||||
<div className='card h-card'>
|
||||
<a target='_blank' rel='noopener' href={account.get('url')}>
|
||||
<div className='card__img'>
|
||||
<img alt='' src={account.get('header')} />
|
||||
<StillImage alt='' src={account.get('header')} />
|
||||
</div>
|
||||
<div className='card__bar'>
|
||||
<div className='avatar'>
|
||||
<img alt='' className='u-photo' src={account.get('avatar')} width='48' height='48' />
|
||||
<StillImage alt='' className='u-photo' src={account.get('avatar')} width='48' height='48' />
|
||||
</div>
|
||||
<div className='display-name'>
|
||||
<span style={{ display: 'none' }}>{account.get('username')}</span>
|
||||
|
|
|
@ -117,18 +117,15 @@ class EditProfile extends ImmutablePureComponent {
|
|||
event.preventDefault();
|
||||
}
|
||||
|
||||
setInitialState = () => {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialState = this.props.account.withMutations(map => {
|
||||
map.merge(map.get('source'));
|
||||
map.delete('source');
|
||||
map.set('fields', normalizeFields(map.get('fields')));
|
||||
unescapeParams(map, ['display_name', 'note']);
|
||||
});
|
||||
this.setState(initialState.toObject());
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setInitialState();
|
||||
this.state = initialState.toObject();
|
||||
}
|
||||
|
||||
handleCheckboxChange = e => {
|
||||
|
|
|
@ -34,7 +34,7 @@ class Favourites extends ImmutablePureComponent {
|
|||
isMyAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFavouritedStatuses());
|
||||
}
|
||||
|
||||
|
|
|
@ -23,11 +23,11 @@ class Favourites extends ImmutablePureComponent {
|
|||
accountIds: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchFavourites(nextProps.params.statusId));
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class Filters extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFilters());
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFollowRequests());
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ class Followers extends ImmutablePureComponent {
|
|||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
|
@ -69,7 +69,7 @@ class Followers extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId)) {
|
||||
this.props.dispatch(fetchAccount(nextProps.accountId));
|
||||
this.props.dispatch(fetchFollowers(nextProps.accountId));
|
||||
|
|
|
@ -58,7 +58,7 @@ class Following extends ImmutablePureComponent {
|
|||
diffCount: PropTypes.number,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
|
@ -69,7 +69,7 @@ class Following extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId)) {
|
||||
this.props.dispatch(fetchAccount(nextProps.accountId));
|
||||
this.props.dispatch(fetchFollowing(nextProps.accountId));
|
||||
|
|
|
@ -49,7 +49,8 @@ class Create extends React.PureComponent {
|
|||
onCoverImageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.props.reset();
|
||||
}
|
||||
|
||||
|
|
|
@ -55,13 +55,14 @@ class Edit extends React.PureComponent {
|
|||
setUp: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount(nextProps) {
|
||||
constructor(nextProps) {
|
||||
super(nextProps);
|
||||
if (this.props.group) {
|
||||
this.props.setUp(this.props.group);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (!this.props.group && nextProps.group) {
|
||||
this.props.setUp(nextProps.group);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ class Groups extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchGroups(this.props.activeTab));
|
||||
}
|
||||
|
||||
|
|
|
@ -30,13 +30,13 @@ class GroupMembers extends ImmutablePureComponent {
|
|||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { params: { id } } = this.props;
|
||||
|
||||
this.props.dispatch(fetchMembers(id));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.props.dispatch(fetchMembers(nextProps.params.id));
|
||||
}
|
||||
|
|
|
@ -37,13 +37,13 @@ class GroupRemovedAccounts extends ImmutablePureComponent {
|
|||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { params: { id } } = this.props;
|
||||
|
||||
this.props.dispatch(fetchRemovedAccounts(id));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.props.dispatch(fetchRemovedAccounts(nextProps.params.id));
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ class HashtagTimeline extends React.PureComponent {
|
|||
dispatch(expandHashtagTimeline(id, { tags }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
const { dispatch, params } = this.props;
|
||||
const { id, tags } = nextProps.params;
|
||||
|
||||
|
|
|
@ -84,7 +84,8 @@ class Introduction extends React.PureComponent {
|
|||
currentIndex: 0,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.pages = [
|
||||
<FrameWelcome domain={this.props.domain} onNext={this.handleNext} />,
|
||||
<FrameFederation onNext={this.handleNext} />,
|
||||
|
|
|
@ -50,7 +50,7 @@ class ListTimeline extends React.PureComponent {
|
|||
this.handleDisconnect();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.handleDisconnect();
|
||||
this.handleConnect(nextProps.params.id);
|
||||
|
|
|
@ -42,7 +42,7 @@ class Lists extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchLists());
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class Mutes extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchMutes());
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class PinnedStatuses extends ImmutablePureComponent {
|
|||
isMyAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchPinnedStatuses());
|
||||
}
|
||||
|
||||
|
|
|
@ -36,12 +36,12 @@ class Reblogs extends ImmutablePureComponent {
|
|||
status: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchReblogs(nextProps.params.statusId));
|
||||
this.props.dispatch(fetchStatus(nextProps.params.statusId));
|
||||
|
|
|
@ -21,7 +21,7 @@ class Header extends ImmutablePureComponent {
|
|||
submittedValue: '',
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.submitted) {
|
||||
const submittedValue = nextProps.value;
|
||||
this.setState({ submittedValue });
|
||||
|
|
|
@ -75,7 +75,7 @@ export default class Card extends React.PureComponent {
|
|||
embedded: false,
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (!Immutable.is(this.props.card, nextProps.card)) {
|
||||
this.setState({ embedded: false });
|
||||
}
|
||||
|
|
|
@ -139,25 +139,11 @@ class Status extends ImmutablePureComponent {
|
|||
loadedStatusId: undefined,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this._scrolledIntoView = false;
|
||||
this.props.dispatch(fetchStatus(nextProps.params.statusId));
|
||||
}
|
||||
|
||||
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
|
||||
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleMediaVisibility = () => {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
}
|
||||
|
@ -404,7 +390,16 @@ class Status extends ImmutablePureComponent {
|
|||
this.node = c;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this._scrolledIntoView = false;
|
||||
this.props.dispatch(fetchStatus(nextProps.params.statusId));
|
||||
}
|
||||
|
||||
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
|
||||
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
|
||||
}
|
||||
|
||||
if (this._scrolledIntoView) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ class Bundle extends React.PureComponent {
|
|||
forceRender: false,
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.load(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.fetchComponent !== this.props.fetchComponent) {
|
||||
this.load(nextProps);
|
||||
}
|
||||
|
|
|
@ -34,11 +34,11 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||
dragging: false,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.updatePositionFromMedia(this.props.media);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (this.props.media.get('id') !== nextProps.media.get('id')) {
|
||||
this.updatePositionFromMedia(nextProps.media);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,16 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import ProgressBar from '../../../components/progress_bar';
|
||||
import { fetchFunding } from 'soapbox/actions/patron';
|
||||
|
||||
const moneyFormat = amount => (
|
||||
new Intl
|
||||
.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'usd',
|
||||
notation: 'compact',
|
||||
})
|
||||
.format(amount/100)
|
||||
);
|
||||
|
||||
class FundingPanel extends ImmutablePureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -12,21 +22,22 @@ class FundingPanel extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { funding } = this.props;
|
||||
const { funding, patronUrl } = this.props;
|
||||
|
||||
if (!funding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const amount = funding.getIn(['funding', 'amount']);
|
||||
const goal = funding.getIn(['goals', '0', 'amount']);
|
||||
const goal_text = funding.getIn(['goals', '0', 'text']);
|
||||
const goal_reached = funding.get('amount') >= goal;
|
||||
const goal_reached = amount >= goal;
|
||||
let ratio_text;
|
||||
|
||||
if (goal_reached) {
|
||||
ratio_text = <><strong>${Math.floor(goal/100)}</strong> per month<span className='funding-panel__reached'>— reached!</span></>;
|
||||
ratio_text = <><strong>{moneyFormat(goal)}</strong> per month<span className='funding-panel__reached'>— reached!</span></>;
|
||||
} else {
|
||||
ratio_text = <><strong>${Math.floor(funding.get('amount')/100)} out of ${Math.floor(goal/100)}</strong> per month</>;
|
||||
ratio_text = <><strong>{moneyFormat(amount)} out of {moneyFormat(goal)}</strong> per month</>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -41,11 +52,11 @@ class FundingPanel extends ImmutablePureComponent {
|
|||
<div className='funding-panel__ratio'>
|
||||
{ratio_text}
|
||||
</div>
|
||||
<ProgressBar progress={funding.get('amount')/goal} />
|
||||
<ProgressBar progress={amount/goal} />
|
||||
<div className='funding-panel__description'>
|
||||
{goal_text}
|
||||
</div>
|
||||
<a className='button' href='/donate'>Donate</a>
|
||||
{patronUrl && <a className='button' href={patronUrl}>Donate</a>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -56,6 +67,7 @@ class FundingPanel extends ImmutablePureComponent {
|
|||
const mapStateToProps = state => {
|
||||
return {
|
||||
funding: state.getIn(['patron', 'funding']),
|
||||
patronUrl: state.getIn(['soapbox', 'extensions', 'patron', 'baseUrl']),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ export default class ImageLoader extends React.PureComponent {
|
|||
this.loadImage(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (this.props.src !== nextProps.src) {
|
||||
this.loadImage(nextProps);
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ class ReportModal extends ImmutablePureComponent {
|
|||
this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (this.props.account !== nextProps.account && nextProps.account) {
|
||||
this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true }));
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import Avatar from 'soapbox/components/avatar';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
import { acctFull } from 'soapbox/utils/accounts';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
class UserPanel extends ImmutablePureComponent {
|
||||
|
||||
|
@ -20,7 +20,7 @@ class UserPanel extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { account, intl, domain, autoPlayGif } = this.props;
|
||||
const { account, intl, domain } = this.props;
|
||||
if (!account) return null;
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||
|
@ -30,7 +30,7 @@ class UserPanel extends ImmutablePureComponent {
|
|||
<div className='user-panel__container'>
|
||||
|
||||
<div className='user-panel__header'>
|
||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' />
|
||||
<StillImage src={account.get('header')} alt='' />
|
||||
</div>
|
||||
|
||||
<div className='user-panel__profile'>
|
||||
|
@ -91,7 +91,6 @@ const mapStateToProps = state => {
|
|||
|
||||
return {
|
||||
account: getAccount(state, me),
|
||||
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -167,7 +167,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
mobile: isMobile(window.innerWidth),
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
|
@ -394,9 +394,9 @@ class UI extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { me } = this.props;
|
||||
if (!me) return;
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||
|
||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||
|
@ -420,11 +420,6 @@ class UI extends React.PureComponent {
|
|||
|
||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { me } = this.props;
|
||||
if (!me) return;
|
||||
this.connectStreaming();
|
||||
}
|
||||
|
||||
|
|
|
@ -291,7 +291,7 @@ class Video extends React.PureComponent {
|
|||
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
||||
this.setState({ revealed: nextProps.visible });
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class GroupPage extends ImmutablePureComponent {
|
|||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { params: { id }, dispatch } = this.props;
|
||||
|
||||
dispatch(fetchGroup(id));
|
||||
|
|
|
@ -16,7 +16,7 @@ const mapStateToProps = state => {
|
|||
const me = state.get('me');
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
hasPatron: state.getIn(['soapbox', 'extensions', 'patron']),
|
||||
hasPatron: state.getIn(['soapbox', 'extensions', 'patron', 'enabled']),
|
||||
features: getFeatures(state.get('instance')),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
background: var(--background-color);
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
img {
|
||||
.still-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -61,7 +61,7 @@
|
|||
height: 48px;
|
||||
padding-top: 2px;
|
||||
|
||||
img {
|
||||
.still-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
@import 'components/navigation-bar';
|
||||
@import 'components/promo-panel';
|
||||
@import 'components/drawer';
|
||||
@import 'components/still-image';
|
||||
@import 'components/timeline-queue-header';
|
||||
@import 'components/badge';
|
||||
@import 'components/trends';
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
background: var(--accent-color--med);
|
||||
@media screen and (max-width: 895px) {height: 225px;}
|
||||
&--none {height: 125px;}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
|
@ -29,6 +28,23 @@
|
|||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.still-image--play-on-hover::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
color: var(--primary-text-color);
|
||||
background: var(--foreground-color);
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
|
@ -58,6 +74,23 @@
|
|||
background-size: 200px 200px;
|
||||
}
|
||||
|
||||
.still-image--play-on-hover::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
color: var(--primary-text-color);
|
||||
background: var(--foreground-color);
|
||||
bottom: 15%;
|
||||
left: 15%;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 8px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 895px) {
|
||||
top: -45px;
|
||||
left: 10px;
|
||||
|
|
|
@ -284,7 +284,7 @@
|
|||
.compose-form__upload-thumbnail {
|
||||
border-radius: 4px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
height: 140px;
|
||||
width: 100%;
|
||||
|
|
|
@ -36,47 +36,30 @@
|
|||
z-index: 1;
|
||||
|
||||
&,
|
||||
img,
|
||||
canvas {
|
||||
.still-image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&--play-on-hover {
|
||||
&::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
color: var(--primary-text-color);
|
||||
background: var(--foreground-color);
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
img,
|
||||
&:hover::before {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
visibility: visible;
|
||||
}
|
||||
.still-image--play-on-hover::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
color: var(--primary-text-color);
|
||||
background: var(--foreground-color);
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +73,23 @@
|
|||
z-index: 0;
|
||||
background: var(--background-color);
|
||||
|
||||
.still-image--play-on-hover::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
color: var(--primary-text-color);
|
||||
background: var(--foreground-color);
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
28
app/styles/components/still-image.scss
Normal file
28
app/styles/components/still-image.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
.still-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img,
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
&--play-on-hover {
|
||||
img {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:hover canvas {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,10 @@
|
|||
width: 100%;
|
||||
background: var(--brand-color--faint);
|
||||
|
||||
.still-image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
|
|
@ -159,8 +159,8 @@
|
|||
font-style: normal;
|
||||
}
|
||||
|
||||
.fa-site-icon::before {
|
||||
font-family: 'FontAwesome';
|
||||
.fa-users::before {
|
||||
font-family: 'ForkAwesome';
|
||||
content: '\f0c0';
|
||||
}
|
||||
|
||||
|
|
|
@ -739,7 +739,7 @@
|
|||
display: block;
|
||||
position: absolute;
|
||||
content: '\f00c';
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font: normal normal normal 14px/1 ForkAwesome;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"exif-js": "^2.3.0",
|
||||
"express": "^4.17.1",
|
||||
"file-loader": "^4.0.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"fork-awesome": "^1.1.7",
|
||||
"glob": "^7.1.1",
|
||||
"html-webpack-harddisk-plugin": "^1.0.1",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
|
|
|
@ -13,7 +13,10 @@
|
|||
}]
|
||||
},
|
||||
"extensions": {
|
||||
"patron": false
|
||||
"patron": {
|
||||
"enabled": false,
|
||||
"baseUrl": "https://patron.example.com"
|
||||
}
|
||||
},
|
||||
"defaultSettings": {
|
||||
"autoPlayGif": false,
|
||||
|
|
|
@ -9,7 +9,6 @@ const { settings, output } = require('./configuration');
|
|||
const watchOptions = {};
|
||||
|
||||
const backendUrl = process.env.BACKEND_URL || 'http://localhost:4000';
|
||||
const patronUrl = process.env.PATRON_URL || 'http://localhost:5000';
|
||||
const secureProxy = !(process.env.PROXY_HTTPS_INSECURE === 'true');
|
||||
|
||||
const backendEndpoints = [
|
||||
|
@ -22,7 +21,6 @@ const backendEndpoints = [
|
|||
'/.well-known/webfinger',
|
||||
'/static',
|
||||
'/emoji',
|
||||
'/patron',
|
||||
];
|
||||
|
||||
const makeProxyConfig = () => {
|
||||
|
@ -33,10 +31,6 @@ const makeProxyConfig = () => {
|
|||
secure: secureProxy,
|
||||
};
|
||||
});
|
||||
proxyConfig['/patron'] = {
|
||||
target: patronUrl,
|
||||
secure: secureProxy,
|
||||
};
|
||||
return proxyConfig;
|
||||
};
|
||||
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -5166,11 +5166,6 @@ follow-redirects@^1.0.0:
|
|||
dependencies:
|
||||
debug "=3.1.0"
|
||||
|
||||
font-awesome@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
|
||||
integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
|
@ -5181,6 +5176,11 @@ forever-agent@~0.6.1:
|
|||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||
|
||||
fork-awesome@^1.1.7:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/fork-awesome/-/fork-awesome-1.1.7.tgz#1427da1cac3d1713046ee88427e5fcecb9501d21"
|
||||
integrity sha512-IHI7XCSXrKfUIWslse8c/PaaVDT1oBaYge+ju40ihL2ooiQeBpTr4wvIXhgTd2NuhntlvX+M5jYHAPTzNlmv0g==
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
|
|
Loading…
Reference in a new issue