Merge remote-tracking branch 'origin' into a11y--

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2021-09-14 14:10:23 +02:00
commit 6152ca40a7
54 changed files with 701 additions and 262 deletions

View file

@ -2,6 +2,9 @@ import loadPolyfills from './soapbox/load_polyfills';
require.context('./images/', true); require.context('./images/', true);
// Load stylesheet
require('./styles/application.scss');
loadPolyfills().then(() => { loadPolyfills().then(() => {
require('./soapbox/main').default(); require('./soapbox/main').default();
}).catch(e => { }).catch(e => {

View file

@ -471,8 +471,6 @@ export function unsubscribeAccountFail(error) {
export function fetchFollowers(id) { export function fetchFollowers(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchFollowersRequest(id)); dispatch(fetchFollowersRequest(id));
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => { api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
@ -561,8 +559,6 @@ export function expandFollowersFail(id, error) {
export function fetchFollowing(id) { export function fetchFollowing(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchFollowingRequest(id)); dispatch(fetchFollowingRequest(id));
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => { api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {

View file

@ -10,6 +10,14 @@ export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_RE
export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS'; export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL'; export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL';
export const ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST';
export const ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS';
export const ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL';
export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST';
export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS';
export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL';
export function fetchFavouritedStatuses() { export function fetchFavouritedStatuses() {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
@ -96,3 +104,96 @@ export function expandFavouritedStatusesFail(error) {
error, error,
}; };
} }
export function fetchAccountFavouritedStatuses(accountId) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
if (getState().getIn(['status_lists', `favourites:${accountId}`, 'isLoading'])) {
return;
}
dispatch(fetchAccountFavouritedStatusesRequest(accountId));
api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchAccountFavouritedStatusesFail(accountId, error));
});
};
}
export function fetchAccountFavouritedStatusesRequest(accountId) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
accountId,
skipLoading: true,
};
}
export function fetchAccountFavouritedStatusesSuccess(accountId, statuses, next) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
accountId,
statuses,
next,
skipLoading: true,
};
}
export function fetchAccountFavouritedStatusesFail(accountId, error) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
accountId,
error,
skipLoading: true,
};
}
export function expandAccountFavouritedStatuses(accountId) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const url = getState().getIn(['status_lists', `favourites:${accountId}`, 'next'], null);
if (url === null || getState().getIn(['status_lists', `favourites:${accountId}`, 'isLoading'])) {
return;
}
dispatch(expandAccountFavouritedStatusesRequest(accountId));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandAccountFavouritedStatusesFail(accountId, error));
});
};
}
export function expandAccountFavouritedStatusesRequest(accountId) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
accountId,
};
}
export function expandAccountFavouritedStatusesSuccess(accountId, statuses, next) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
accountId,
statuses,
next,
};
}
export function expandAccountFavouritedStatusesFail(accountId, error) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
accountId,
error,
};
}

View file

@ -50,6 +50,7 @@ export const makeDefaultConfig = features => {
limit: 1, limit: 1,
}), }),
aboutPages: ImmutableMap(), aboutPages: ImmutableMap(),
authenticatedProfile: true,
}); });
}; };

View file

@ -11,6 +11,7 @@ const {
BACKEND_URL, BACKEND_URL,
FE_SUBDIRECTORY, FE_SUBDIRECTORY,
FE_BUILD_DIR, FE_BUILD_DIR,
SENTRY_DSN,
} = process.env; } = process.env;
const sanitizeURL = url => { const sanitizeURL = url => {
@ -38,4 +39,5 @@ module.exports = sanitize({
BACKEND_URL: sanitizeURL(BACKEND_URL), BACKEND_URL: sanitizeURL(BACKEND_URL),
FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY), FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY),
FE_BUILD_DIR: sanitizePath(FE_BUILD_DIR) || 'static', FE_BUILD_DIR: sanitizePath(FE_BUILD_DIR) || 'static',
SENTRY_DSN,
}); });

View file

@ -20,7 +20,7 @@ exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
<img <img
alt="💙" alt="💙"
className="emojione" className="emojione"
src="/emoji/1f499.svg" src="/packs/emoji/1f499.svg"
/> />
:foobar: :foobar:
</div> </div>

View file

@ -15,7 +15,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji" className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={ dangerouslySetInnerHTML={
Object { Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"👍\\" title=\\":+1:\\" src=\\"/emoji/1f44d.svg\\" />", "__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"👍\\" title=\\":+1:\\" src=\\"/packs/emoji/1f44d.svg\\" />",
} }
} }
onClick={[Function]} onClick={[Function]}
@ -26,7 +26,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji" className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={ dangerouslySetInnerHTML={
Object { Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"❤\\" title=\\":heart:\\" src=\\"/emoji/2764.svg\\" />", "__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"❤\\" title=\\":heart:\\" src=\\"/packs/emoji/2764.svg\\" />",
} }
} }
onClick={[Function]} onClick={[Function]}
@ -37,7 +37,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji" className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={ dangerouslySetInnerHTML={
Object { Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😆\\" title=\\":laughing:\\" src=\\"/emoji/1f606.svg\\" />", "__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😆\\" title=\\":laughing:\\" src=\\"/packs/emoji/1f606.svg\\" />",
} }
} }
onClick={[Function]} onClick={[Function]}
@ -48,7 +48,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji" className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={ dangerouslySetInnerHTML={
Object { Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😮\\" title=\\":open_mouth:\\" src=\\"/emoji/1f62e.svg\\" />", "__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😮\\" title=\\":open_mouth:\\" src=\\"/packs/emoji/1f62e.svg\\" />",
} }
} }
onClick={[Function]} onClick={[Function]}
@ -59,7 +59,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji" className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={ dangerouslySetInnerHTML={
Object { Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😢\\" title=\\":cry:\\" src=\\"/emoji/1f622.svg\\" />", "__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😢\\" title=\\":cry:\\" src=\\"/packs/emoji/1f622.svg\\" />",
} }
} }
onClick={[Function]} onClick={[Function]}
@ -70,7 +70,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji" className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={ dangerouslySetInnerHTML={
Object { Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😩\\" title=\\":weary:\\" src=\\"/emoji/1f629.svg\\" />", "__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😩\\" title=\\":weary:\\" src=\\"/packs/emoji/1f629.svg\\" />",
} }
} }
onClick={[Function]} onClick={[Function]}

View file

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
import { join } from 'path'; import { joinPublicPath } from 'soapbox/utils/static';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
export default class AutosuggestEmoji extends React.PureComponent { export default class AutosuggestEmoji extends React.PureComponent {
@ -23,7 +22,7 @@ export default class AutosuggestEmoji extends React.PureComponent {
return null; return null;
} }
url = join(FE_SUBDIRECTORY, 'emoji', `${mapping.filename}.svg`); url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`);
} }
return ( return (

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Bowser from 'bowser'; import { captureException } from 'soapbox/monitoring';
export default class ErrorBoundary extends React.PureComponent { export default class ErrorBoundary extends React.PureComponent {
@ -15,11 +15,21 @@ export default class ErrorBoundary extends React.PureComponent {
} }
componentDidCatch(error, info) { componentDidCatch(error, info) {
captureException(error);
this.setState({ this.setState({
hasError: true, hasError: true,
error, error,
componentStack: info && info.componentStack, componentStack: info && info.componentStack,
}); });
import(/* webpackChunkName: "error" */'bowser')
.then(({ default: Bowser }) => {
this.setState({
browser: Bowser.getParser(window.navigator.userAgent),
});
})
.catch(() => {});
} }
setTextareaRef = c => { setTextareaRef = c => {
@ -46,9 +56,7 @@ export default class ErrorBoundary extends React.PureComponent {
} }
render() { render() {
const browser = Bowser.getParser(window.navigator.userAgent); const { browser, hasError } = this.state;
const { hasError } = this.state;
if (!hasError) { if (!hasError) {
return this.props.children; return this.props.children;
@ -72,9 +80,9 @@ export default class ErrorBoundary extends React.PureComponent {
onClick={this.handleCopy} onClick={this.handleCopy}
readOnly readOnly
/>} />}
<p className='error-boundary__browser'> {browser && <p className='error-boundary__browser'>
{browser.getBrowserName()} {browser.getBrowserVersion()} {browser.getBrowserName()} {browser.getBrowserVersion()}
</p> </p>}
<p className='help-text'> <p className='help-text'>
<FormattedMessage <FormattedMessage
id='alert.unexpected.help_text' id='alert.unexpected.help_text'

View file

@ -265,15 +265,16 @@ class StatusContent extends React.PureComponent {
} }
if (status.get('poll')) { if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} />); output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
} }
return output; return output;
} else { } else {
const output = [ const output = [
<div <div
tabIndex='0'
ref={this.setRef} ref={this.setRef}
tabIndex='0'
key='content'
className={classnames('status__content', { className={classnames('status__content', {
'status__content--big': onlyEmoji, 'status__content--big': onlyEmoji,
})} })}
@ -284,7 +285,7 @@ class StatusContent extends React.PureComponent {
]; ];
if (status.get('poll')) { if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} />); output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
} }
return output; return output;

View file

@ -17,7 +17,6 @@ import {
isRemote, isRemote,
getDomain, getDomain,
} from 'soapbox/utils/accounts'; } from 'soapbox/utils/accounts';
import { parseVersion } from 'soapbox/utils/features';
import classNames from 'classnames'; import classNames from 'classnames';
import Avatar from 'soapbox/components/avatar'; import Avatar from 'soapbox/components/avatar';
import { shortNumberFormat } from 'soapbox/utils/numbers'; import { shortNumberFormat } from 'soapbox/utils/numbers';
@ -30,6 +29,7 @@ import ActionButton from 'soapbox/features/ui/components/action_button';
import SubscriptionButton from 'soapbox/features/ui/components/subscription_button'; import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
import { openModal } from 'soapbox/actions/modal'; import { openModal } from 'soapbox/actions/modal';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@ -72,11 +72,13 @@ const messages = defineMessages({
const mapStateToProps = state => { const mapStateToProps = state => {
const me = state.get('me'); const me = state.get('me');
const account = state.getIn(['accounts', me]); const account = state.getIn(['accounts', me]);
const instance = state.get('instance');
const features = getFeatures(instance);
return { return {
me, me,
meAccount: account, meAccount: account,
version: parseVersion(state.getIn(['instance', 'version'])), features,
}; };
}; };
@ -90,7 +92,7 @@ class Header extends ImmutablePureComponent {
identity_props: ImmutablePropTypes.list, identity_props: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
username: PropTypes.string, username: PropTypes.string,
version: PropTypes.object, features: PropTypes.object,
}; };
state = { state = {
@ -156,7 +158,7 @@ class Header extends ImmutablePureComponent {
} }
makeMenu() { makeMenu() {
const { account, intl, me, meAccount, version } = this.props; const { account, intl, me, meAccount, features } = this.props;
const menu = []; const menu = [];
@ -196,7 +198,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList }); menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
// menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); // menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push(null); menu.push(null);
} else if (version.software === 'Pleroma') { } else if (features.unrestrictedLists) {
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList }); menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
} }
@ -285,7 +287,7 @@ class Header extends ImmutablePureComponent {
} }
render() { render() {
const { account, intl, username, me } = this.props; const { account, intl, username, me, features } = this.props;
const { isSmallScreen } = this.state; const { isSmallScreen } = this.state;
if (!account) { if (!account) {
@ -327,9 +329,9 @@ class Header extends ImmutablePureComponent {
<StillImage src={account.get('header')} alt='' className='parallax' /> <StillImage src={account.get('header')} alt='' className='parallax' />
</a>} </a>}
<div className='account__header__subscribe'> {features.accountSubscriptions && <div className='account__header__subscribe'>
<SubscriptionButton account={account} /> <SubscriptionButton account={account} />
</div> </div>}
</div> </div>
<div className='account__header__bar'> <div className='account__header__bar'>
@ -356,16 +358,13 @@ class Header extends ImmutablePureComponent {
<span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span> <span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
</NavLink>} </NavLink>}
{ {(ownAccount || !account.getIn(['pleroma', 'hide_favorites'], true)) && <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/favorites`}>
ownAccount &&
<div>
<NavLink
exact activeClassName='active' to={`/@${account.get('acct')}/favorites`}
>
{ /* : TODO : shortNumberFormat(account.get('favourite_count')) */ } { /* : TODO : shortNumberFormat(account.get('favourite_count')) */ }
<span></span> <span></span>
<span><FormattedMessage id='navigation_bar.favourites' defaultMessage='Likes' /></span> <span><FormattedMessage id='navigation_bar.favourites' defaultMessage='Likes' /></span>
</NavLink> </NavLink>}
{ownAccount &&
<NavLink <NavLink
exact activeClassName='active' to={`/@${account.get('acct')}/pins`} exact activeClassName='active' to={`/@${account.get('acct')}/pins`}
> >
@ -373,7 +372,6 @@ class Header extends ImmutablePureComponent {
<span></span> <span></span>
<span><FormattedMessage id='navigation_bar.pins' defaultMessage='Pins' /></span> <span><FormattedMessage id='navigation_bar.pins' defaultMessage='Pins' /></span>
</NavLink> </NavLink>
</div>
} }
</div> </div>

View file

@ -7,8 +7,7 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import { buildCustomEmojis } from '../../emoji/emoji'; import { buildCustomEmojis } from '../../emoji/emoji';
import { join } from 'path'; import { joinPublicPath } from 'soapbox/utils/static';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
const messages = defineMessages({ const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@ -29,7 +28,7 @@ const messages = defineMessages({
let EmojiPicker, Emoji; // load asynchronously let EmojiPicker, Emoji; // load asynchronously
const backgroundImageFn = () => join(FE_SUBDIRECTORY, 'emoji', 'sheet_13.png'); const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const categoriesSort = [ const categoriesSort = [
@ -358,8 +357,8 @@ class EmojiPickerDropdown extends React.PureComponent {
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
<img <img
className={classNames('emojione', { 'pulse-loading': active && loading })} className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂' alt='😂'
src={join(FE_SUBDIRECTORY, 'emoji', '1f602.svg')} src={joinPublicPath('packs/emoji/1f602.svg')}
/> />
</div> </div>

View file

@ -4,10 +4,10 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { setSchedule, removeSchedule } from '../../../actions/compose';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import IconButton from 'soapbox/components/icon_button'; import IconButton from 'soapbox/components/icon_button';
import { removeSchedule } from 'soapbox/actions/compose';
import classNames from 'classnames'; import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
@ -15,11 +15,22 @@ const messages = defineMessages({
remove: { id: 'schedule.remove', defaultMessage: 'Remove schedule' }, remove: { id: 'schedule.remove', defaultMessage: 'Remove schedule' },
}); });
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = state => ({
active: state.getIn(['compose', 'schedule']) ? true : false,
scheduledAt: state.getIn(['compose', 'schedule']), scheduledAt: state.getIn(['compose', 'schedule']),
}); });
export default @connect(mapStateToProps) const mapDispatchToProps = dispatch => ({
onSchedule(date) {
dispatch(setSchedule(date));
},
onRemoveSchedule(date) {
dispatch(removeSchedule());
},
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl @injectIntl
class ScheduleForm extends React.Component { class ScheduleForm extends React.Component {
@ -27,6 +38,7 @@ class ScheduleForm extends React.Component {
scheduledAt: PropTypes.instanceOf(Date), scheduledAt: PropTypes.instanceOf(Date),
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onSchedule: PropTypes.func.isRequired, onSchedule: PropTypes.func.isRequired,
onRemoveSchedule: PropTypes.func.isRequired,
dispatch: PropTypes.func, dispatch: PropTypes.func,
active: PropTypes.bool, active: PropTypes.bool,
}; };
@ -60,7 +72,7 @@ class ScheduleForm extends React.Component {
} }
handleRemove = e => { handleRemove = e => {
this.props.dispatch(removeSchedule()); this.props.onRemoveSchedule();
e.preventDefault(); e.preventDefault();
} }

View file

@ -1,16 +1,15 @@
import { connect } from 'react-redux'; import React from 'react';
import ScheduleForm from '../components/schedule_form'; import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { setSchedule } from '../../../actions/compose'; import { ScheduleForm } from 'soapbox/features/ui/util/async-components';
const mapStateToProps = state => ({ export default class ScheduleFormContainer extends React.PureComponent {
schedule: state.getIn(['compose', 'schedule']),
active: state.getIn(['compose', 'schedule']) ? true : false,
});
const mapDispatchToProps = dispatch => ({ render() {
onSchedule(date) { return (
dispatch(setSchedule(date)); <BundleContainer fetchComponent={ScheduleForm}>
}, {Component => <Component {...this.props} />}
}); </BundleContainer>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleForm); }

View file

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import CoinDB from '../utils/coin_db'; import CoinDB from '../utils/coin_db';
import { getCoinIcon } from '../utils/coin_icons'; import CryptoIcon from './crypto_icon';
import { openModal } from 'soapbox/actions/modal'; import { openModal } from 'soapbox/actions/modal';
import { CopyableInput } from 'soapbox/features/forms'; import { CopyableInput } from 'soapbox/features/forms';
import { getExplorerUrl } from '../utils/block_explorer'; import { getExplorerUrl } from '../utils/block_explorer';
@ -31,9 +31,11 @@ class CryptoAddress extends ImmutablePureComponent {
return ( return (
<div className='crypto-address'> <div className='crypto-address'>
<div className='crypto-address__head'> <div className='crypto-address__head'>
<div className='crypto-address__icon'> <CryptoIcon
<img src={getCoinIcon(ticker)} alt={title} /> className='crypto-address__icon'
</div> ticker={ticker}
title={title}
/>
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div> <div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
<div className='crypto-address__actions'> <div className='crypto-address__actions'>
<a href='' onClick={this.handleModalClick}> <a href='' onClick={this.handleModalClick}>

View file

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class CryptoIcon extends React.PureComponent {
static propTypes = {
ticker: PropTypes.string.isRequired,
title: PropTypes.string,
className: PropTypes.string,
}
render() {
const { ticker, title, className } = this.props;
return (
<div className={classNames('crypto-icon', className)}>
<img
src={require(`cryptocurrency-icons/svg/color/${ticker.toLowerCase()}.svg`)}
alt={title || ticker}
/>
</div>
);
}
}

View file

@ -1,16 +1,14 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import QRCode from 'qrcode.react'; import QRCode from 'qrcode.react';
import CoinDB from '../utils/coin_db'; import CoinDB from '../utils/coin_db';
import { getCoinIcon } from '../utils/coin_icons'; import CryptoIcon from './crypto_icon';
import { CopyableInput } from 'soapbox/features/forms'; import { CopyableInput } from 'soapbox/features/forms';
import { getExplorerUrl } from '../utils/block_explorer'; import { getExplorerUrl } from '../utils/block_explorer';
export default @connect() export default class DetailedCryptoAddress extends ImmutablePureComponent {
class DetailedCryptoAddress extends ImmutablePureComponent {
static propTypes = { static propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
@ -26,9 +24,11 @@ class DetailedCryptoAddress extends ImmutablePureComponent {
return ( return (
<div className='crypto-address'> <div className='crypto-address'>
<div className='crypto-address__head'> <div className='crypto-address__head'>
<div className='crypto-address__icon'> <CryptoIcon
<img src={getCoinIcon(ticker)} alt={title} /> className='crypto-address__icon'
</div> ticker={ticker}
title={title}
/>
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div> <div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
<div className='crypto-address__actions'> <div className='crypto-address__actions'>
{explorerUrl && <a href={explorerUrl} target='_blank'> {explorerUrl && <a href={explorerUrl} target='_blank'>

View file

@ -1,20 +0,0 @@
// Does some trickery to import all the icons into the project
// See: https://stackoverflow.com/questions/42118296/dynamically-import-images-from-a-directory-using-webpack
const icons = {};
function importAll(r) {
const pathRegex = /\.\/(.*)\.svg/i;
r.keys().forEach((key) => {
const ticker = pathRegex.exec(key)[1];
return icons[ticker] = r(key).default;
});
}
importAll(require.context('cryptocurrency-icons/svg/color/', true, /\.svg$/));
export default icons;
// For getting the icon
export const getCoinIcon = ticker => icons[ticker] || icons.generic || null;

View file

@ -22,23 +22,23 @@ describe('emoji', () => {
it('does unicode', () => { it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual( expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />'); '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
expect(emojify('👨‍👩‍👧‍👧')).toEqual( expect(emojify('👨‍👩‍👧‍👧')).toEqual(
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />'); '<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/packs/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />'); expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466.svg" />');
expect(emojify('\u2757')).toEqual( expect(emojify('\u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" />');
}); });
it('does multiple unicode', () => { it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual( expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" />');
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual( expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" />');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual( expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />'); '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" />');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual( expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar'); 'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" /> bar');
}); });
it('ignores unicode inside of tags', () => { it('ignores unicode inside of tags', () => {
@ -46,16 +46,16 @@ describe('emoji', () => {
}); });
it('does multiple emoji properly (issue 5188)', () => { it('does multiple emoji properly (issue 5188)', () => {
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />'); expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg" />');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />'); expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg" />');
}); });
it('does an emoji that has no shortcode', () => { it('does an emoji that has no shortcode', () => {
expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg" />'); expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/packs/emoji/1f441-200d-1f5e8.svg" />');
}); });
it('does an emoji whose filename is irregular', () => { it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />'); expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/packs/emoji/2199.svg" />');
}); });
it('avoid emojifying on invisible text', () => { it('avoid emojifying on invisible text', () => {
@ -67,16 +67,16 @@ describe('emoji', () => {
it('avoid emojifying on invisible text with nested tags', () => { it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇')) expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />'); .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇')) expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />'); .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<br/>😴</span>😇')) expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />'); .toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
}); });
it('skips the textual presentation VS15 character', () => { it('skips the textual presentation VS15 character', () => {
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15 expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734.svg" />'); .toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/packs/emoji/2734.svg" />');
}); });
}); });
}); });

View file

@ -1,7 +1,6 @@
import unicodeMapping from './emoji_unicode_mapping_light'; import unicodeMapping from './emoji_unicode_mapping_light';
import Trie from 'substring-trie'; import Trie from 'substring-trie';
import { join } from 'path'; import { joinPublicPath } from 'soapbox/utils/static';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
const trie = new Trie(Object.keys(unicodeMapping)); const trie = new Trie(Object.keys(unicodeMapping));
@ -62,7 +61,8 @@ const emojify = (str, customEmojis = {}, autoplay = false) => {
} else { // matched to unicode emoji } else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match]; const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : ''; const title = shortCode ? `:${shortCode}:` : '';
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${join(FE_SUBDIRECTORY, 'emoji', `${filename}.svg`)}" />`; const src = joinPublicPath(`packs/emoji/${filename}.svg`);
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${src}" />`;
rend = i + match.length; rend = i + match.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it. // If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend) === 65038) { if (str.codePointAt(rend) === 65038) {

View file

@ -2,24 +2,56 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites'; import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from '../../actions/favourites';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import { injectIntl, FormattedMessage } from 'react-intl'; import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import MissingIndicator from 'soapbox/components/missing_indicator'; import MissingIndicator from 'soapbox/components/missing_indicator';
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
import LoadingIndicator from '../../components/loading_indicator';
const mapStateToProps = (state, { params }) => { const mapStateToProps = (state, { params }) => {
const username = params.username || ''; const username = params.username || '';
const me = state.get('me'); const me = state.get('me');
const meUsername = state.getIn(['accounts', me, 'username']); const meUsername = state.getIn(['accounts', me, 'username'], '');
const isMyAccount = (username.toLowerCase() === meUsername.toLowerCase());
if (isMyAccount) {
return { return {
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()), isMyAccount,
statusIds: state.getIn(['status_lists', 'favourites', 'items']), statusIds: state.getIn(['status_lists', 'favourites', 'items']),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
}; };
}
const accounts = state.getIn(['accounts']);
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
let accountId = -1;
if (accountFetchError) {
accountId = null;
} else {
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
accountId = account ? account.getIn(['id'], null) : -1;
}
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
const unavailable = (me === accountId) ? false : isBlocked;
return {
isMyAccount,
accountId,
unavailable,
username,
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['status_lists', `favourites:${accountId}`, 'items'], []),
isLoading: state.getIn(['status_lists', `favourites:${accountId}`, 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', `favourites:${accountId}`, 'next']),
};
}; };
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -36,17 +68,43 @@ class Favourites extends ImmutablePureComponent {
}; };
componentDidMount() { componentDidMount() {
const { accountId, isMyAccount, username } = this.props;
if (isMyAccount)
this.props.dispatch(fetchFavouritedStatuses()); this.props.dispatch(fetchFavouritedStatuses());
else {
if (accountId && accountId !== -1) {
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(fetchAccountFavouritedStatuses(accountId));
} else {
this.props.dispatch(fetchAccountByUsername(username));
}
}
}
componentDidUpdate(prevProps) {
const { accountId, isMyAccount } = this.props;
if (!isMyAccount && accountId && accountId !== -1 && (accountId !== prevProps.accountId && accountId)) {
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(fetchAccountFavouritedStatuses(accountId));
}
} }
handleLoadMore = debounce(() => { handleLoadMore = debounce(() => {
const { accountId, isMyAccount } = this.props;
if (isMyAccount) {
this.props.dispatch(expandFavouritedStatuses()); this.props.dispatch(expandFavouritedStatuses());
} else {
this.props.dispatch(expandAccountFavouritedStatuses(accountId));
}
}, 300, { leading: true }) }, 300, { leading: true })
render() { render() {
const { statusIds, hasMore, isLoading, isMyAccount } = this.props; const { statusIds, isLoading, hasMore, isMyAccount, isAccount, accountId, unavailable } = this.props;
if (!isMyAccount) { if (!isMyAccount && !isAccount && accountId !== -1) {
return ( return (
<Column> <Column>
<MissingIndicator /> <MissingIndicator />
@ -54,7 +112,27 @@ class Favourites extends ImmutablePureComponent {
); );
} }
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any liked posts yet. When you like one, it will show up here." />; if (accountId === -1) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
if (unavailable) {
return (
<Column>
<div className='empty-column-indicator'>
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
</div>
</Column>
);
}
const emptyMessage = isMyAccount
? <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any liked posts yet. When you like one, it will show up here." />
: <FormattedMessage id='empty_column.account_favourited_statuses' defaultMessage="This user doesn't have any liked posts yet." />;
return ( return (
<Column> <Column>

View file

@ -12,7 +12,7 @@ import MissingIndicator from 'soapbox/components/missing_indicator';
const mapStateToProps = (state, { params }) => { const mapStateToProps = (state, { params }) => {
const username = params.username || ''; const username = params.username || '';
const me = state.get('me'); const me = state.get('me');
const meUsername = state.getIn(['accounts', me, 'username']); const meUsername = state.getIn(['accounts', me, 'username'], '');
return { return {
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()), isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
statusIds: state.getIn(['status_lists', 'pins', 'items']), statusIds: state.getIn(['status_lists', 'pins', 'items']),

View file

@ -73,7 +73,7 @@ class Reactions extends ImmutablePureComponent {
render() { render() {
const { params, reactions, accounts, status } = this.props; const { params, reactions, accounts, status } = this.props;
const { username, statusId, reaction } = params; const { username, statusId } = params;
const back = `/@${username}/posts/${statusId}`; const back = `/@${username}/posts/${statusId}`;
@ -95,7 +95,6 @@ class Reactions extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />; const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
console.log(params.reaction);
return ( return (
<Column back={back}> <Column back={back}>
{ {

View file

@ -51,6 +51,8 @@ const messages = defineMessages({
displayFqnLabel: { id: 'soapbox_config.display_fqn_label', defaultMessage: 'Display domain (eg @user@domain) for local accounts.' }, displayFqnLabel: { id: 'soapbox_config.display_fqn_label', defaultMessage: 'Display domain (eg @user@domain) for local accounts.' },
greentextLabel: { id: 'soapbox_config.greentext_label', defaultMessage: 'Enable greentext support' }, greentextLabel: { id: 'soapbox_config.greentext_label', defaultMessage: 'Enable greentext support' },
promoPanelIconsLink: { id: 'soapbox_config.hints.promo_panel_icons.link', defaultMessage: 'Soapbox Icons List' }, promoPanelIconsLink: { id: 'soapbox_config.hints.promo_panel_icons.link', defaultMessage: 'Soapbox Icons List' },
authenticatedProfileLabel: { id: 'soapbox_config.authenticated_profile_label', defaultMessage: 'Profiles require authentication' },
authenticatedProfileHint: { id: 'soapbox_config.authenticated_profile_hint', defaultMessage: 'Users must be logged-in to view replies and media on user profiles.' },
}); });
const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
@ -279,6 +281,13 @@ class SoapboxConfig extends ImmutablePureComponent {
checked={soapbox.get('greentext') === true} checked={soapbox.get('greentext') === true}
onChange={this.handleChange(['greentext'], (e) => e.target.checked)} onChange={this.handleChange(['greentext'], (e) => e.target.checked)}
/> />
<Checkbox
name='authenticatedProfile'
label={intl.formatMessage(messages.authenticatedProfileLabel)}
hint={intl.formatMessage(messages.authenticatedProfileHint)}
checked={soapbox.get('authenticatedProfile') === true}
onChange={this.handleChange(['authenticatedProfile'], (e) => e.target.checked)}
/>
</FieldsGroup> </FieldsGroup>
<FieldsGroup> <FieldsGroup>
<div className='input with_block_label popup'> <div className='input with_block_label popup'>

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Immutable from 'immutable'; import { is, fromJS } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode'; import punycode from 'punycode';
import classnames from 'classnames'; import classnames from 'classnames';
@ -77,7 +77,7 @@ export default class Card extends React.PureComponent {
}; };
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (!Immutable.is(prevProps.card, this.props.card)) { if (!is(prevProps.card, this.props.card)) {
this.setState({ embedded: false }); this.setState({ embedded: false });
} }
} }
@ -86,7 +86,7 @@ export default class Card extends React.PureComponent {
const { card, onOpenMedia } = this.props; const { card, onOpenMedia } = this.props;
onOpenMedia( onOpenMedia(
Immutable.fromJS([ fromJS([
{ {
type: 'image', type: 'image',
url: card.get('embed_url'), url: card.get('embed_url'),

View file

@ -1,4 +1,4 @@
import Immutable from 'immutable'; import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -71,11 +71,11 @@ const makeMapStateToProps = () => {
(_, { id }) => id, (_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']), state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => { ], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.OrderedSet(); let ancestorsIds = ImmutableOrderedSet();
let id = statusId; let id = statusId;
while (id) { while (id) {
ancestorsIds = Immutable.OrderedSet([id]).union(ancestorsIds); ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
id = inReplyTos.get(id); id = inReplyTos.get(id);
} }
@ -86,7 +86,7 @@ const makeMapStateToProps = () => {
(_, { id }) => id, (_, { id }) => id,
state => state.getIn(['contexts', 'replies']), state => state.getIn(['contexts', 'replies']),
], (statusId, contextReplies) => { ], (statusId, contextReplies) => {
let descendantsIds = Immutable.OrderedSet(); let descendantsIds = ImmutableOrderedSet();
const ids = [statusId]; const ids = [statusId];
while (ids.length > 0) { while (ids.length > 0) {
@ -109,8 +109,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId }); const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List(); let ancestorsIds = ImmutableOrderedSet();
let descendantsIds = Immutable.List(); let descendantsIds = ImmutableOrderedSet();
if (status) { if (status) {
ancestorsIds = getAncestorsIds(state, { id: state.getIn(['contexts', 'inReplyTos', status.get('id')]) }); ancestorsIds = getAncestorsIds(state, { id: state.getIn(['contexts', 'inReplyTos', status.get('id')]) });
@ -146,8 +146,8 @@ class Status extends ImmutablePureComponent {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list, ancestorsIds: ImmutablePropTypes.orderedSet,
descendantsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
domain: PropTypes.string, domain: PropTypes.string,

View file

@ -14,13 +14,13 @@ import FocalPointModal from './focal_point_modal';
import HotkeysModal from './hotkeys_modal'; import HotkeysModal from './hotkeys_modal';
import ComposeModal from './compose_modal'; import ComposeModal from './compose_modal';
import UnauthorizedModal from './unauthorized_modal'; import UnauthorizedModal from './unauthorized_modal';
import CryptoDonateModal from './crypto_donate_modal';
import EditFederationModal from './edit_federation_modal'; import EditFederationModal from './edit_federation_modal';
import { import {
MuteModal, MuteModal,
ReportModal, ReportModal,
EmbedModal, EmbedModal,
CryptoDonateModal,
ListEditor, ListEditor,
ListAdder, ListAdder,
} from '../../../features/ui/util/async-components'; } from '../../../features/ui/util/async-components';
@ -41,7 +41,7 @@ const MODAL_COMPONENTS = {
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }), 'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
'COMPOSE': () => Promise.resolve({ default: ComposeModal }), 'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }), 'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
'CRYPTO_DONATE': () => Promise.resolve({ default: CryptoDonateModal }), 'CRYPTO_DONATE': CryptoDonateModal,
'EDIT_FEDERATION': () => Promise.resolve({ default: EditFederationModal }), 'EDIT_FEDERATION': () => Promise.resolve({ default: EditFederationModal }),
}; };

View file

@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import VerificationBadge from 'soapbox/components/verification_badge'; import VerificationBadge from 'soapbox/components/verification_badge';
@ -13,7 +14,7 @@ import { List as ImmutableList } from 'immutable';
import { getAcct, isAdmin, isModerator, isLocal } from 'soapbox/utils/accounts'; import { getAcct, isAdmin, isModerator, isLocal } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state'; import { displayFqn } from 'soapbox/utils/state';
import classNames from 'classnames'; import classNames from 'classnames';
import CryptoAddress from 'soapbox/features/crypto_donate/components/crypto_address'; import { CryptoAddress } from 'soapbox/features/ui/util/async-components';
const TICKER_REGEX = /\$([a-zA-Z]*)/i; const TICKER_REGEX = /\$([a-zA-Z]*)/i;
@ -143,7 +144,15 @@ class ProfileInfoPanel extends ImmutablePureComponent {
{fields.map((pair, i) => {fields.map((pair, i) =>
isTicker(pair.get('name', '')) ? ( isTicker(pair.get('name', '')) ? (
<CryptoAddress key={i} ticker={getTicker(pair.get('name')).toLowerCase()} address={pair.get('value_plain')} /> <BundleContainer fetchComponent={CryptoAddress}>
{Component => (
<Component
key={i}
ticker={getTicker(pair.get('name')).toLowerCase()}
address={pair.get('value_plain')}
/>
)}
</BundleContainer>
) : ( ) : (
<dl className='profile-info-panel-content__fields__item' key={i}> <dl className='profile-info-panel-content__fields__item' key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />

View file

@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Switch, withRouter } from 'react-router-dom'; import { Switch, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
import NotificationsContainer from './containers/notifications_container'; import NotificationsContainer from './containers/notifications_container';
import LoadingBarContainer from './containers/loading_bar_container'; import LoadingBarContainer from './containers/loading_bar_container';
@ -44,6 +45,7 @@ import ProfileHoverCard from 'soapbox/components/profile_hover_card';
import { getAccessToken } from 'soapbox/utils/auth'; import { getAccessToken } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features'; import { getFeatures } from 'soapbox/utils/features';
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis'; import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { import {
Status, Status,
@ -121,6 +123,7 @@ const mapStateToProps = state => {
const me = state.get('me'); const me = state.get('me');
const account = state.getIn(['accounts', me]); const account = state.getIn(['accounts', me]);
const instance = state.get('instance'); const instance = state.get('instance');
const soapbox = getSoapboxConfig(state);
return { return {
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
@ -129,6 +132,7 @@ const mapStateToProps = state => {
me, me,
account, account,
features: getFeatures(instance), features: getFeatures(instance),
soapbox,
}; };
}; };
@ -166,6 +170,7 @@ class SwitchingColumnsArea extends React.PureComponent {
children: PropTypes.node, children: PropTypes.node,
location: PropTypes.object, location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired, onLayoutChange: PropTypes.func.isRequired,
soapbox: ImmutablePropTypes.map.isRequired,
}; };
state = { state = {
@ -194,7 +199,8 @@ class SwitchingColumnsArea extends React.PureComponent {
} }
render() { render() {
const { children } = this.props; const { children, soapbox } = this.props;
const authenticatedProfile = soapbox.get('authenticatedProfile');
return ( return (
<Switch> <Switch>
@ -254,10 +260,10 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} /> <WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} />
<WrappedRoute path='/filters' page={DefaultPage} component={Filters} content={children} /> <WrappedRoute path='/filters' page={DefaultPage} component={Filters} content={children} />
<WrappedRoute path='/@:username' publicRoute exact component={AccountTimeline} page={ProfilePage} content={children} /> <WrappedRoute path='/@:username' publicRoute exact component={AccountTimeline} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/with_replies' component={AccountTimeline} page={ProfilePage} content={children} componentParams={{ withReplies: true }} /> <WrappedRoute path='/@:username/with_replies' publicRoute={!authenticatedProfile} component={AccountTimeline} page={ProfilePage} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/@:username/followers' component={Followers} page={ProfilePage} content={children} /> <WrappedRoute path='/@:username/followers' publicRoute={!authenticatedProfile} component={Followers} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/following' component={Following} page={ProfilePage} content={children} /> <WrappedRoute path='/@:username/following' publicRoute={!authenticatedProfile} component={Following} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/media' component={AccountGallery} page={ProfilePage} content={children} /> <WrappedRoute path='/@:username/media' publicRoute={!authenticatedProfile} component={AccountGallery} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/tagged/:tag' exact component={AccountTimeline} page={ProfilePage} content={children} /> <WrappedRoute path='/@:username/tagged/:tag' exact component={AccountTimeline} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} /> <WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} /> <WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
@ -314,6 +320,7 @@ class UI extends React.PureComponent {
streamingUrl: PropTypes.string, streamingUrl: PropTypes.string,
account: PropTypes.object, account: PropTypes.object,
features: PropTypes.object.isRequired, features: PropTypes.object.isRequired,
soapbox: ImmutablePropTypes.map.isRequired,
}; };
state = { state = {
@ -594,7 +601,7 @@ class UI extends React.PureComponent {
} }
render() { render() {
const { streamingUrl, features } = this.props; const { streamingUrl, features, soapbox } = this.props;
const { draggingOver, mobile } = this.state; const { draggingOver, mobile } = this.state;
const { intl, children, location, dropdownMenuIsOpen, me } = this.props; const { intl, children, location, dropdownMenuIsOpen, me } = this.props;
@ -644,7 +651,7 @@ class UI extends React.PureComponent {
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused> <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<div className={classnames} ref={this.setRef} style={style}> <div className={classnames} ref={this.setRef} style={style}>
<TabsBar /> <TabsBar />
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}> <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} soapbox={soapbox}>
{children} {children}
</SwitchingColumnsArea> </SwitchingColumnsArea>

View file

@ -250,6 +250,18 @@ export function CryptoDonate() {
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate'); return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate');
} }
export function CryptoDonatePanel() {
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate/components/crypto_donate_panel');
}
export function CryptoAddress() {
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate/components/crypto_address');
}
export function CryptoDonateModal() {
return import(/* webpackChunkName: "features/crypto_donate" */'../components/crypto_donate_modal');
}
export function ScheduledStatuses() { export function ScheduledStatuses() {
return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled_statuses'); return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled_statuses');
} }
@ -265,3 +277,7 @@ export function FederationRestrictions() {
export function Aliases() { export function Aliases() {
return import(/* webpackChunkName: "features/aliases" */'../../aliases'); return import(/* webpackChunkName: "features/aliases" */'../../aliases');
} }
export function ScheduleForm() {
return import(/* webpackChunkName: "features/compose" */'../../compose/components/schedule_form');
}

View file

@ -312,6 +312,7 @@
"emoji_button.search_results": "Wyniki wyszukiwania", "emoji_button.search_results": "Wyniki wyszukiwania",
"emoji_button.symbols": "Symbole", "emoji_button.symbols": "Symbole",
"emoji_button.travel": "Podróże i miejsca", "emoji_button.travel": "Podróże i miejsca",
"empty_column.account_favourited_statuses": "Ten użytkownik nie polubił jeszcze żadnego wpisu.",
"empty_column.account_timeline": "Brak wpisów tutaj!", "empty_column.account_timeline": "Brak wpisów tutaj!",
"empty_column.account_unavailable": "Profil niedostępny", "empty_column.account_unavailable": "Profil niedostępny",
"empty_column.aliases": "Nie utworzyłeś(-aś) jeszcze żadnego aliasu konta.", "empty_column.aliases": "Nie utworzyłeś(-aś) jeszcze żadnego aliasu konta.",
@ -321,7 +322,7 @@
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!", "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
"empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Kiedy dostaniesz lub wyślesz jakąś, pojawi się ona tutaj.", "empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Kiedy dostaniesz lub wyślesz jakąś, pojawi się ona tutaj.",
"empty_column.domain_blocks": "Brak ukrytych domen.", "empty_column.domain_blocks": "Brak ukrytych domen.",
"empty_column.favourited_statuses": "Nie dodałeś(-aś) żadnego wpisu do ulubionych. Kiedy to zrobisz, pojawi się on tutaj.", "empty_column.favourited_statuses": "Nie polubiłeś(-aś) żadnego wpisu. Kiedy to zrobisz, pojawi się on tutaj.",
"empty_column.favourites": "Nikt nie dodał tego wpisu do ulubionych. Gdy ktoś to zrobi, pojawi się tutaj.", "empty_column.favourites": "Nikt nie dodał tego wpisu do ulubionych. Gdy ktoś to zrobi, pojawi się tutaj.",
"empty_column.filters": "Nie wyciszyłeś(-aś) jeszcze żadnego słowa.", "empty_column.filters": "Nie wyciszyłeś(-aś) jeszcze żadnego słowa.",
"empty_column.follow_requests": "Nie masz żadnych próśb o możliwość śledzenia. Kiedy ktoś utworzy ją, pojawi się tutaj.", "empty_column.follow_requests": "Nie masz żadnych próśb o możliwość śledzenia. Kiedy ktoś utworzy ją, pojawi się tutaj.",

View file

@ -1,6 +1,5 @@
'use strict'; 'use strict';
import './wdyr';
import './precheck'; import './precheck';
// FIXME: Push notifications are temporarily removed // FIXME: Push notifications are temporarily removed
// import * as registerPushNotifications from './actions/push_notifications'; // import * as registerPushNotifications from './actions/push_notifications';
@ -10,12 +9,16 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime'; import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime';
import * as perf from './performance'; import * as perf from './performance';
import * as monitoring from './monitoring';
import ready from './ready'; import ready from './ready';
import { NODE_ENV } from 'soapbox/build_config'; import { NODE_ENV } from 'soapbox/build_config';
function main() { function main() {
perf.start('main()'); perf.start('main()');
// Sentry
monitoring.start();
ready(() => { ready(() => {
const mountNode = document.getElementById('soapbox'); const mountNode = document.getElementById('soapbox');

View file

@ -1,8 +1,5 @@
'use strict'; 'use strict';
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
const createAudio = sources => { const createAudio = sources => {
const audio = new Audio(); const audio = new Audio();
sources.forEach(({ type, src }) => { sources.forEach(({ type, src }) => {
@ -31,21 +28,21 @@ export default function soundsMiddleware() {
const soundCache = { const soundCache = {
boop: createAudio([ boop: createAudio([
{ {
src: join(FE_SUBDIRECTORY, '/sounds/boop.ogg'), src: require('../../sounds/boop.ogg'),
type: 'audio/ogg', type: 'audio/ogg',
}, },
{ {
src: join(FE_SUBDIRECTORY, '/sounds/boop.mp3'), src: require('../../sounds/boop.mp3'),
type: 'audio/mpeg', type: 'audio/mpeg',
}, },
]), ]),
chat: createAudio([ chat: createAudio([
{ {
src: join(FE_SUBDIRECTORY, '/sounds/chat.oga'), src: require('../../sounds/chat.oga'),
type: 'audio/ogg', type: 'audio/ogg',
}, },
{ {
src: join(FE_SUBDIRECTORY, '/sounds/chat.mp3'), src: require('../../sounds/chat.mp3'),
type: 'audio/mpeg', type: 'audio/mpeg',
}, },
]), ]),

27
app/soapbox/monitoring.js Normal file
View file

@ -0,0 +1,27 @@
import { NODE_ENV, SENTRY_DSN } from 'soapbox/build_config';
export const start = () => {
Promise.all([
import(/* webpackChunkName: "error" */'@sentry/react'),
import(/* webpackChunkName: "error" */'@sentry/tracing'),
]).then(([Sentry, { Integrations: Integrations }]) => {
Sentry.init({
dsn: SENTRY_DSN,
environment: NODE_ENV,
debug: false,
integrations: [new Integrations.BrowserTracing()],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
});
}).catch(console.error);
};
export const captureException = error => {
import(/* webpackChunkName: "error" */'@sentry/react')
.then(Sentry => {
Sentry.captureException(error);
})
.catch(console.error);
};

View file

@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import BundleContainer from '../features/ui/containers/bundle_container';
import ComposeFormContainer from '../features/compose/containers/compose_form_container'; import ComposeFormContainer from '../features/compose/containers/compose_form_container';
import Avatar from '../components/avatar'; import Avatar from '../components/avatar';
import UserPanel from 'soapbox/features/ui/components/user_panel'; import UserPanel from 'soapbox/features/ui/components/user_panel';
@ -9,7 +10,7 @@ import WhoToFollowPanel from 'soapbox/features/ui/components/who_to_follow_panel
import TrendsPanel from 'soapbox/features/ui/components/trends_panel'; import TrendsPanel from 'soapbox/features/ui/components/trends_panel';
import PromoPanel from 'soapbox/features/ui/components/promo_panel'; import PromoPanel from 'soapbox/features/ui/components/promo_panel';
import FundingPanel from 'soapbox/features/ui/components/funding_panel'; import FundingPanel from 'soapbox/features/ui/components/funding_panel';
import CryptoDonatePanel from 'soapbox/features/crypto_donate/components/crypto_donate_panel'; import { CryptoDonatePanel } from 'soapbox/features/ui/util/async-components';
// import GroupSidebarPanel from '../features/groups/sidebar_panel'; // import GroupSidebarPanel from '../features/groups/sidebar_panel';
import FeaturesPanel from 'soapbox/features/ui/components/features_panel'; import FeaturesPanel from 'soapbox/features/ui/components/features_panel';
import SignUpPanel from 'soapbox/features/ui/components/sign_up_panel'; import SignUpPanel from 'soapbox/features/ui/components/sign_up_panel';
@ -58,7 +59,11 @@ class HomePage extends ImmutablePureComponent {
<div className='columns-area__panels__pane__inner'> <div className='columns-area__panels__pane__inner'>
<UserPanel accountId={me} key='user-panel' /> <UserPanel accountId={me} key='user-panel' />
{showFundingPanel && <FundingPanel key='funding-panel' />} {showFundingPanel && <FundingPanel key='funding-panel' />}
{showCryptoDonatePanel && <CryptoDonatePanel limit={cryptoLimit} key='crypto-panel' />} {showCryptoDonatePanel && (
<BundleContainer fetchComponent={CryptoDonatePanel}>
{Component => <Component limit={cryptoLimit} key='crypto-panel' />}
</BundleContainer>
)}
</div> </div>
</div> </div>

View file

@ -1,10 +1,10 @@
import Immutable from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { import {
DROPDOWN_MENU_OPEN, DROPDOWN_MENU_OPEN,
DROPDOWN_MENU_CLOSE, DROPDOWN_MENU_CLOSE,
} from '../actions/dropdown_menu'; } from '../actions/dropdown_menu';
const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false }); const initialState = ImmutableMap({ openId: null, placement: null, keyboard: false });
export default function dropdownMenu(state = initialState, action) { export default function dropdownMenu(state = initialState, action) {
switch (action.type) { switch (action.type) {

View file

@ -1,12 +1,12 @@
import Immutable from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { import {
MUTES_INIT_MODAL, MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS, MUTES_TOGGLE_HIDE_NOTIFICATIONS,
} from '../actions/mutes'; } from '../actions/mutes';
const initialState = Immutable.Map({ const initialState = ImmutableMap({
new: Immutable.Map({ new: ImmutableMap({
isSubmitting: false, isSubmitting: false,
account: null, account: null,
notifications: true, notifications: true,

View file

@ -1,9 +1,9 @@
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push_notifications'; import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push_notifications';
import Immutable from 'immutable'; import { Map as ImmutableMap } from 'immutable';
const initialState = Immutable.Map({ const initialState = ImmutableMap({
subscription: null, subscription: null,
alerts: new Immutable.Map({ alerts: new ImmutableMap({
follow: false, follow: false,
follow_request: false, follow_request: false,
favourite: false, favourite: false,
@ -19,11 +19,11 @@ export default function push_subscriptions(state = initialState, action) {
switch(action.type) { switch(action.type) {
case SET_SUBSCRIPTION: case SET_SUBSCRIPTION:
return state return state
.set('subscription', new Immutable.Map({ .set('subscription', new ImmutableMap({
id: action.subscription.id, id: action.subscription.id,
endpoint: action.subscription.endpoint, endpoint: action.subscription.endpoint,
})) }))
.set('alerts', new Immutable.Map(action.subscription.alerts)) .set('alerts', new ImmutableMap(action.subscription.alerts))
.set('isSubscribed', true); .set('isSubscribed', true);
case SET_BROWSER_SUPPORT: case SET_BROWSER_SUPPORT:
return state.set('browserSupport', action.value); return state.set('browserSupport', action.value);

View file

@ -5,6 +5,12 @@ import {
FAVOURITED_STATUSES_EXPAND_REQUEST, FAVOURITED_STATUSES_EXPAND_REQUEST,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
FAVOURITED_STATUSES_EXPAND_FAIL, FAVOURITED_STATUSES_EXPAND_FAIL,
ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
} from '../actions/favourites'; } from '../actions/favourites';
import { import {
BOOKMARKED_STATUSES_FETCH_REQUEST, BOOKMARKED_STATUSES_FETCH_REQUEST,
@ -101,6 +107,16 @@ export default function statusLists(state = initialState, action) {
return normalizeList(state, 'favourites', action.statuses, action.next); return normalizeList(state, 'favourites', action.statuses, action.next);
case FAVOURITED_STATUSES_EXPAND_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, 'favourites', action.statuses, action.next); return appendToList(state, 'favourites', action.statuses, action.next);
case ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST:
return setLoading(state, `favourites:${action.accountId}`, true);
case ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL:
return setLoading(state, `favourites:${action.accountId}`, false);
case ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, `favourites:${action.accountId}`, action.statuses, action.next);
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, `favourites:${action.accountId}`, action.statuses, action.next);
case BOOKMARKED_STATUSES_FETCH_REQUEST: case BOOKMARKED_STATUSES_FETCH_REQUEST:
case BOOKMARKED_STATUSES_EXPAND_REQUEST: case BOOKMARKED_STATUSES_EXPAND_REQUEST:
return setLoading(state, 'bookmarks', true); return setLoading(state, 'bookmarks', true);

View file

@ -26,6 +26,8 @@ export const getFeatures = createSelector([
accountAliasesAPI: v.software === 'Pleroma', accountAliasesAPI: v.software === 'Pleroma',
resetPasswordAPI: v.software === 'Pleroma', resetPasswordAPI: v.software === 'Pleroma',
exposableReactions: features.includes('exposable_reactions'), exposableReactions: features.includes('exposable_reactions'),
accountSubscriptions: v.software === 'Pleroma' && gte(v.version, '1.0.0'),
unrestrictedLists: v.software === 'Pleroma',
}; };
}); });

View file

@ -1,6 +1,4 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
import EXIF from 'exif-js';
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
const _browser_quirks = {}; const _browser_quirks = {};
@ -115,6 +113,7 @@ const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
return; return;
} }
import(/* webpackChunkName: "features/compose" */'exif-js').then(({ default: EXIF }) => {
EXIF.getData(img, () => { EXIF.getData(img, () => {
const orientation = EXIF.getTag(img, 'Orientation'); const orientation = EXIF.getTag(img, 'Orientation');
if (orientation !== 1) { if (orientation !== 1) {
@ -123,6 +122,7 @@ const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
resolve(orientation); resolve(orientation);
} }
}); });
}).catch(() => {});
}); });
const processImage = (img, { width, height, orientation, type = 'image/png', name = 'resized.png' }) => new Promise(resolve => { const processImage = (img, { width, height, orientation, type = 'image/png', name = 'resized.png' }) => new Promise(resolve => {

View file

@ -0,0 +1,11 @@
/**
* Static: functions related to static files.
* @module soapbox/utils/static
*/
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
export const joinPublicPath = (...paths) => {
return join(FE_SUBDIRECTORY, ...paths);
};

View file

@ -1,7 +0,0 @@
import React from 'react';
import { NODE_ENV } from 'soapbox/build_config';
if (NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React);
}

View file

@ -156,6 +156,7 @@
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
transition: 0.2s !important; transition: 0.2s !important;
background: var(--foreground-color);
&:hover { &:hover {
background-color: var(--background-color) !important; background-color: var(--background-color) !important;

View file

@ -60,3 +60,18 @@ For example, if you want to host the build on `https://gleasonator.com/soapbox`,
```sh ```sh
NODE_ENV="production" FE_SUBDIRECTORY="/soapbox" yarn build NODE_ENV="production" FE_SUBDIRECTORY="/soapbox" yarn build
``` ```
### `SENTRY_DSN`
[Sentry](https://sentry.io/) endpoint for this custom build.
Sentry is an error monitoring service that may be optionally included.
When an endpoint is not configured, it does nothing.
Sentry's backend was FOSS until 2019 when it moved to source-available, but a BSD-3 fork called [GlitchTip](https://glitchtip.com/) may also be used.
Options:
- Endpoint URL, eg `"https://abcdefg@app.glitchtip.com/123"`
Default: `""`

View file

@ -31,4 +31,11 @@ module.exports = {
'<rootDir>/app', '<rootDir>/app',
], ],
'testEnvironment': 'jsdom', 'testEnvironment': 'jsdom',
'moduleNameMapper': {
'^.+.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub',
},
'transform': {
'\\.[jt]sx?$': 'babel-jest',
'.+\\.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub',
},
}; };

View file

@ -31,7 +31,7 @@
"browserslist": [ "browserslist": [
"> 0.5%", "> 0.5%",
"last 2 versions", "last 2 versions",
"Firefox ESR", "not IE 11",
"not dead" "not dead"
], ],
"dependencies": { "dependencies": {
@ -51,7 +51,9 @@
"@fontsource/roboto": "^4.5.0", "@fontsource/roboto": "^4.5.0",
"@lcdp/offline-plugin": "^5.1.0", "@lcdp/offline-plugin": "^5.1.0",
"@popperjs/core": "^2.4.4", "@popperjs/core": "^2.4.4",
"@welldone-software/why-did-you-render": "^6.2.0", "@sentry/browser": "^6.12.0",
"@sentry/react": "^6.12.0",
"@sentry/tracing": "^6.12.0",
"array-includes": "^3.0.3", "array-includes": "^3.0.3",
"autoprefixer": "^10.0.0", "autoprefixer": "^10.0.0",
"axios": "^0.21.0", "axios": "^0.21.0",
@ -92,6 +94,7 @@
"intl-messageformat-parser": "^6.0.0", "intl-messageformat-parser": "^6.0.0",
"intl-pluralrules": "^1.3.0", "intl-pluralrules": "^1.3.0",
"is-nan": "^1.2.1", "is-nan": "^1.2.1",
"jest-transform-stub": "^2.0.0",
"jsdoc": "~3.6.7", "jsdoc": "~3.6.7",
"lodash": "^4.7.11", "lodash": "^4.7.11",
"mark-loader": "^0.1.6", "mark-loader": "^0.1.6",

View file

@ -12,7 +12,6 @@ const settings = {
test_root_path: `${FE_BUILD_DIR}-test`, test_root_path: `${FE_BUILD_DIR}-test`,
cache_path: 'tmp/cache', cache_path: 'tmp/cache',
resolved_paths: [], resolved_paths: [],
static_assets_extensions: [ '.jpg', '.jpeg', '.png', '.tiff', '.ico', '.svg', '.gif', '.eot', '.otf', '.ttf', '.woff', '.woff2', '.mp3', '.ogg', '.oga' ],
extensions: [ '.mjs', '.js', '.sass', '.scss', '.css', '.module.sass', '.module.scss', '.module.css', '.png', '.svg', '.gif', '.jpeg', '.jpg' ], extensions: [ '.mjs', '.js', '.sass', '.scss', '.css', '.module.sass', '.module.scss', '.module.css', '.png', '.svg', '.gif', '.jpeg', '.jpg' ],
}; };

View file

@ -1,11 +1,15 @@
// Note: You must restart bin/webpack-dev-server for changes to take effect // Note: You must restart bin/webpack-dev-server for changes to take effect
console.log('Running in production mode'); // eslint-disable-line no-console console.log('Running in production mode'); // eslint-disable-line no-console
const { join } = require('path');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const OfflinePlugin = require('@lcdp/offline-plugin'); const OfflinePlugin = require('@lcdp/offline-plugin');
const sharedConfig = require('./shared'); const sharedConfig = require('./shared');
const { FE_SUBDIRECTORY } = require(join(__dirname, '..', 'app', 'soapbox', 'build_config'));
const joinPublicPath = (...paths) => join(FE_SUBDIRECTORY, ...paths);
module.exports = merge(sharedConfig, { module.exports = merge(sharedConfig, {
mode: 'production', mode: 'production',
devtool: 'source-map', devtool: 'source-map',
@ -25,37 +29,37 @@ module.exports = merge(sharedConfig, {
new OfflinePlugin({ new OfflinePlugin({
caches: { caches: {
main: [':rest:'], main: [':rest:'],
additional: [':externals:'], additional: [
':externals:',
'packs/images/32-*.png', // used in emoji-mart
],
optional: [ optional: [
'**/locale_*.js', // don't fetch every locale; the user only needs one '**/locale_*.js', // don't fetch every locale; the user only needs one
'**/*_polyfills-*.js', // the user may not need polyfills '**/*_polyfills-*.js', // the user may not need polyfills
'**/*.chunk.js', // only cache chunks when needed '**/*.chunk.js', // only cache chunks when needed
'**/*.chunk.css',
'**/*.woff2', // the user may have system-fonts enabled '**/*.woff2', // the user may have system-fonts enabled
// images/audio can be cached on-demand // images can be cached on-demand
'**/*.png', '**/*.png',
'**/*.jpg',
'**/*.jpeg',
'**/*.svg', '**/*.svg',
'**/*.mp3',
'**/*.ogg',
], ],
}, },
externals: [ externals: [
'/emoji/1f602.svg', // used for emoji picker dropdown joinPublicPath('packs/emoji/1f602.svg'), // used for emoji picker dropdown
'/emoji/sheet_13.png', // used in emoji-mart
// Default emoji reacts // Default emoji reacts
'/emoji/1f44d.svg', // Thumbs up joinPublicPath('packs/emoji/1f44d.svg'), // Thumbs up
'/emoji/2764.svg', // Heart joinPublicPath('packs/emoji/2764.svg'), // Heart
'/emoji/1f606.svg', // Laughing joinPublicPath('packs/emoji/1f606.svg'), // Laughing
'/emoji/1f62e.svg', // Surprised joinPublicPath('packs/emoji/1f62e.svg'), // Surprised
'/emoji/1f622.svg', // Crying joinPublicPath('packs/emoji/1f622.svg'), // Crying
'/emoji/1f629.svg', // Weary joinPublicPath('packs/emoji/1f629.svg'), // Weary
'/emoji/1f621.svg', // Angry (Spinster) joinPublicPath('packs/emoji/1f621.svg'), // Angry (Spinster)
], ],
excludes: [ excludes: [
'**/*.gz', '**/*.gz',
'**/*.map', '**/*.map',
'**/*.LICENSE.txt',
'stats.json', 'stats.json',
'report.html', 'report.html',
'instance/**/*', 'instance/**/*',
@ -66,15 +70,24 @@ module.exports = merge(sharedConfig, {
'**/*.woff', '**/*.woff',
// Sounds return a 206 causing sw.js to crash // Sounds return a 206 causing sw.js to crash
// https://stackoverflow.com/a/66335638 // https://stackoverflow.com/a/66335638
'sounds/**/*', '**/*.ogg',
// Don't cache index.html '**/*.oga',
'**/*.mp3',
// Don't serve index.html
// https://github.com/bromite/bromite/issues/1294
'index.html', 'index.html',
'404.html',
'assets-manifest.json',
// It would be nice to serve these, but they bloat up sw.js
'packs/images/crypto/**/*',
'packs/emoji/**/*',
], ],
// ServiceWorker: { ServiceWorker: {
// entry: join(__dirname, '../app/soapbox/service_worker/entry.js'), // entry: join(__dirname, '../app/soapbox/service_worker/entry.js'),
// cacheName: 'soapbox', // cacheName: 'soapbox',
// minify: true, minify: true,
// }, },
safeToUseOptionalCaches: true,
}), }),
], ],
}); });

50
webpack/rules/assets.js Normal file
View file

@ -0,0 +1,50 @@
// Asset modules
// https://webpack.js.org/guides/asset-modules/
const { resolve } = require('path');
// These are processed in reverse-order
// We use the name 'packs' instead of 'assets' for legacy reasons
module.exports = [{
test: /\.(png|svg)/,
type: 'asset/resource',
include: [
resolve('app', 'images'),
resolve('node_modules', 'emoji-datasource'),
],
generator: {
filename: 'packs/images/[name]-[contenthash:8][ext]',
},
}, {
test: /\.(ttf|eot|svg|woff|woff2)/,
type: 'asset/resource',
include: [
resolve('app', 'fonts'),
resolve('node_modules', 'fork-awesome'),
resolve('node_modules', '@fontsource'),
],
generator: {
filename: 'packs/fonts/[name]-[contenthash:8][ext]',
},
}, {
test: /\.(ogg|oga|mp3)/,
type: 'asset/resource',
include: resolve('app', 'sounds'),
generator: {
filename: 'packs/sounds/[name]-[contenthash:8][ext]',
},
}, {
test: /\.svg$/,
type: 'asset/resource',
include: resolve('node_modules', 'twemoji'),
generator: {
filename: 'packs/emoji/[name]-[contenthash:8][ext]',
},
}, {
test: /\.svg$/,
type: 'asset/resource',
include: resolve('node_modules', 'cryptocurrency-icons'),
generator: {
filename: 'packs/images/crypto/[name]-[contenthash:8][ext]',
},
}];

View file

@ -1,20 +0,0 @@
const { join } = require('path');
const { settings } = require('../configuration');
module.exports = {
test: new RegExp(`(${settings.static_assets_extensions.join('|')})$`, 'i'),
use: [
{
loader: 'file-loader',
options: {
name(file) {
if (file.includes(settings.source_path)) {
return 'packs/media/[path][name]-[contenthash].[ext]';
}
return 'packs/media/[folder]/[name]-[contenthash:8].[ext]';
},
context: join(settings.source_path),
},
},
],
};

View file

@ -3,14 +3,14 @@ const git = require('./babel-git');
const gitRefresh = require('./git-refresh'); const gitRefresh = require('./git-refresh');
const buildConfig = require('./babel-build-config'); const buildConfig = require('./babel-build-config');
const css = require('./css'); const css = require('./css');
const file = require('./file'); const assets = require('./assets');
const nodeModules = require('./node_modules'); const nodeModules = require('./node_modules');
// Webpack loaders are processed in reverse order // Webpack loaders are processed in reverse order
// https://webpack.js.org/concepts/loaders/#loader-features // https://webpack.js.org/concepts/loaders/#loader-features
// Lastly, process static files using file loader // Lastly, process static files using file loader
module.exports = [ module.exports = [
file, ...assets,
css, css,
nodeModules, nodeModules,
babel, babel,

View file

@ -30,10 +30,9 @@ const makeHtmlConfig = (params = {}) => {
}; };
module.exports = { module.exports = {
entry: Object.assign( entry: {
{ application: resolve('app/application.js') }, application: resolve('app/application.js'),
{ styles: resolve(join(settings.source_path, 'styles/application.scss')) }, },
),
output: { output: {
filename: 'packs/js/[name]-[chunkhash].js', filename: 'packs/js/[name]-[chunkhash].js',
@ -65,7 +64,7 @@ module.exports = {
}, },
module: { module: {
rules: Object.keys(rules).map(key => rules[key]), rules,
}, },
plugins: [ plugins: [
@ -90,13 +89,7 @@ module.exports = {
new CopyPlugin({ new CopyPlugin({
patterns: [{ patterns: [{
from: join(__dirname, '../node_modules/twemoji/assets/svg'), from: join(__dirname, '../node_modules/twemoji/assets/svg'),
to: join(output.path, 'emoji'), to: join(output.path, 'packs/emoji'),
}, {
from: join(__dirname, '../node_modules/emoji-datasource/img/twitter/sheets/32.png'),
to: join(output.path, 'emoji/sheet_13.png'),
}, {
from: join(__dirname, '../app/sounds'),
to: join(output.path, 'sounds'),
}, { }, {
from: join(__dirname, '../app/instance'), from: join(__dirname, '../app/instance'),
to: join(output.path, 'instance'), to: join(output.path, 'instance'),

View file

@ -2039,6 +2039,81 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@sentry/browser@6.12.0", "@sentry/browser@^6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.12.0.tgz#970cd68fa117a1e1336fdb373e3b1fa76cd63e2d"
integrity sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ==
dependencies:
"@sentry/core" "6.12.0"
"@sentry/types" "6.12.0"
"@sentry/utils" "6.12.0"
tslib "^1.9.3"
"@sentry/core@6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.12.0.tgz#bc7c5f0785b6a392d9ad47bd9b1fae3f5389996c"
integrity sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ==
dependencies:
"@sentry/hub" "6.12.0"
"@sentry/minimal" "6.12.0"
"@sentry/types" "6.12.0"
"@sentry/utils" "6.12.0"
tslib "^1.9.3"
"@sentry/hub@6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.12.0.tgz#29e323ab6a95e178fb14fffb684aa0e09707197f"
integrity sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg==
dependencies:
"@sentry/types" "6.12.0"
"@sentry/utils" "6.12.0"
tslib "^1.9.3"
"@sentry/minimal@6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.12.0.tgz#cbe20e95056cedb9709d7d5b2119ef95206a9f8c"
integrity sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw==
dependencies:
"@sentry/hub" "6.12.0"
"@sentry/types" "6.12.0"
tslib "^1.9.3"
"@sentry/react@^6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.12.0.tgz#8ae2680d226fafb0da0f3d8366bb285004ba6c2e"
integrity sha512-E8Nw9PPzP/EyMy64ksr9xcyYYlBmUA5ROnkPQp7o5wF0xf5/J+nMS1tQdyPnLQe2KUgHlN4kVs2HHft1m7mSYQ==
dependencies:
"@sentry/browser" "6.12.0"
"@sentry/minimal" "6.12.0"
"@sentry/types" "6.12.0"
"@sentry/utils" "6.12.0"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/tracing@^6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.12.0.tgz#a05c8985ee7fed7310b029b147d8f9f14f2a2e67"
integrity sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA==
dependencies:
"@sentry/hub" "6.12.0"
"@sentry/minimal" "6.12.0"
"@sentry/types" "6.12.0"
"@sentry/utils" "6.12.0"
tslib "^1.9.3"
"@sentry/types@6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.12.0.tgz#b7395688a79403c6df8d8bb8d81deb8222519853"
integrity sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA==
"@sentry/utils@6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.12.0.tgz#3de261e8d11bdfdc7add64a3065d43517802e975"
integrity sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA==
dependencies:
"@sentry/types" "6.12.0"
tslib "^1.9.3"
"@sinonjs/commons@^1.7.0": "@sinonjs/commons@^1.7.0":
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
@ -2454,13 +2529,6 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.2.tgz#ea584b637ff63c5a477f6f21604b5a205b72c9ec" resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.2.tgz#ea584b637ff63c5a477f6f21604b5a205b72c9ec"
integrity sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw== integrity sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==
"@welldone-software/why-did-you-render@^6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.0.tgz#a053e63f45adb57161c723dee4b005769ea1b64f"
integrity sha512-ViwaE09Vgb0yXzyZuGTWCmWy/nBRAEGyztMdFYuxIgmL8yoXX5TVMCfieiJGdRQQPiDUznlYmcu0lu8kN1lwtQ==
dependencies:
lodash "^4"
"@xtuc/ieee754@^1.2.0": "@xtuc/ieee754@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -7239,6 +7307,11 @@ jest-snapshot@^27.1.0:
pretty-format "^27.1.0" pretty-format "^27.1.0"
semver "^7.3.2" semver "^7.3.2"
jest-transform-stub@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d"
integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==
jest-util@^27.0.0: jest-util@^27.0.0:
version "27.0.6" version "27.0.6"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.6.tgz#e8e04eec159de2f4d5f57f795df9cdc091e50297" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.6.tgz#e8e04eec159de2f4d5f57f795df9cdc091e50297"
@ -7731,7 +7804,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@4.x, lodash@^4, lodash@^4.17.21, lodash@^4.7.0: lodash@4.x, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -11665,6 +11738,11 @@ tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.1: tslib@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242"