diff --git a/app/application.js b/app/application.js index a6d4ed199..96cedfeef 100644 --- a/app/application.js +++ b/app/application.js @@ -2,6 +2,9 @@ import loadPolyfills from './soapbox/load_polyfills'; require.context('./images/', true); +// Load stylesheet +require('./styles/application.scss'); + loadPolyfills().then(() => { require('./soapbox/main').default(); }).catch(e => { diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index e215e5256..9fd4a478e 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -471,8 +471,6 @@ export function unsubscribeAccountFail(error) { export function fetchFollowers(id) { return (dispatch, getState) => { - if (!isLoggedIn(getState)) return; - dispatch(fetchFollowersRequest(id)); api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => { @@ -561,8 +559,6 @@ export function expandFollowersFail(id, error) { export function fetchFollowing(id) { return (dispatch, getState) => { - if (!isLoggedIn(getState)) return; - dispatch(fetchFollowingRequest(id)); api(getState).get(`/api/v1/accounts/${id}/following`).then(response => { diff --git a/app/soapbox/actions/favourites.js b/app/soapbox/actions/favourites.js index 44c40cd81..dfc1ee9ba 100644 --- a/app/soapbox/actions/favourites.js +++ b/app/soapbox/actions/favourites.js @@ -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_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() { return (dispatch, getState) => { if (!isLoggedIn(getState)) return; @@ -96,3 +104,96 @@ export function expandFavouritedStatusesFail(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, + }; +} diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index d2abc83a6..64fbe1891 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -50,6 +50,7 @@ export const makeDefaultConfig = features => { limit: 1, }), aboutPages: ImmutableMap(), + authenticatedProfile: true, }); }; diff --git a/app/soapbox/build_config.js b/app/soapbox/build_config.js index 7e0419619..bb9209d6c 100644 --- a/app/soapbox/build_config.js +++ b/app/soapbox/build_config.js @@ -11,6 +11,7 @@ const { BACKEND_URL, FE_SUBDIRECTORY, FE_BUILD_DIR, + SENTRY_DSN, } = process.env; const sanitizeURL = url => { @@ -38,4 +39,5 @@ module.exports = sanitize({ BACKEND_URL: sanitizeURL(BACKEND_URL), FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY), FE_BUILD_DIR: sanitizePath(FE_BUILD_DIR) || 'static', + SENTRY_DSN, }); diff --git a/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap index 1c3727848..1ab178e15 100644 --- a/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap @@ -20,7 +20,7 @@ exports[` renders native emoji 1`] = ` ๐Ÿ’™ :foobar: diff --git a/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap index 06d7764ec..0ff8f9961 100644 --- a/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/emoji_selector-test.js.snap @@ -15,7 +15,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ‘\\"", + "__html": "\\"๐Ÿ‘\\"", } } onClick={[Function]} @@ -26,7 +26,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"โค\\"", + "__html": "\\"โค\\"", } } onClick={[Function]} @@ -37,7 +37,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜†\\"", + "__html": "\\"๐Ÿ˜†\\"", } } onClick={[Function]} @@ -48,7 +48,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜ฎ\\"", + "__html": "\\"๐Ÿ˜ฎ\\"", } } onClick={[Function]} @@ -59,7 +59,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜ข\\"", + "__html": "\\"๐Ÿ˜ข\\"", } } onClick={[Function]} @@ -70,7 +70,7 @@ exports[` renders correctly 1`] = ` className="emoji-react-selector__emoji" dangerouslySetInnerHTML={ Object { - "__html": "\\"๐Ÿ˜ฉ\\"", + "__html": "\\"๐Ÿ˜ฉ\\"", } } onClick={[Function]} diff --git a/app/soapbox/components/autosuggest_emoji.js b/app/soapbox/components/autosuggest_emoji.js index 6311061b0..188dc6c0e 100644 --- a/app/soapbox/components/autosuggest_emoji.js +++ b/app/soapbox/components/autosuggest_emoji.js @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; +import { joinPublicPath } from 'soapbox/utils/static'; export default class AutosuggestEmoji extends React.PureComponent { @@ -23,7 +22,7 @@ export default class AutosuggestEmoji extends React.PureComponent { return null; } - url = join(FE_SUBDIRECTORY, 'emoji', `${mapping.filename}.svg`); + url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`); } return ( diff --git a/app/soapbox/components/error_boundary.js b/app/soapbox/components/error_boundary.js index 731e7ecf2..55bd4b08a 100644 --- a/app/soapbox/components/error_boundary.js +++ b/app/soapbox/components/error_boundary.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import Bowser from 'bowser'; +import { captureException } from 'soapbox/monitoring'; export default class ErrorBoundary extends React.PureComponent { @@ -15,11 +15,21 @@ export default class ErrorBoundary extends React.PureComponent { } componentDidCatch(error, info) { + captureException(error); + this.setState({ hasError: true, error, componentStack: info && info.componentStack, }); + + import(/* webpackChunkName: "error" */'bowser') + .then(({ default: Bowser }) => { + this.setState({ + browser: Bowser.getParser(window.navigator.userAgent), + }); + }) + .catch(() => {}); } setTextareaRef = c => { @@ -46,9 +56,7 @@ export default class ErrorBoundary extends React.PureComponent { } render() { - const browser = Bowser.getParser(window.navigator.userAgent); - - const { hasError } = this.state; + const { browser, hasError } = this.state; if (!hasError) { return this.props.children; @@ -72,9 +80,9 @@ export default class ErrorBoundary extends React.PureComponent { onClick={this.handleCopy} readOnly />} -

+ {browser &&

{browser.getBrowserName()} {browser.getBrowserVersion()} -

+

}

); + output.push(); } return output; } else { const output = [

); + output.push(); } return output; diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 70e2190fa..f3bc72a38 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -17,7 +17,6 @@ import { isRemote, getDomain, } from 'soapbox/utils/accounts'; -import { parseVersion } from 'soapbox/utils/features'; import classNames from 'classnames'; import Avatar from 'soapbox/components/avatar'; 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 { openModal } from 'soapbox/actions/modal'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; +import { getFeatures } from 'soapbox/utils/features'; const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -72,11 +72,13 @@ const messages = defineMessages({ const mapStateToProps = state => { const me = state.get('me'); const account = state.getIn(['accounts', me]); + const instance = state.get('instance'); + const features = getFeatures(instance); return { me, meAccount: account, - version: parseVersion(state.getIn(['instance', 'version'])), + features, }; }; @@ -90,7 +92,7 @@ class Header extends ImmutablePureComponent { identity_props: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, username: PropTypes.string, - version: PropTypes.object, + features: PropTypes.object, }; state = { @@ -156,7 +158,7 @@ class Header extends ImmutablePureComponent { } makeMenu() { - const { account, intl, me, meAccount, version } = this.props; + const { account, intl, me, meAccount, features } = this.props; 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(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); 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 }); } @@ -285,7 +287,7 @@ class Header extends ImmutablePureComponent { } render() { - const { account, intl, username, me } = this.props; + const { account, intl, username, me, features } = this.props; const { isSmallScreen } = this.state; if (!account) { @@ -327,9 +329,9 @@ class Header extends ImmutablePureComponent { } -
+ {features.accountSubscriptions &&
-
+
}
@@ -356,24 +358,20 @@ class Header extends ImmutablePureComponent { } - { - ownAccount && -
- - { /* : TODO : shortNumberFormat(account.get('favourite_count')) */ } - โ€ข - - - - { /* : TODO : shortNumberFormat(account.get('pinned_count')) */ } - โ€ข - - -
+ {(ownAccount || !account.getIn(['pleroma', 'hide_favorites'], true)) && + { /* : TODO : shortNumberFormat(account.get('favourite_count')) */ } + โ€ข + + } + + {ownAccount && + + { /* : TODO : shortNumberFormat(account.get('pinned_count')) */ } + โ€ข + + }
diff --git a/app/soapbox/features/compose/components/emoji_picker_dropdown.js b/app/soapbox/features/compose/components/emoji_picker_dropdown.js index 2d9f155fd..29d3270c9 100644 --- a/app/soapbox/features/compose/components/emoji_picker_dropdown.js +++ b/app/soapbox/features/compose/components/emoji_picker_dropdown.js @@ -7,8 +7,7 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { supportsPassiveEvents } from 'detect-passive-events'; import { buildCustomEmojis } from '../../emoji/emoji'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; +import { joinPublicPath } from 'soapbox/utils/static'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -29,7 +28,7 @@ const messages = defineMessages({ 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 categoriesSort = [ @@ -358,8 +357,8 @@ class EmojiPickerDropdown extends React.PureComponent {
๐Ÿ™‚
diff --git a/app/soapbox/features/compose/components/schedule_form.js b/app/soapbox/features/compose/components/schedule_form.js index e679259a4..1394cb6a2 100644 --- a/app/soapbox/features/compose/components/schedule_form.js +++ b/app/soapbox/features/compose/components/schedule_form.js @@ -4,10 +4,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { setSchedule, removeSchedule } from '../../../actions/compose'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import IconButton from 'soapbox/components/icon_button'; -import { removeSchedule } from 'soapbox/actions/compose'; import classNames from 'classnames'; const messages = defineMessages({ @@ -15,11 +15,22 @@ const messages = defineMessages({ 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']), }); -export default @connect(mapStateToProps) +const mapDispatchToProps = dispatch => ({ + onSchedule(date) { + dispatch(setSchedule(date)); + }, + + onRemoveSchedule(date) { + dispatch(removeSchedule()); + }, +}); + +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl class ScheduleForm extends React.Component { @@ -27,6 +38,7 @@ class ScheduleForm extends React.Component { scheduledAt: PropTypes.instanceOf(Date), intl: PropTypes.object.isRequired, onSchedule: PropTypes.func.isRequired, + onRemoveSchedule: PropTypes.func.isRequired, dispatch: PropTypes.func, active: PropTypes.bool, }; @@ -60,7 +72,7 @@ class ScheduleForm extends React.Component { } handleRemove = e => { - this.props.dispatch(removeSchedule()); + this.props.onRemoveSchedule(); e.preventDefault(); } diff --git a/app/soapbox/features/compose/containers/schedule_form_container.js b/app/soapbox/features/compose/containers/schedule_form_container.js index da4887300..50042e5b3 100644 --- a/app/soapbox/features/compose/containers/schedule_form_container.js +++ b/app/soapbox/features/compose/containers/schedule_form_container.js @@ -1,16 +1,15 @@ -import { connect } from 'react-redux'; -import ScheduleForm from '../components/schedule_form'; -import { setSchedule } from '../../../actions/compose'; +import React from 'react'; +import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; +import { ScheduleForm } from 'soapbox/features/ui/util/async-components'; -const mapStateToProps = state => ({ - schedule: state.getIn(['compose', 'schedule']), - active: state.getIn(['compose', 'schedule']) ? true : false, -}); +export default class ScheduleFormContainer extends React.PureComponent { -const mapDispatchToProps = dispatch => ({ - onSchedule(date) { - dispatch(setSchedule(date)); - }, -}); + render() { + return ( + + {Component => } + + ); + } -export default connect(mapStateToProps, mapDispatchToProps)(ScheduleForm); +} diff --git a/app/soapbox/features/crypto_donate/components/crypto_address.js b/app/soapbox/features/crypto_donate/components/crypto_address.js index b2498101c..0eba72655 100644 --- a/app/soapbox/features/crypto_donate/components/crypto_address.js +++ b/app/soapbox/features/crypto_donate/components/crypto_address.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Icon from 'soapbox/components/icon'; import CoinDB from '../utils/coin_db'; -import { getCoinIcon } from '../utils/coin_icons'; +import CryptoIcon from './crypto_icon'; import { openModal } from 'soapbox/actions/modal'; import { CopyableInput } from 'soapbox/features/forms'; import { getExplorerUrl } from '../utils/block_explorer'; @@ -31,9 +31,11 @@ class CryptoAddress extends ImmutablePureComponent { return (
-
- {title} -
+
{title || ticker.toUpperCase()}
diff --git a/app/soapbox/features/crypto_donate/components/crypto_icon.js b/app/soapbox/features/crypto_donate/components/crypto_icon.js new file mode 100644 index 000000000..401c78963 --- /dev/null +++ b/app/soapbox/features/crypto_donate/components/crypto_icon.js @@ -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 ( +
+ {title +
+ ); + } + +} diff --git a/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js b/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js index de5971d36..0382291c0 100644 --- a/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js +++ b/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js @@ -1,16 +1,14 @@ import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Icon from 'soapbox/components/icon'; import QRCode from 'qrcode.react'; import CoinDB from '../utils/coin_db'; -import { getCoinIcon } from '../utils/coin_icons'; +import CryptoIcon from './crypto_icon'; import { CopyableInput } from 'soapbox/features/forms'; import { getExplorerUrl } from '../utils/block_explorer'; -export default @connect() -class DetailedCryptoAddress extends ImmutablePureComponent { +export default class DetailedCryptoAddress extends ImmutablePureComponent { static propTypes = { address: PropTypes.string.isRequired, @@ -26,9 +24,11 @@ class DetailedCryptoAddress extends ImmutablePureComponent { return (
-
- {title} -
+
{title || ticker.toUpperCase()}
{explorerUrl && diff --git a/app/soapbox/features/crypto_donate/utils/coin_icons.js b/app/soapbox/features/crypto_donate/utils/coin_icons.js deleted file mode 100644 index 2c0376bfa..000000000 --- a/app/soapbox/features/crypto_donate/utils/coin_icons.js +++ /dev/null @@ -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; diff --git a/app/soapbox/features/emoji/__tests__/emoji-test.js b/app/soapbox/features/emoji/__tests__/emoji-test.js index c8425c4c6..f1374e8c1 100644 --- a/app/soapbox/features/emoji/__tests__/emoji-test.js +++ b/app/soapbox/features/emoji/__tests__/emoji-test.js @@ -22,23 +22,23 @@ describe('emoji', () => { it('does unicode', () => { expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual( - '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); expect(emojify('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง')).toEqual( - '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); - expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); + expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); expect(emojify('\u2757')).toEqual( - 'โ—'); + 'โ—'); }); it('does multiple unicode', () => { expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual( - 'โ— #๏ธโƒฃ'); + 'โ— #๏ธโƒฃ'); expect(emojify('\u2757#\uFE0F\u20E3')).toEqual( - 'โ—#๏ธโƒฃ'); + 'โ—#๏ธโƒฃ'); expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual( - 'โ— #๏ธโƒฃ โ—'); + 'โ— #๏ธโƒฃ โ—'); expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual( - 'foo โ— #๏ธโƒฃ bar'); + 'foo โ— #๏ธโƒฃ bar'); }); it('ignores unicode inside of tags', () => { @@ -46,16 +46,16 @@ describe('emoji', () => { }); it('does multiple emoji properly (issue 5188)', () => { - expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); - expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); }); it('does an emoji that has no shortcode', () => { - expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); + expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); }); it('does an emoji whose filename is irregular', () => { - expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); + expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); }); it('avoid emojifying on invisible text', () => { @@ -67,16 +67,16 @@ describe('emoji', () => { it('avoid emojifying on invisible text with nested tags', () => { expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); }); it('skips the textual presentation VS15 character', () => { expect(emojify('โœด๏ธŽ')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15 - .toEqual('โœด'); + .toEqual('โœด'); }); }); }); diff --git a/app/soapbox/features/emoji/emoji.js b/app/soapbox/features/emoji/emoji.js index eb0df79a7..e43f67ac7 100644 --- a/app/soapbox/features/emoji/emoji.js +++ b/app/soapbox/features/emoji/emoji.js @@ -1,7 +1,6 @@ import unicodeMapping from './emoji_unicode_mapping_light'; import Trie from 'substring-trie'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; +import { joinPublicPath } from 'soapbox/utils/static'; const trie = new Trie(Object.keys(unicodeMapping)); @@ -62,7 +61,8 @@ const emojify = (str, customEmojis = {}, autoplay = false) => { } else { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; - replacement = `${match}`; + const src = joinPublicPath(`packs/emoji/${filename}.svg`); + replacement = `${match}`; rend = i + match.length; // If the matched character was followed by VS15 (for selecting text presentation), skip it. if (str.codePointAt(rend) === 65038) { diff --git a/app/soapbox/features/favourited_statuses/index.js b/app/soapbox/features/favourited_statuses/index.js index 233acf640..be87190fa 100644 --- a/app/soapbox/features/favourited_statuses/index.js +++ b/app/soapbox/features/favourited_statuses/index.js @@ -2,23 +2,55 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; 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 StatusList from '../../components/status_list'; import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { debounce } from 'lodash'; import MissingIndicator from 'soapbox/components/missing_indicator'; +import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts'; +import LoadingIndicator from '../../components/loading_indicator'; const mapStateToProps = (state, { params }) => { const username = params.username || ''; 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 { + isMyAccount, + statusIds: state.getIn(['status_lists', 'favourites', 'items']), + isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), + 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: (username.toLowerCase() === meUsername.toLowerCase()), - statusIds: state.getIn(['status_lists', 'favourites', 'items']), - isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), - hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), + 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']), }; }; @@ -36,17 +68,43 @@ class Favourites extends ImmutablePureComponent { }; componentDidMount() { - this.props.dispatch(fetchFavouritedStatuses()); + const { accountId, isMyAccount, username } = this.props; + + if (isMyAccount) + 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(() => { - this.props.dispatch(expandFavouritedStatuses()); + const { accountId, isMyAccount } = this.props; + + if (isMyAccount) { + this.props.dispatch(expandFavouritedStatuses()); + } else { + this.props.dispatch(expandAccountFavouritedStatuses(accountId)); + } }, 300, { leading: true }) 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 ( @@ -54,7 +112,27 @@ class Favourites extends ImmutablePureComponent { ); } - const emptyMessage = ; + if (accountId === -1) { + return ( + + + + ); + } + + if (unavailable) { + return ( + +
+ +
+
+ ); + } + + const emptyMessage = isMyAccount + ? + : ; return ( diff --git a/app/soapbox/features/pinned_statuses/index.js b/app/soapbox/features/pinned_statuses/index.js index dd5447051..bfa57417e 100644 --- a/app/soapbox/features/pinned_statuses/index.js +++ b/app/soapbox/features/pinned_statuses/index.js @@ -12,7 +12,7 @@ import MissingIndicator from 'soapbox/components/missing_indicator'; const mapStateToProps = (state, { params }) => { const username = params.username || ''; const me = state.get('me'); - const meUsername = state.getIn(['accounts', me, 'username']); + const meUsername = state.getIn(['accounts', me, 'username'], ''); return { isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()), statusIds: state.getIn(['status_lists', 'pins', 'items']), diff --git a/app/soapbox/features/reactions/index.js b/app/soapbox/features/reactions/index.js index 88f6c20f5..30de2d33a 100644 --- a/app/soapbox/features/reactions/index.js +++ b/app/soapbox/features/reactions/index.js @@ -73,7 +73,7 @@ class Reactions extends ImmutablePureComponent { render() { const { params, reactions, accounts, status } = this.props; - const { username, statusId, reaction } = params; + const { username, statusId } = params; const back = `/@${username}/posts/${statusId}`; @@ -95,7 +95,6 @@ class Reactions extends ImmutablePureComponent { const emptyMessage = ; - console.log(params.reaction); return ( { diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index b0199c449..70da9c18b 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -51,6 +51,8 @@ const messages = defineMessages({ 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' }, 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; @@ -279,6 +281,13 @@ class SoapboxConfig extends ImmutablePureComponent { checked={soapbox.get('greentext') === true} onChange={this.handleChange(['greentext'], (e) => e.target.checked)} /> + e.target.checked)} + />
diff --git a/app/soapbox/features/status/components/card.js b/app/soapbox/features/status/components/card.js index a0506a694..d2e349bca 100644 --- a/app/soapbox/features/status/components/card.js +++ b/app/soapbox/features/status/components/card.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Immutable from 'immutable'; +import { is, fromJS } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import punycode from 'punycode'; import classnames from 'classnames'; @@ -77,7 +77,7 @@ export default class Card extends React.PureComponent { }; componentDidUpdate(prevProps) { - if (!Immutable.is(prevProps.card, this.props.card)) { + if (!is(prevProps.card, this.props.card)) { this.setState({ embedded: false }); } } @@ -86,7 +86,7 @@ export default class Card extends React.PureComponent { const { card, onOpenMedia } = this.props; onOpenMedia( - Immutable.fromJS([ + fromJS([ { type: 'image', url: card.get('embed_url'), diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js index 03890cb26..c6262057e 100644 --- a/app/soapbox/features/status/index.js +++ b/app/soapbox/features/status/index.js @@ -1,4 +1,4 @@ -import Immutable from 'immutable'; +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -71,11 +71,11 @@ const makeMapStateToProps = () => { (_, { id }) => id, state => state.getIn(['contexts', 'inReplyTos']), ], (statusId, inReplyTos) => { - let ancestorsIds = Immutable.OrderedSet(); + let ancestorsIds = ImmutableOrderedSet(); let id = statusId; while (id) { - ancestorsIds = Immutable.OrderedSet([id]).union(ancestorsIds); + ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds); id = inReplyTos.get(id); } @@ -86,7 +86,7 @@ const makeMapStateToProps = () => { (_, { id }) => id, state => state.getIn(['contexts', 'replies']), ], (statusId, contextReplies) => { - let descendantsIds = Immutable.OrderedSet(); + let descendantsIds = ImmutableOrderedSet(); const ids = [statusId]; while (ids.length > 0) { @@ -109,8 +109,8 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => { const status = getStatus(state, { id: props.params.statusId }); - let ancestorsIds = Immutable.List(); - let descendantsIds = Immutable.List(); + let ancestorsIds = ImmutableOrderedSet(); + let descendantsIds = ImmutableOrderedSet(); if (status) { ancestorsIds = getAncestorsIds(state, { id: state.getIn(['contexts', 'inReplyTos', status.get('id')]) }); @@ -146,8 +146,8 @@ class Status extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, status: ImmutablePropTypes.map, - ancestorsIds: ImmutablePropTypes.list, - descendantsIds: ImmutablePropTypes.list, + ancestorsIds: ImmutablePropTypes.orderedSet, + descendantsIds: ImmutablePropTypes.orderedSet, intl: PropTypes.object.isRequired, askReplyConfirmation: PropTypes.bool, domain: PropTypes.string, diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index ab690d44e..9f8a90b0a 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -14,13 +14,13 @@ import FocalPointModal from './focal_point_modal'; import HotkeysModal from './hotkeys_modal'; import ComposeModal from './compose_modal'; import UnauthorizedModal from './unauthorized_modal'; -import CryptoDonateModal from './crypto_donate_modal'; import EditFederationModal from './edit_federation_modal'; import { MuteModal, ReportModal, EmbedModal, + CryptoDonateModal, ListEditor, ListAdder, } from '../../../features/ui/util/async-components'; @@ -41,7 +41,7 @@ const MODAL_COMPONENTS = { 'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }), 'COMPOSE': () => Promise.resolve({ default: ComposeModal }), 'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }), - 'CRYPTO_DONATE': () => Promise.resolve({ default: CryptoDonateModal }), + 'CRYPTO_DONATE': CryptoDonateModal, 'EDIT_FEDERATION': () => Promise.resolve({ default: EditFederationModal }), }; diff --git a/app/soapbox/features/ui/components/profile_info_panel.js b/app/soapbox/features/ui/components/profile_info_panel.js index 219925ca1..71c4e1386 100644 --- a/app/soapbox/features/ui/components/profile_info_panel.js +++ b/app/soapbox/features/ui/components/profile_info_panel.js @@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Icon from 'soapbox/components/icon'; 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 { displayFqn } from 'soapbox/utils/state'; 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; @@ -143,7 +144,15 @@ class ProfileInfoPanel extends ImmutablePureComponent { {fields.map((pair, i) => isTicker(pair.get('name', '')) ? ( - + + {Component => ( + + )} + ) : (
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 73644d6f4..af09692bb 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Switch, withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; import NotificationsContainer from './containers/notifications_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 { getFeatures } from 'soapbox/utils/features'; import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { Status, @@ -121,6 +123,7 @@ const mapStateToProps = state => { const me = state.get('me'); const account = state.getIn(['accounts', me]); const instance = state.get('instance'); + const soapbox = getSoapboxConfig(state); return { dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, @@ -129,6 +132,7 @@ const mapStateToProps = state => { me, account, features: getFeatures(instance), + soapbox, }; }; @@ -166,6 +170,7 @@ class SwitchingColumnsArea extends React.PureComponent { children: PropTypes.node, location: PropTypes.object, onLayoutChange: PropTypes.func.isRequired, + soapbox: ImmutablePropTypes.map.isRequired, }; state = { @@ -194,7 +199,8 @@ class SwitchingColumnsArea extends React.PureComponent { } render() { - const { children } = this.props; + const { children, soapbox } = this.props; + const authenticatedProfile = soapbox.get('authenticatedProfile'); return ( @@ -254,10 +260,10 @@ class SwitchingColumnsArea extends React.PureComponent { - - - - + + + + @@ -314,6 +320,7 @@ class UI extends React.PureComponent { streamingUrl: PropTypes.string, account: PropTypes.object, features: PropTypes.object.isRequired, + soapbox: ImmutablePropTypes.map.isRequired, }; state = { @@ -594,7 +601,7 @@ class UI extends React.PureComponent { } render() { - const { streamingUrl, features } = this.props; + const { streamingUrl, features, soapbox } = this.props; const { draggingOver, mobile } = this.state; const { intl, children, location, dropdownMenuIsOpen, me } = this.props; @@ -644,7 +651,7 @@ class UI extends React.PureComponent {
- + {children} diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index fb98c4f5b..a0d1e0f36 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -250,6 +250,18 @@ export function CryptoDonate() { 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() { return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled_statuses'); } @@ -265,3 +277,7 @@ export function FederationRestrictions() { export function Aliases() { return import(/* webpackChunkName: "features/aliases" */'../../aliases'); } + +export function ScheduleForm() { + return import(/* webpackChunkName: "features/compose" */'../../compose/components/schedule_form'); +} diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 5ae873e7d..997828ce5 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -312,6 +312,7 @@ "emoji_button.search_results": "Wyniki wyszukiwania", "emoji_button.symbols": "Symbole", "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_unavailable": "Profil niedostฤ™pny", "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.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.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.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.", diff --git a/app/soapbox/main.js b/app/soapbox/main.js index 599831e08..91822efef 100644 --- a/app/soapbox/main.js +++ b/app/soapbox/main.js @@ -1,6 +1,5 @@ 'use strict'; -import './wdyr'; import './precheck'; // FIXME: Push notifications are temporarily removed // import * as registerPushNotifications from './actions/push_notifications'; @@ -10,12 +9,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime'; import * as perf from './performance'; +import * as monitoring from './monitoring'; import ready from './ready'; import { NODE_ENV } from 'soapbox/build_config'; function main() { perf.start('main()'); + // Sentry + monitoring.start(); + ready(() => { const mountNode = document.getElementById('soapbox'); diff --git a/app/soapbox/middleware/sounds.js b/app/soapbox/middleware/sounds.js index a2fc7572f..6950e7618 100644 --- a/app/soapbox/middleware/sounds.js +++ b/app/soapbox/middleware/sounds.js @@ -1,8 +1,5 @@ 'use strict'; -import { join } from 'path'; -import { FE_SUBDIRECTORY } from 'soapbox/build_config'; - const createAudio = sources => { const audio = new Audio(); sources.forEach(({ type, src }) => { @@ -31,21 +28,21 @@ export default function soundsMiddleware() { const soundCache = { boop: createAudio([ { - src: join(FE_SUBDIRECTORY, '/sounds/boop.ogg'), + src: require('../../sounds/boop.ogg'), type: 'audio/ogg', }, { - src: join(FE_SUBDIRECTORY, '/sounds/boop.mp3'), + src: require('../../sounds/boop.mp3'), type: 'audio/mpeg', }, ]), chat: createAudio([ { - src: join(FE_SUBDIRECTORY, '/sounds/chat.oga'), + src: require('../../sounds/chat.oga'), type: 'audio/ogg', }, { - src: join(FE_SUBDIRECTORY, '/sounds/chat.mp3'), + src: require('../../sounds/chat.mp3'), type: 'audio/mpeg', }, ]), diff --git a/app/soapbox/monitoring.js b/app/soapbox/monitoring.js new file mode 100644 index 000000000..adb99a891 --- /dev/null +++ b/app/soapbox/monitoring.js @@ -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); +}; diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js index fc630d658..b39403c2f 100644 --- a/app/soapbox/pages/home_page.js +++ b/app/soapbox/pages/home_page.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; 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 Avatar from '../components/avatar'; 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 PromoPanel from 'soapbox/features/ui/components/promo_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 FeaturesPanel from 'soapbox/features/ui/components/features_panel'; import SignUpPanel from 'soapbox/features/ui/components/sign_up_panel'; @@ -58,7 +59,11 @@ class HomePage extends ImmutablePureComponent {
{showFundingPanel && } - {showCryptoDonatePanel && } + {showCryptoDonatePanel && ( + + {Component => } + + )}
diff --git a/app/soapbox/reducers/dropdown_menu.js b/app/soapbox/reducers/dropdown_menu.js index 36fd4f132..4cceee9f5 100644 --- a/app/soapbox/reducers/dropdown_menu.js +++ b/app/soapbox/reducers/dropdown_menu.js @@ -1,10 +1,10 @@ -import Immutable from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; import { DROPDOWN_MENU_OPEN, DROPDOWN_MENU_CLOSE, } 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) { switch (action.type) { diff --git a/app/soapbox/reducers/mutes.js b/app/soapbox/reducers/mutes.js index a96232dbd..56fd39fb3 100644 --- a/app/soapbox/reducers/mutes.js +++ b/app/soapbox/reducers/mutes.js @@ -1,12 +1,12 @@ -import Immutable from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; import { MUTES_INIT_MODAL, MUTES_TOGGLE_HIDE_NOTIFICATIONS, } from '../actions/mutes'; -const initialState = Immutable.Map({ - new: Immutable.Map({ +const initialState = ImmutableMap({ + new: ImmutableMap({ isSubmitting: false, account: null, notifications: true, diff --git a/app/soapbox/reducers/push_notifications.js b/app/soapbox/reducers/push_notifications.js index d68845908..e72952934 100644 --- a/app/soapbox/reducers/push_notifications.js +++ b/app/soapbox/reducers/push_notifications.js @@ -1,9 +1,9 @@ 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, - alerts: new Immutable.Map({ + alerts: new ImmutableMap({ follow: false, follow_request: false, favourite: false, @@ -19,11 +19,11 @@ export default function push_subscriptions(state = initialState, action) { switch(action.type) { case SET_SUBSCRIPTION: return state - .set('subscription', new Immutable.Map({ + .set('subscription', new ImmutableMap({ id: action.subscription.id, endpoint: action.subscription.endpoint, })) - .set('alerts', new Immutable.Map(action.subscription.alerts)) + .set('alerts', new ImmutableMap(action.subscription.alerts)) .set('isSubscribed', true); case SET_BROWSER_SUPPORT: return state.set('browserSupport', action.value); diff --git a/app/soapbox/reducers/status_lists.js b/app/soapbox/reducers/status_lists.js index 7ac8184ac..1953e636c 100644 --- a/app/soapbox/reducers/status_lists.js +++ b/app/soapbox/reducers/status_lists.js @@ -5,6 +5,12 @@ import { FAVOURITED_STATUSES_EXPAND_REQUEST, FAVOURITED_STATUSES_EXPAND_SUCCESS, 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'; import { BOOKMARKED_STATUSES_FETCH_REQUEST, @@ -101,6 +107,16 @@ export default function statusLists(state = initialState, action) { return normalizeList(state, 'favourites', action.statuses, action.next); case FAVOURITED_STATUSES_EXPAND_SUCCESS: 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_EXPAND_REQUEST: return setLoading(state, 'bookmarks', true); diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index 86a02479a..3d9d7260b 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -26,6 +26,8 @@ export const getFeatures = createSelector([ accountAliasesAPI: v.software === 'Pleroma', resetPasswordAPI: v.software === 'Pleroma', exposableReactions: features.includes('exposable_reactions'), + accountSubscriptions: v.software === 'Pleroma' && gte(v.version, '1.0.0'), + unrestrictedLists: v.software === 'Pleroma', }; }); diff --git a/app/soapbox/utils/resize_image.js b/app/soapbox/utils/resize_image.js index ffb4ef936..26bb36c76 100644 --- a/app/soapbox/utils/resize_image.js +++ b/app/soapbox/utils/resize_image.js @@ -1,6 +1,4 @@ /* eslint-disable no-case-declarations */ -import EXIF from 'exif-js'; - const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px const _browser_quirks = {}; @@ -115,14 +113,16 @@ const getOrientation = (img, type = 'image/png') => new Promise(resolve => { return; } - EXIF.getData(img, () => { - const orientation = EXIF.getTag(img, 'Orientation'); - if (orientation !== 1) { - dropOrientationIfNeeded(orientation).then(resolve).catch(() => resolve(orientation)); - } else { - resolve(orientation); - } - }); + import(/* webpackChunkName: "features/compose" */'exif-js').then(({ default: EXIF }) => { + EXIF.getData(img, () => { + const orientation = EXIF.getTag(img, 'Orientation'); + if (orientation !== 1) { + dropOrientationIfNeeded(orientation).then(resolve).catch(() => resolve(orientation)); + } else { + resolve(orientation); + } + }); + }).catch(() => {}); }); const processImage = (img, { width, height, orientation, type = 'image/png', name = 'resized.png' }) => new Promise(resolve => { diff --git a/app/soapbox/utils/static.js b/app/soapbox/utils/static.js new file mode 100644 index 000000000..e9fcd7001 --- /dev/null +++ b/app/soapbox/utils/static.js @@ -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); +}; diff --git a/app/soapbox/wdyr.js b/app/soapbox/wdyr.js deleted file mode 100644 index 13470ce33..000000000 --- a/app/soapbox/wdyr.js +++ /dev/null @@ -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); -} diff --git a/app/styles/components/datepicker.scss b/app/styles/components/datepicker.scss index ef8483b97..78a20b01f 100644 --- a/app/styles/components/datepicker.scss +++ b/app/styles/components/datepicker.scss @@ -156,6 +156,7 @@ display: flex !important; align-items: center !important; transition: 0.2s !important; + background: var(--foreground-color); &:hover { background-color: var(--background-color) !important; diff --git a/docs/development/build-config.md b/docs/development/build-config.md index 7cf038ce5..e5083b8a0 100644 --- a/docs/development/build-config.md +++ b/docs/development/build-config.md @@ -60,3 +60,18 @@ For example, if you want to host the build on `https://gleasonator.com/soapbox`, ```sh 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: `""` diff --git a/jest.config.js b/jest.config.js index d7b2fb44a..efe164457 100644 --- a/jest.config.js +++ b/jest.config.js @@ -31,4 +31,11 @@ module.exports = { '/app', ], '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', + }, }; diff --git a/package.json b/package.json index 018088f2d..2199accba 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "browserslist": [ "> 0.5%", "last 2 versions", - "Firefox ESR", + "not IE 11", "not dead" ], "dependencies": { @@ -51,7 +51,9 @@ "@fontsource/roboto": "^4.5.0", "@lcdp/offline-plugin": "^5.1.0", "@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", "autoprefixer": "^10.0.0", "axios": "^0.21.0", @@ -92,6 +94,7 @@ "intl-messageformat-parser": "^6.0.0", "intl-pluralrules": "^1.3.0", "is-nan": "^1.2.1", + "jest-transform-stub": "^2.0.0", "jsdoc": "~3.6.7", "lodash": "^4.7.11", "mark-loader": "^0.1.6", diff --git a/webpack/configuration.js b/webpack/configuration.js index 0089469c4..960e25e60 100644 --- a/webpack/configuration.js +++ b/webpack/configuration.js @@ -12,7 +12,6 @@ const settings = { test_root_path: `${FE_BUILD_DIR}-test`, cache_path: 'tmp/cache', 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' ], }; diff --git a/webpack/production.js b/webpack/production.js index 9968a2ca9..0e1643fee 100644 --- a/webpack/production.js +++ b/webpack/production.js @@ -1,11 +1,15 @@ // Note: You must restart bin/webpack-dev-server for changes to take effect console.log('Running in production mode'); // eslint-disable-line no-console +const { join } = require('path'); const { merge } = require('webpack-merge'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const OfflinePlugin = require('@lcdp/offline-plugin'); 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, { mode: 'production', devtool: 'source-map', @@ -25,37 +29,37 @@ module.exports = merge(sharedConfig, { new OfflinePlugin({ caches: { main: [':rest:'], - additional: [':externals:'], + additional: [ + ':externals:', + 'packs/images/32-*.png', // used in emoji-mart + ], optional: [ '**/locale_*.js', // don't fetch every locale; the user only needs one '**/*_polyfills-*.js', // the user may not need polyfills '**/*.chunk.js', // only cache chunks when needed + '**/*.chunk.css', '**/*.woff2', // the user may have system-fonts enabled - // images/audio can be cached on-demand + // images can be cached on-demand '**/*.png', - '**/*.jpg', - '**/*.jpeg', '**/*.svg', - '**/*.mp3', - '**/*.ogg', ], }, externals: [ - '/emoji/1f602.svg', // used for emoji picker dropdown - '/emoji/sheet_13.png', // used in emoji-mart + joinPublicPath('packs/emoji/1f602.svg'), // used for emoji picker dropdown // Default emoji reacts - '/emoji/1f44d.svg', // Thumbs up - '/emoji/2764.svg', // Heart - '/emoji/1f606.svg', // Laughing - '/emoji/1f62e.svg', // Surprised - '/emoji/1f622.svg', // Crying - '/emoji/1f629.svg', // Weary - '/emoji/1f621.svg', // Angry (Spinster) + joinPublicPath('packs/emoji/1f44d.svg'), // Thumbs up + joinPublicPath('packs/emoji/2764.svg'), // Heart + joinPublicPath('packs/emoji/1f606.svg'), // Laughing + joinPublicPath('packs/emoji/1f62e.svg'), // Surprised + joinPublicPath('packs/emoji/1f622.svg'), // Crying + joinPublicPath('packs/emoji/1f629.svg'), // Weary + joinPublicPath('packs/emoji/1f621.svg'), // Angry (Spinster) ], excludes: [ '**/*.gz', '**/*.map', + '**/*.LICENSE.txt', 'stats.json', 'report.html', 'instance/**/*', @@ -66,15 +70,24 @@ module.exports = merge(sharedConfig, { '**/*.woff', // Sounds return a 206 causing sw.js to crash // https://stackoverflow.com/a/66335638 - 'sounds/**/*', - // Don't cache index.html + '**/*.ogg', + '**/*.oga', + '**/*.mp3', + // Don't serve index.html + // https://github.com/bromite/bromite/issues/1294 '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: { - // entry: join(__dirname, '../app/soapbox/service_worker/entry.js'), - // cacheName: 'soapbox', - // minify: true, - // }, + ServiceWorker: { + // entry: join(__dirname, '../app/soapbox/service_worker/entry.js'), + // cacheName: 'soapbox', + minify: true, + }, + safeToUseOptionalCaches: true, }), ], }); diff --git a/webpack/rules/assets.js b/webpack/rules/assets.js new file mode 100644 index 000000000..2c6fb3f0d --- /dev/null +++ b/webpack/rules/assets.js @@ -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]', + }, +}]; diff --git a/webpack/rules/file.js b/webpack/rules/file.js deleted file mode 100644 index d23a0a977..000000000 --- a/webpack/rules/file.js +++ /dev/null @@ -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), - }, - }, - ], -}; diff --git a/webpack/rules/index.js b/webpack/rules/index.js index 91a4abd19..d3290659e 100644 --- a/webpack/rules/index.js +++ b/webpack/rules/index.js @@ -3,14 +3,14 @@ const git = require('./babel-git'); const gitRefresh = require('./git-refresh'); const buildConfig = require('./babel-build-config'); const css = require('./css'); -const file = require('./file'); +const assets = require('./assets'); const nodeModules = require('./node_modules'); // Webpack loaders are processed in reverse order // https://webpack.js.org/concepts/loaders/#loader-features // Lastly, process static files using file loader module.exports = [ - file, + ...assets, css, nodeModules, babel, diff --git a/webpack/shared.js b/webpack/shared.js index c0bbad931..fd01df7be 100644 --- a/webpack/shared.js +++ b/webpack/shared.js @@ -30,10 +30,9 @@ const makeHtmlConfig = (params = {}) => { }; module.exports = { - entry: Object.assign( - { application: resolve('app/application.js') }, - { styles: resolve(join(settings.source_path, 'styles/application.scss')) }, - ), + entry: { + application: resolve('app/application.js'), + }, output: { filename: 'packs/js/[name]-[chunkhash].js', @@ -65,7 +64,7 @@ module.exports = { }, module: { - rules: Object.keys(rules).map(key => rules[key]), + rules, }, plugins: [ @@ -90,13 +89,7 @@ module.exports = { new CopyPlugin({ patterns: [{ from: join(__dirname, '../node_modules/twemoji/assets/svg'), - to: join(output.path, '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'), + to: join(output.path, 'packs/emoji'), }, { from: join(__dirname, '../app/instance'), to: join(output.path, 'instance'), diff --git a/yarn.lock b/yarn.lock index 4876a41de..1da4cb448 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2039,6 +2039,81 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" 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": version "1.8.0" 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" 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": version "1.2.0" 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" 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: version "27.0.6" 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" 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" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -11665,6 +11738,11 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" 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: version "2.0.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242"