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"