Merge branch 'develop' into 'reactions-page'
# Conflicts: # app/soapbox/utils/features.js
This commit is contained in:
commit
c2fc7a0331
|
@ -1,6 +1,7 @@
|
|||
/node_modules/**
|
||||
/static/**
|
||||
/static-test/**
|
||||
/public/**
|
||||
/tmp/**
|
||||
/coverage/**
|
||||
!.eslintrc.js
|
||||
|
|
|
@ -8,20 +8,6 @@
|
|||
/.vs/
|
||||
yarn-error.log*
|
||||
|
||||
/static/packs
|
||||
/static/packs-test
|
||||
/static/emoji
|
||||
/static/index.html
|
||||
/static/index.html.gz
|
||||
/static/404.html
|
||||
/static/404.html.gz
|
||||
/static/manifest.json
|
||||
/static/manifest.json.gz
|
||||
/static/report.html
|
||||
/static/sw.js
|
||||
/static/instance/**
|
||||
!/static/instance/**.example
|
||||
!/static/instance/**.example.*
|
||||
!/static/instance/**.example/**
|
||||
/static-test
|
||||
/public
|
||||
/static/
|
||||
/static-test/
|
||||
/public/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
image: node:12
|
||||
image: node:14
|
||||
|
||||
variables:
|
||||
NODE_ENV: test
|
||||
|
@ -17,6 +17,7 @@ stages:
|
|||
- deploy
|
||||
|
||||
before_script:
|
||||
- env
|
||||
- yarn
|
||||
|
||||
lint-js:
|
||||
|
@ -85,10 +86,10 @@ docs-deploy:
|
|||
|
||||
pages:
|
||||
stage: deploy
|
||||
before_script: []
|
||||
script:
|
||||
- yarn build
|
||||
# artifacts are kept between jobs
|
||||
- mv static public
|
||||
- cp public/{index.html,404.html}
|
||||
variables:
|
||||
NODE_ENV: production
|
||||
artifacts:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
nodejs 14.17.6
|
|
@ -3,6 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
</head>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
const api = jest.requireActual('../api').default;
|
||||
const api = jest.requireActual('../api');
|
||||
let mocks = [];
|
||||
|
||||
export const __stub = func => mocks.push(func);
|
||||
|
@ -11,8 +11,10 @@ const setupMock = axios => {
|
|||
mocks.map(func => func(mock));
|
||||
};
|
||||
|
||||
export const staticClient = api.staticClient;
|
||||
|
||||
export default (...params) => {
|
||||
const axios = api(...params);
|
||||
const axios = api.default(...params);
|
||||
setupMock(axios);
|
||||
return axios;
|
||||
};
|
||||
|
|
|
@ -5,15 +5,17 @@ import {
|
|||
fetchAboutPage,
|
||||
} from '../about';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { __stub as stubApi } from 'soapbox/api';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { staticClient } from 'soapbox/api';
|
||||
import { mockStore } from 'soapbox/test_helpers';
|
||||
|
||||
describe('fetchAboutPage()', () => {
|
||||
it('creates the expected actions on success', () => {
|
||||
|
||||
stubApi(mock => {
|
||||
mock.onGet('/instance/about/index.html').reply(200, '<h1>Hello world</h1>');
|
||||
});
|
||||
const mock = new MockAdapter(staticClient);
|
||||
|
||||
mock.onGet('/instance/about/index.html')
|
||||
.reply(200, '<h1>Hello world</h1>');
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_ABOUT_PAGE_REQUEST, slug: 'index' },
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api from '../api';
|
||||
import { staticClient } from '../api';
|
||||
|
||||
export const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST';
|
||||
export const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS';
|
||||
|
@ -7,9 +7,10 @@ export const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL';
|
|||
export function fetchAboutPage(slug = 'index', locale) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale });
|
||||
return api(getState).get(`/instance/about/${slug}${locale ? `.${locale}` : ''}.html`).then(response => {
|
||||
dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html: response.data });
|
||||
return response.data;
|
||||
const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
|
||||
return staticClient.get(`/instance/about/${filename}`).then(({ data: html }) => {
|
||||
dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html });
|
||||
return html;
|
||||
}).catch(error => {
|
||||
dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error });
|
||||
throw error;
|
||||
|
|
|
@ -18,6 +18,7 @@ import { createApp } from 'soapbox/actions/apps';
|
|||
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||
|
||||
|
@ -162,10 +163,18 @@ export function logIn(intl, username, password) {
|
|||
return dispatch(createUserToken(username, password));
|
||||
}).catch(error => {
|
||||
if (error.response.data.error === 'mfa_required') {
|
||||
// If MFA is required, throw the error and handle it in the component.
|
||||
throw error;
|
||||
} else if(error.response.data.error) {
|
||||
} else if (error.response.data.error === 'invalid_grant') {
|
||||
// Mastodon returns this user-unfriendly error as a catch-all
|
||||
// for everything from "bad request" to "wrong password".
|
||||
// Assume our code is correct and it's a wrong password.
|
||||
dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials)));
|
||||
} else if (error.response.data.error) {
|
||||
// If the backend returns an error, display it.
|
||||
dispatch(snackbar.error(error.response.data.error));
|
||||
} else {
|
||||
// Return "wrong password" message.
|
||||
dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials)));
|
||||
}
|
||||
throw error;
|
||||
|
@ -177,6 +186,7 @@ export function logOut(intl) {
|
|||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const account = getLoggedInAccount(state);
|
||||
const standalone = isStandalone(state);
|
||||
|
||||
const params = {
|
||||
client_id: state.getIn(['auth', 'app', 'client_id']),
|
||||
|
@ -185,7 +195,7 @@ export function logOut(intl) {
|
|||
};
|
||||
|
||||
return dispatch(revokeOAuthToken(params)).finally(() => {
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account });
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.loggedOut)));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST';
|
||||
export const EXPORT_FOLLOWS_SUCCESS = 'EXPORT_FOLLOWS_SUCCESS';
|
||||
export const EXPORT_FOLLOWS_FAIL = 'EXPORT_FOLLOWS_FAIL';
|
||||
|
||||
export const EXPORT_BLOCKS_REQUEST = 'EXPORT_BLOCKS_REQUEST';
|
||||
export const EXPORT_BLOCKS_SUCCESS = 'EXPORT_BLOCKS_SUCCESS';
|
||||
export const EXPORT_BLOCKS_FAIL = 'EXPORT_BLOCKS_FAIL';
|
||||
|
||||
export const EXPORT_MUTES_REQUEST = 'EXPORT_MUTES_REQUEST';
|
||||
export const EXPORT_MUTES_SUCCESS = 'EXPORT_MUTES_SUCCESS';
|
||||
export const EXPORT_MUTES_FAIL = 'EXPORT_MUTES_FAIL';
|
||||
|
||||
const messages = defineMessages({
|
||||
blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' },
|
||||
followersSuccess: { id: 'export_data.success.followers', defaultMessage: 'Followers exported successfully' },
|
||||
mutesSuccess: { id: 'export_data.success.mutes', defaultMessage: 'Mutes exported successfully' },
|
||||
});
|
||||
|
||||
function fileExport(content, fileName) {
|
||||
const fileToDownload = document.createElement('a');
|
||||
|
||||
fileToDownload.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(content));
|
||||
fileToDownload.setAttribute('download', fileName);
|
||||
fileToDownload.style.display = 'none';
|
||||
document.body.appendChild(fileToDownload);
|
||||
fileToDownload.click();
|
||||
document.body.removeChild(fileToDownload);
|
||||
}
|
||||
|
||||
function listAccounts(state) {
|
||||
return async apiResponse => {
|
||||
const followings = apiResponse.data;
|
||||
let accounts = [];
|
||||
let next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
|
||||
while (next) {
|
||||
apiResponse = await api(state).get(next.uri);
|
||||
next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
|
||||
Array.prototype.push.apply(followings, apiResponse.data);
|
||||
}
|
||||
|
||||
accounts = followings.map(account => account.fqn);
|
||||
return [... new Set(accounts)];
|
||||
};
|
||||
}
|
||||
|
||||
export function exportFollows(intl) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: EXPORT_FOLLOWS_REQUEST });
|
||||
const me = getState().get('me');
|
||||
return api(getState)
|
||||
.get(`/api/v1/accounts/${me}/following?limit=40`)
|
||||
.then(listAccounts(getState))
|
||||
.then((followings) => {
|
||||
followings = followings.map(fqn => fqn + ',true');
|
||||
followings.unshift('Account address,Show boosts');
|
||||
fileExport(followings.join('\n'), 'export_followings.csv');
|
||||
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.followersSuccess)));
|
||||
dispatch({ type: EXPORT_FOLLOWS_SUCCESS });
|
||||
}).catch(error => {
|
||||
dispatch({ type: EXPORT_FOLLOWS_FAIL, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function exportBlocks(intl) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: EXPORT_BLOCKS_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/v1/blocks?limit=40')
|
||||
.then(listAccounts(getState))
|
||||
.then((blocks) => {
|
||||
fileExport(blocks.join('\n'), 'export_block.csv');
|
||||
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.blocksSuccess)));
|
||||
dispatch({ type: EXPORT_BLOCKS_SUCCESS });
|
||||
}).catch(error => {
|
||||
dispatch({ type: EXPORT_BLOCKS_FAIL, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function exportMutes(intl) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: EXPORT_MUTES_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/v1/mutes?limit=40')
|
||||
.then(listAccounts(getState))
|
||||
.then((mutes) => {
|
||||
fileExport(mutes.join('\n'), 'export_mutes.csv');
|
||||
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.mutesSuccess)));
|
||||
dispatch({ type: EXPORT_MUTES_SUCCESS });
|
||||
}).catch(error => {
|
||||
dispatch({ type: EXPORT_MUTES_FAIL, error });
|
||||
});
|
||||
};
|
||||
}
|
|
@ -9,16 +9,25 @@
|
|||
import { baseClient } from '../api';
|
||||
import { createApp } from 'soapbox/actions/apps';
|
||||
import { obtainOAuthToken } from 'soapbox/actions/oauth';
|
||||
import { authLoggedIn, verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||
import { parseBaseURL } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { fromJS } from 'immutable';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
const fetchExternalInstance = baseURL => {
|
||||
return baseClient(null, baseURL)
|
||||
.get('/api/v1/instance')
|
||||
.then(({ data: instance }) => fromJS(instance));
|
||||
.then(({ data: instance }) => fromJS(instance))
|
||||
.catch(error => {
|
||||
if (error.response.status === 401) {
|
||||
// Authenticated fetch is enabled.
|
||||
// Continue with a limited featureset.
|
||||
return ImmutableMap({ version: '0.0.0' });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function createAppAndRedirect(host) {
|
||||
|
@ -73,6 +82,7 @@ export function loginWithCode(code) {
|
|||
return dispatch(obtainOAuthToken(params, baseURL))
|
||||
.then(token => dispatch(authLoggedIn(token)))
|
||||
.then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
|
||||
.then(account => dispatch(switchAccount(account.id)))
|
||||
.then(() => window.location.href = '/');
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export function obtainOAuthToken(params, baseURL) {
|
|||
dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token });
|
||||
return token;
|
||||
}).catch(error => {
|
||||
dispatch({ type: OAUTH_TOKEN_CREATE_FAIL, params, error });
|
||||
dispatch({ type: OAUTH_TOKEN_CREATE_FAIL, params, error, skipAlert: true });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api from '../api';
|
||||
import api, { staticClient } from '../api';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { createSelector } from 'reselect';
|
||||
|
@ -76,7 +76,7 @@ export function fetchSoapboxConfig() {
|
|||
|
||||
export function fetchSoapboxJson() {
|
||||
return (dispatch, getState) => {
|
||||
api(getState).get('/instance/soapbox.json').then(({ data }) => {
|
||||
staticClient.get('/instance/soapbox.json').then(({ data }) => {
|
||||
if (!isObject(data)) throw 'soapbox.json failed';
|
||||
dispatch(importSoapboxConfig(data));
|
||||
}).catch(error => {
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
/**
|
||||
* API: HTTP client and utilities.
|
||||
* @see {@link https://github.com/axios/axios}
|
||||
* @module soapbox/api
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth';
|
||||
import { createSelector } from 'reselect';
|
||||
import { BACKEND_URL } from 'soapbox/build_config';
|
||||
import { BACKEND_URL, FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
import { isURL } from 'soapbox/utils/auth';
|
||||
|
||||
/**
|
||||
Parse Link headers, mostly for pagination.
|
||||
@see {@link https://www.npmjs.com/package/http-link-header}
|
||||
@param {object} response - Axios response object
|
||||
@returns {object} Link object
|
||||
*/
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
if (!value) return { refs: [] };
|
||||
|
@ -33,6 +44,12 @@ const getAuthBaseURL = createSelector([
|
|||
return baseURL !== window.location.origin ? baseURL : '';
|
||||
});
|
||||
|
||||
/**
|
||||
* Base client for HTTP requests.
|
||||
* @param {string} accessToken
|
||||
* @param {string} baseURL
|
||||
* @returns {object} Axios instance
|
||||
*/
|
||||
export const baseClient = (accessToken, baseURL = '') => {
|
||||
return axios.create({
|
||||
// When BACKEND_URL is set, always use it.
|
||||
|
@ -45,6 +62,23 @@ export const baseClient = (accessToken, baseURL = '') => {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dumb client for grabbing static files.
|
||||
* It uses FE_SUBDIRECTORY and parses JSON if possible.
|
||||
* No authorization is needed.
|
||||
*/
|
||||
export const staticClient = axios.create({
|
||||
baseURL: FE_SUBDIRECTORY,
|
||||
transformResponse: [maybeParseJSON],
|
||||
});
|
||||
|
||||
/**
|
||||
* Stateful API client.
|
||||
* Uses credentials from the Redux store if available.
|
||||
* @param {function} getState - Must return the Redux state
|
||||
* @param {string} authType - Either 'user' or 'app'
|
||||
* @returns {object} Axios instance
|
||||
*/
|
||||
export default (getState, authType = 'user') => {
|
||||
const state = getState();
|
||||
const accessToken = getToken(state, authType);
|
||||
|
|
|
@ -4,20 +4,38 @@
|
|||
* @module soapbox/build_config
|
||||
*/
|
||||
|
||||
const { BACKEND_URL } = process.env;
|
||||
const { trim, trimEnd } = require('lodash');
|
||||
|
||||
const {
|
||||
NODE_ENV,
|
||||
BACKEND_URL,
|
||||
FE_SUBDIRECTORY,
|
||||
FE_BUILD_DIR,
|
||||
} = process.env;
|
||||
|
||||
const sanitizeURL = url => {
|
||||
try {
|
||||
return new URL(url).toString();
|
||||
return trimEnd(new URL(url).toString(), '/');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeBasename = path => {
|
||||
return `/${trim(path, '/')}`;
|
||||
};
|
||||
|
||||
const sanitizePath = path => {
|
||||
return trim(path, '/');
|
||||
};
|
||||
|
||||
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
|
||||
// inconsistent behavior in dev mode
|
||||
const sanitize = obj => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
module.exports = sanitize({
|
||||
NODE_ENV: NODE_ENV || 'development',
|
||||
BACKEND_URL: sanitizeURL(BACKEND_URL),
|
||||
FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY),
|
||||
FE_BUILD_DIR: sanitizePath(FE_BUILD_DIR) || 'static',
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
|||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<button
|
||||
|
@ -30,7 +30,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
|||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<button
|
||||
|
@ -41,7 +41,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
|||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<button
|
||||
|
@ -52,7 +52,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
|||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<button
|
||||
|
@ -63,7 +63,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
|||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<button
|
||||
|
@ -74,7 +74,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
|||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
||||
|
||||
const assetHost = process.env.CDN_HOST || '';
|
||||
import { join } from 'path';
|
||||
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
|
||||
export default class AutosuggestEmoji extends React.PureComponent {
|
||||
|
||||
|
@ -23,7 +23,7 @@ export default class AutosuggestEmoji extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
url = `${assetHost}/emoji/${mapping.filename}.svg`;
|
||||
url = join(FE_SUBDIRECTORY, 'emoji', `${mapping.filename}.svg`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -35,21 +35,38 @@ class EmojiSelector extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleKeyUp = i => e => {
|
||||
_selectPreviousEmoji = i => {
|
||||
if (i !== 0) {
|
||||
this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`).focus();
|
||||
} else {
|
||||
this.node.querySelector('.emoji-react-selector__emoji:last-child').focus();
|
||||
}
|
||||
};
|
||||
|
||||
_selectNextEmoji = i => {
|
||||
if (i !== this.props.allowedEmoji.size - 1) {
|
||||
this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`).focus();
|
||||
} else {
|
||||
this.node.querySelector('.emoji-react-selector__emoji:first-child').focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = i => e => {
|
||||
const { onUnfocus } = this.props;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) this._selectPreviousEmoji(i);
|
||||
else this._selectNextEmoji(i);
|
||||
break;
|
||||
case 'Left':
|
||||
case 'ArrowLeft':
|
||||
if (i !== 0) {
|
||||
this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`).focus();
|
||||
}
|
||||
this._selectPreviousEmoji(i);
|
||||
break;
|
||||
case 'Right':
|
||||
case 'ArrowRight':
|
||||
if (i !== this.props.allowedEmoji.size - 1) {
|
||||
this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`).focus();
|
||||
}
|
||||
this._selectNextEmoji(i);
|
||||
break;
|
||||
case 'Escape':
|
||||
onUnfocus();
|
||||
|
@ -94,7 +111,7 @@ class EmojiSelector extends ImmutablePureComponent {
|
|||
className='emoji-react-selector__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
||||
onClick={this.handleReact(emoji)}
|
||||
onKeyUp={this.handleKeyUp(i, emoji)}
|
||||
onKeyDown={this.handleKeyDown(i, emoji)}
|
||||
tabIndex={(visible || focused) ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -19,6 +19,7 @@ import ThemeToggle from '../features/ui/components/theme_toggle_container';
|
|||
import { fetchOwnAccounts } from 'soapbox/actions/auth';
|
||||
import { is as ImmutableIs } from 'immutable';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
followers: { id: 'account.followers', defaultMessage: 'Followers' },
|
||||
|
@ -53,6 +54,9 @@ const makeMapStateToProps = () => {
|
|||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const instance = state.get('instance');
|
||||
|
||||
const features = getFeatures(instance);
|
||||
const soapbox = getSoapboxConfig(state);
|
||||
|
||||
return {
|
||||
|
@ -61,6 +65,7 @@ const makeMapStateToProps = () => {
|
|||
donateUrl: state.getIn(['patron', 'instance', 'url']),
|
||||
hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string',
|
||||
otherAccounts: getOtherAccounts(state),
|
||||
features,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -93,6 +98,7 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
otherAccounts: ImmutablePropTypes.list,
|
||||
sidebarOpen: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
features: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -149,7 +155,7 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { sidebarOpen, intl, account, onClickLogOut, donateUrl, otherAccounts, hasCrypto } = this.props;
|
||||
const { sidebarOpen, intl, account, onClickLogOut, donateUrl, otherAccounts, hasCrypto, features } = this.props;
|
||||
const { switcher } = this.state;
|
||||
if (!account) return null;
|
||||
const acct = account.get('acct');
|
||||
|
@ -231,10 +237,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon id='ban' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.blocks)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/domain_blocks' onClick={this.handleClose}>
|
||||
{features.federating && <NavLink className='sidebar-menu-item' to='/domain_blocks' onClick={this.handleClose}>
|
||||
<Icon id='ban' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.domain_blocks)}</span>
|
||||
</NavLink>
|
||||
</NavLink>}
|
||||
<NavLink className='sidebar-menu-item' to='/mutes' onClick={this.handleClose}>
|
||||
<Icon id='times-circle' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
||||
|
@ -259,10 +265,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon id='cloud-upload' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/settings/aliases' onClick={this.handleClose}>
|
||||
{(features.federating && features.accountAliasesAPI) && <NavLink className='sidebar-menu-item' to='/settings/aliases' onClick={this.handleClose}>
|
||||
<Icon id='suitcase' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.account_aliases)}</span>
|
||||
</NavLink>
|
||||
</NavLink>}
|
||||
<NavLink className='sidebar-menu-item' to='/auth/edit' onClick={this.handleClose}>
|
||||
<Icon id='lock' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.security)}</span>
|
||||
|
|
|
@ -26,15 +26,21 @@ import { getSettings } from 'soapbox/actions/settings';
|
|||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||
import messages from 'soapbox/locales/messages';
|
||||
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
|
||||
const validLocale = locale => Object.keys(messages).includes(locale);
|
||||
|
||||
export const store = configureStore();
|
||||
|
||||
store.dispatch(preload());
|
||||
store.dispatch(fetchMe());
|
||||
store.dispatch(fetchInstance());
|
||||
store.dispatch(fetchSoapboxConfig());
|
||||
|
||||
store.dispatch(fetchMe())
|
||||
.then(() => {
|
||||
// Postpone for authenticated fetch
|
||||
store.dispatch(fetchInstance());
|
||||
store.dispatch(fetchSoapboxConfig());
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const me = state.get('me');
|
||||
|
@ -142,7 +148,7 @@ class SoapboxMount extends React.PureComponent {
|
|||
))}
|
||||
<meta name='theme-color' content={this.props.brandColor} />
|
||||
</Helmet>
|
||||
<BrowserRouter>
|
||||
<BrowserRouter basename={FE_SUBDIRECTORY}>
|
||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<Switch>
|
||||
{!me && <Route exact path='/' component={PublicLayout} />}
|
||||
|
|
|
@ -54,6 +54,7 @@ const makeMapStateToProps = () => {
|
|||
unavailable,
|
||||
accountUsername,
|
||||
accountApId,
|
||||
isBlocked,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
|
||||
featuredStatusIds: showPins ? getStatusIds(state, { type: `account:${accountId}:pinned`, prefix: 'account_timeline' }) : ImmutableOrderedSet(),
|
||||
|
@ -142,7 +143,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isBlocked, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
|
@ -165,7 +166,8 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
{isBlocked ? <FormattedMessage id='empty_column.account_blocked' defaultMessage='You are blocked by @{accountUsername}.' values={{ accountUsername: accountUsername }} />
|
||||
: <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LoginForm /> renders correctly 1`] = `
|
||||
exports[`<LoginForm /> renders for Mastodon 1`] = `
|
||||
<form
|
||||
className="simple_form new_user"
|
||||
method="post"
|
||||
>
|
||||
<fieldset>
|
||||
<div
|
||||
className="fields-group"
|
||||
>
|
||||
<div
|
||||
className="input email user_email"
|
||||
>
|
||||
<input
|
||||
aria-label="Username"
|
||||
autoComplete="off"
|
||||
className="string email"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
required={true}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="input password user_password"
|
||||
>
|
||||
<input
|
||||
aria-label="Password"
|
||||
autoComplete="off"
|
||||
className="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required={true}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className="hint subtle-hint"
|
||||
>
|
||||
<a
|
||||
href="/auth/password/new"
|
||||
>
|
||||
Trouble logging in?
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div
|
||||
className="actions"
|
||||
>
|
||||
<button
|
||||
className="btn button button-primary"
|
||||
name="button"
|
||||
type="submit"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`<LoginForm /> renders for Pleroma 1`] = `
|
||||
<form
|
||||
className="simple_form new_user"
|
||||
method="post"
|
||||
|
|
|
@ -63,67 +63,3 @@ exports[`<LoginPage /> renders correctly on load 1`] = `
|
|||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`<LoginPage /> renders correctly on load 2`] = `
|
||||
<form
|
||||
className="simple_form new_user"
|
||||
method="post"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<fieldset
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="fields-group"
|
||||
>
|
||||
<div
|
||||
className="input email user_email"
|
||||
>
|
||||
<input
|
||||
aria-label="Username"
|
||||
autoComplete="off"
|
||||
className="string email"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
required={true}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="input password user_password"
|
||||
>
|
||||
<input
|
||||
aria-label="Password"
|
||||
autoComplete="off"
|
||||
className="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required={true}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className="hint subtle-hint"
|
||||
>
|
||||
<a
|
||||
href="/auth/reset_password"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Trouble logging in?
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div
|
||||
className="actions"
|
||||
>
|
||||
<button
|
||||
className="btn button button-primary"
|
||||
name="button"
|
||||
type="submit"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
|
|
@ -1,11 +1,29 @@
|
|||
import React from 'react';
|
||||
import LoginForm from '../login_form';
|
||||
import { createComponent } from 'soapbox/test_helpers';
|
||||
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
||||
import rootReducer from 'soapbox/reducers';
|
||||
|
||||
describe('<LoginForm />', () => {
|
||||
it('renders correctly', () => {
|
||||
|
||||
it('renders for Pleroma', () => {
|
||||
const state = rootReducer(undefined, {})
|
||||
.update('instance', instance => instance.set('version', '2.7.2 (compatible; Pleroma 2.3.0)'));
|
||||
const store = mockStore(state);
|
||||
|
||||
expect(createComponent(
|
||||
<LoginForm />,
|
||||
{ store },
|
||||
).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders for Mastodon', () => {
|
||||
const state = rootReducer(undefined, {})
|
||||
.update('instance', instance => instance.set('version', '3.0.0'));
|
||||
const store = mockStore(state);
|
||||
|
||||
expect(createComponent(
|
||||
<LoginForm />,
|
||||
{ store },
|
||||
).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
import React from 'react';
|
||||
import LoginPage from '../login_page';
|
||||
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
// import { __stub as stubApi } from 'soapbox/api';
|
||||
// import { logIn } from 'soapbox/actions/auth';
|
||||
import rootReducer from 'soapbox/reducers';
|
||||
|
||||
describe('<LoginPage />', () => {
|
||||
beforeEach(() => {
|
||||
const store = mockStore(ImmutableMap({}));
|
||||
return store;
|
||||
});
|
||||
|
||||
it('renders correctly on load', () => {
|
||||
expect(createComponent(
|
||||
<LoginPage />,
|
||||
).toJSON()).toMatchSnapshot();
|
||||
const state = rootReducer(undefined, {})
|
||||
.set('me', '1234')
|
||||
.update('instance', instance => instance.set('version', '2.7.2 (compatible; Pleroma 2.3.0)'));
|
||||
const store = mockStore(state);
|
||||
|
||||
const store = mockStore(ImmutableMap({ me: '1234' }));
|
||||
expect(createComponent(
|
||||
<LoginPage />,
|
||||
{ store },
|
||||
|
|
|
@ -3,18 +3,30 @@ import { connect } from 'react-redux';
|
|||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { getBaseURL } from 'soapbox/utils/state';
|
||||
|
||||
const messages = defineMessages({
|
||||
username: { id: 'login.fields.username_placeholder', defaultMessage: 'Username' },
|
||||
password: { id: 'login.fields.password_placeholder', defaultMessage: 'Password' },
|
||||
});
|
||||
|
||||
export default @connect()
|
||||
const mapStateToProps = state => {
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
|
||||
return {
|
||||
baseURL: getBaseURL(state),
|
||||
hasResetPasswordAPI: features.resetPasswordAPI,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class LoginForm extends ImmutablePureComponent {
|
||||
|
||||
render() {
|
||||
const { intl, isLoading, handleSubmit } = this.props;
|
||||
const { intl, isLoading, handleSubmit, baseURL, hasResetPasswordAPI } = this.props;
|
||||
|
||||
return (
|
||||
<form className='simple_form new_user' method='post' onSubmit={handleSubmit}>
|
||||
|
@ -43,9 +55,15 @@ class LoginForm extends ImmutablePureComponent {
|
|||
/>
|
||||
</div>
|
||||
<p className='hint subtle-hint'>
|
||||
<Link to='/auth/reset_password'>
|
||||
<FormattedMessage id='login.reset_password_hint' defaultMessage='Trouble logging in?' />
|
||||
</Link>
|
||||
{hasResetPasswordAPI ? (
|
||||
<Link to='/auth/reset_password'>
|
||||
<FormattedMessage id='login.reset_password_hint' defaultMessage='Trouble logging in?' />
|
||||
</Link>
|
||||
) : (
|
||||
<a href={`${baseURL}/auth/password/new`}>
|
||||
<FormattedMessage id='login.reset_password_hint' defaultMessage='Trouble logging in?' />
|
||||
</a>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
@ -6,10 +6,13 @@ import { injectIntl } from 'react-intl';
|
|||
import LoginForm from './login_form';
|
||||
import OtpAuthForm from './otp_auth_form';
|
||||
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
me: state.get('me'),
|
||||
isLoading: false,
|
||||
standalone: isStandalone(state),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -38,7 +41,9 @@ class LoginPage extends ImmutablePureComponent {
|
|||
const { dispatch, intl, me } = this.props;
|
||||
const { username, password } = this.getFormData(event.target);
|
||||
dispatch(logIn(intl, username, password)).then(({ access_token }) => {
|
||||
return dispatch(verifyCredentials(access_token));
|
||||
return dispatch(verifyCredentials(access_token))
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance()));
|
||||
}).then(account => {
|
||||
this.setState({ shouldRedirect: true });
|
||||
if (typeof me === 'string') {
|
||||
|
@ -55,8 +60,11 @@ class LoginPage extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { standalone } = this.props;
|
||||
const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state;
|
||||
|
||||
if (standalone) return <Redirect to='/auth/external' />;
|
||||
|
||||
if (shouldRedirect) return <Redirect to='/' />;
|
||||
|
||||
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;
|
||||
|
|
|
@ -7,6 +7,8 @@ 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';
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
|
@ -25,10 +27,9 @@ const messages = defineMessages({
|
|||
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
|
||||
});
|
||||
|
||||
const assetHost = process.env.CDN_HOST || '';
|
||||
let EmojiPicker, Emoji; // load asynchronously
|
||||
|
||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
|
||||
const backgroundImageFn = () => join(FE_SUBDIRECTORY, 'emoji', 'sheet_13.png');
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
const categoriesSort = [
|
||||
|
@ -358,7 +359,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
<img
|
||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||
alt='🙂'
|
||||
src={`${assetHost}/emoji/1f602.svg`}
|
||||
src={join(FE_SUBDIRECTORY, 'emoji', '1f602.svg')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import unicodeMapping from './emoji_unicode_mapping_light';
|
||||
import Trie from 'substring-trie';
|
||||
import { join } from 'path';
|
||||
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
|
||||
const trie = new Trie(Object.keys(unicodeMapping));
|
||||
|
||||
const assetHost = process.env.CDN_HOST || '';
|
||||
|
||||
const emojify = (str, customEmojis = {}, autoplay = false) => {
|
||||
const tagCharsWithoutEmojis = '<&';
|
||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
||||
|
@ -62,7 +62,7 @@ const emojify = (str, customEmojis = {}, autoplay = false) => {
|
|||
} else { // matched to unicode emoji
|
||||
const { filename, shortCode } = unicodeMapping[match];
|
||||
const title = shortCode ? `:${shortCode}:` : '';
|
||||
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`;
|
||||
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${join(FE_SUBDIRECTORY, 'emoji', `${filename}.svg`)}" />`;
|
||||
rend = i + match.length;
|
||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
||||
if (str.codePointAt(rend) === 65038) {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SimpleForm } from 'soapbox/features/forms';
|
||||
|
||||
export default @connect()
|
||||
@injectIntl
|
||||
class CSVExporter extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
action: PropTypes.func.isRequired,
|
||||
messages: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
handleClick = (event) => {
|
||||
const { dispatch, action, intl } = this.props;
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
dispatch(action(intl)).then(() => {
|
||||
this.setState({ isLoading: false });
|
||||
}).catch((error) => {
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, messages } = this.props;
|
||||
|
||||
return (
|
||||
<SimpleForm>
|
||||
<h2 className='export-title'>{intl.formatMessage(messages.input_label)}</h2>
|
||||
<div>
|
||||
<p className='export-hint hint'>{intl.formatMessage(messages.input_hint)}</p>
|
||||
<button name='button' type='button' className='button button-primary' onClick={this.handleClick}>
|
||||
{intl.formatMessage(messages.submit)}
|
||||
</button>
|
||||
</div>
|
||||
</SimpleForm>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import Column from '../ui/components/column';
|
||||
import {
|
||||
exportFollows,
|
||||
exportBlocks,
|
||||
exportMutes,
|
||||
} from 'soapbox/actions/export_data';
|
||||
import CSVExporter from './components/csv_exporter';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.export_data', defaultMessage: 'Export data' },
|
||||
submit: { id: 'export_data.actions.export', defaultMessage: 'Export' },
|
||||
});
|
||||
|
||||
const followMessages = defineMessages({
|
||||
input_label: { id: 'export_data.follows_label', defaultMessage: 'Follows' },
|
||||
input_hint: { id: 'export_data.hints.follows', defaultMessage: 'Get a CSV file containing a list of followed accounts' },
|
||||
submit: { id: 'export_data.actions.export_follows', defaultMessage: 'Export follows' },
|
||||
});
|
||||
|
||||
const blockMessages = defineMessages({
|
||||
input_label: { id: 'export_data.blocks_label', defaultMessage: 'Blocks' },
|
||||
input_hint: { id: 'export_data.hints.blocks', defaultMessage: 'Get a CSV file containing a list of blocked accounts' },
|
||||
submit: { id: 'export_data.actions.export_blocks', defaultMessage: 'Export blocks' },
|
||||
});
|
||||
|
||||
const muteMessages = defineMessages({
|
||||
input_label: { id: 'export_data.mutes_label', defaultMessage: 'Mutes' },
|
||||
input_hint: { id: 'export_data.hints.mutes', defaultMessage: 'Get a CSV file containing a list of muted accounts' },
|
||||
submit: { id: 'export_data.actions.export_mutes', defaultMessage: 'Export mutes' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
features: getFeatures(state.get('instance')),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ExportData extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
features: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Column icon='cloud-download' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<CSVExporter action={exportFollows} messages={followMessages} />
|
||||
<CSVExporter action={exportBlocks} messages={blockMessages} />
|
||||
<CSVExporter action={exportMutes} messages={muteMessages} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||
|
@ -80,13 +81,15 @@ class GroupTimeline extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const acct = account ? account.get('acct') : '';
|
||||
|
||||
return (
|
||||
<div>
|
||||
{relationships.get('member') && (
|
||||
<div className='timeline-compose-block'>
|
||||
<div className='timeline-compose-block__avatar'>
|
||||
<Link className='timeline-compose-block__avatar' to={`/@${acct}`}>
|
||||
<Avatar account={account} size={46} />
|
||||
</div>
|
||||
</Link>
|
||||
<ComposeFormContainer group={group} shouldCondense autoFocus={false} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -9,6 +9,7 @@ import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
|
@ -55,7 +56,9 @@ class Header extends ImmutablePureComponent {
|
|||
const { dispatch, intl } = this.props;
|
||||
const { username, password } = this.getFormData(event.target);
|
||||
dispatch(logIn(intl, username, password)).then(({ access_token }) => {
|
||||
return dispatch(verifyCredentials(access_token));
|
||||
return dispatch(verifyCredentials(access_token))
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance()));
|
||||
}).catch(error => {
|
||||
if (error.response.data.error === 'mfa_required') {
|
||||
this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token });
|
||||
|
|
|
@ -9,19 +9,7 @@ import Footer from './components/footer';
|
|||
import LandingPage from '../landing_page';
|
||||
import AboutPage from '../about';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { isPrerendered } from 'soapbox/precheck';
|
||||
|
||||
const validInstance = state => {
|
||||
const v = state.getIn(['instance', 'version']);
|
||||
return v && typeof v === 'string' && v !== '0.0.0';
|
||||
};
|
||||
|
||||
const isStandalone = state => {
|
||||
const hasInstance = validInstance(state);
|
||||
const instanceFetchFailed = state.getIn(['meta', 'instance_fetch_failed']);
|
||||
|
||||
return !isPrerendered && !hasInstance && instanceFetchFailed;
|
||||
};
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
soapbox: getSoapboxConfig(state),
|
||||
|
|
|
@ -43,6 +43,7 @@ const LinkFooter = ({ onOpenHotkeys, account, federating, showAliases, onClickLo
|
|||
<li><Link to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></Link></li>
|
||||
{isAdmin(account) && <li><a href='/pleroma/admin'><FormattedMessage id='navigation_bar.admin_settings' defaultMessage='AdminFE' /></a></li>}
|
||||
{isAdmin(account) && <li><Link to='/soapbox/config'><FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' /></Link></li>}
|
||||
<li><Link to='/settings/export'><FormattedMessage id='navigation_bar.export_data' defaultMessage='Export data' /></Link></li>
|
||||
<li><Link to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></Link></li>
|
||||
{(federating && showAliases) && <li><Link to='/settings/aliases'><FormattedMessage id='navigation_bar.account_aliases' defaultMessage='Account aliases' /></Link></li>}
|
||||
<li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li>
|
||||
|
|
|
@ -85,6 +85,7 @@ import {
|
|||
Preferences,
|
||||
EditProfile,
|
||||
SoapboxConfig,
|
||||
ExportData,
|
||||
ImportData,
|
||||
Backups,
|
||||
PasswordReset,
|
||||
|
@ -270,6 +271,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<Redirect exact from='/settings' to='/settings/preferences' />
|
||||
<WrappedRoute path='/settings/preferences' page={DefaultPage} component={Preferences} content={children} />
|
||||
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
|
||||
<WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} />
|
||||
<WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />
|
||||
<WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />
|
||||
<WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} />
|
||||
|
|
|
@ -190,6 +190,10 @@ export function SoapboxConfig() {
|
|||
return import(/* webpackChunkName: "features/soapbox_config" */'../../soapbox_config');
|
||||
}
|
||||
|
||||
export function ExportData() {
|
||||
return import(/* webpackChunkName: "features/export_data" */ '../../export_data');
|
||||
}
|
||||
|
||||
export function ImportData() {
|
||||
return import(/* webpackChunkName: "features/import_data" */'../../import_data');
|
||||
}
|
||||
|
|
|
@ -88,6 +88,14 @@ export const getPointerPosition = (el, event) => {
|
|||
return position;
|
||||
};
|
||||
|
||||
export const fileNameFromURL = str => {
|
||||
const url = new URL(str);
|
||||
const pathname = url.pathname;
|
||||
const index = pathname.lastIndexOf('/');
|
||||
|
||||
return pathname.substring(index + 1);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
displayMedia: getSettings(state).get('displayMedia'),
|
||||
});
|
||||
|
|
|
@ -8,9 +8,10 @@ import './precheck';
|
|||
import { default as Soapbox } from './containers/soapbox';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime';
|
||||
import * as perf from './performance';
|
||||
import ready from './ready';
|
||||
|
||||
const perf = require('./performance');
|
||||
import { NODE_ENV } from 'soapbox/build_config';
|
||||
|
||||
function main() {
|
||||
perf.start('main()');
|
||||
|
@ -19,9 +20,10 @@ function main() {
|
|||
const mountNode = document.getElementById('soapbox');
|
||||
|
||||
ReactDOM.render(<Soapbox />, mountNode);
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
if (NODE_ENV === 'production') {
|
||||
// avoid offline in dev mode because it's harder to debug
|
||||
require('offline-plugin/runtime').install();
|
||||
OfflinePluginRuntime.install();
|
||||
// FIXME: Push notifications are temporarily removed
|
||||
// store.dispatch(registerPushNotifications.register());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
import { join } from 'path';
|
||||
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
|
||||
const createAudio = sources => {
|
||||
const audio = new Audio();
|
||||
sources.forEach(({ type, src }) => {
|
||||
|
@ -28,21 +31,21 @@ export default function soundsMiddleware() {
|
|||
const soundCache = {
|
||||
boop: createAudio([
|
||||
{
|
||||
src: '/sounds/boop.ogg',
|
||||
src: join(FE_SUBDIRECTORY, '/sounds/boop.ogg'),
|
||||
type: 'audio/ogg',
|
||||
},
|
||||
{
|
||||
src: '/sounds/boop.mp3',
|
||||
src: join(FE_SUBDIRECTORY, '/sounds/boop.mp3'),
|
||||
type: 'audio/mpeg',
|
||||
},
|
||||
]),
|
||||
chat: createAudio([
|
||||
{
|
||||
src: '/sounds/chat.oga',
|
||||
src: join(FE_SUBDIRECTORY, '/sounds/chat.oga'),
|
||||
type: 'audio/ogg',
|
||||
},
|
||||
{
|
||||
src: '/sounds/chat.mp3',
|
||||
src: join(FE_SUBDIRECTORY, '/sounds/chat.mp3'),
|
||||
type: 'audio/mpeg',
|
||||
},
|
||||
]),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
|
||||
import Avatar from '../components/avatar';
|
||||
|
@ -46,6 +47,8 @@ class HomePage extends ImmutablePureComponent {
|
|||
render() {
|
||||
const { me, children, account, showFundingPanel, showCryptoDonatePanel, cryptoLimit, showTrendsPanel, showWhoToFollowPanel } = this.props;
|
||||
|
||||
const acct = account ? account.get('acct') : '';
|
||||
|
||||
return (
|
||||
<div className='page'>
|
||||
<div className='page__columns'>
|
||||
|
@ -62,9 +65,9 @@ class HomePage extends ImmutablePureComponent {
|
|||
<div className='columns-area__panels__main'>
|
||||
<div className='columns-area columns-area--mobile'>
|
||||
{me && <div className='timeline-compose-block' ref={this.composeBlock}>
|
||||
<div className='timeline-compose-block__avatar'>
|
||||
<Link className='timeline-compose-block__avatar' to={`/@${acct}`}>
|
||||
<Avatar account={account} size={46} />
|
||||
</div>
|
||||
</Link>
|
||||
<ComposeFormContainer
|
||||
shouldCondense
|
||||
autoFocus={false}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import { NODE_ENV } from 'soapbox/build_config';
|
||||
|
||||
//
|
||||
// Tools for performance debugging, only enabled in development mode.
|
||||
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
|
||||
|
@ -8,7 +10,7 @@
|
|||
|
||||
let marky;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (NODE_ENV === 'development') {
|
||||
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
|
||||
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
|
||||
|
@ -21,13 +23,13 @@ if (process.env.NODE_ENV === 'development') {
|
|||
}
|
||||
|
||||
export function start(name) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (NODE_ENV === 'development') {
|
||||
marky.mark(name);
|
||||
}
|
||||
}
|
||||
|
||||
export function stop(name) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (NODE_ENV === 'development') {
|
||||
marky.stop(name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
import { ME_FETCH_SKIP } from '../actions/me';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { validId, isURL } from 'soapbox/utils/auth';
|
||||
import { trim } from 'lodash';
|
||||
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
|
||||
const defaultState = ImmutableMap({
|
||||
app: ImmutableMap(),
|
||||
|
@ -18,13 +20,21 @@ const defaultState = ImmutableMap({
|
|||
me: null,
|
||||
});
|
||||
|
||||
const buildKey = parts => parts.join(':');
|
||||
|
||||
// For subdirectory support
|
||||
const NAMESPACE = trim(FE_SUBDIRECTORY, '/') ? `soapbox@${FE_SUBDIRECTORY}` : 'soapbox';
|
||||
|
||||
const STORAGE_KEY = buildKey([NAMESPACE, 'auth']);
|
||||
const SESSION_KEY = buildKey([NAMESPACE, 'auth', 'me']);
|
||||
|
||||
const getSessionUser = () => {
|
||||
const id = sessionStorage.getItem('soapbox:auth:me');
|
||||
const id = sessionStorage.getItem(SESSION_KEY);
|
||||
return validId(id) ? id : undefined;
|
||||
};
|
||||
|
||||
const sessionUser = getSessionUser();
|
||||
const localState = fromJS(JSON.parse(localStorage.getItem('soapbox:auth')));
|
||||
const localState = fromJS(JSON.parse(localStorage.getItem(STORAGE_KEY)));
|
||||
|
||||
// Checks if the user has an ID and access token
|
||||
const validUser = user => {
|
||||
|
@ -119,12 +129,12 @@ const sanitizeState = state => {
|
|||
});
|
||||
};
|
||||
|
||||
const persistAuth = state => localStorage.setItem('soapbox:auth', JSON.stringify(state.toJS()));
|
||||
const persistAuth = state => localStorage.setItem(STORAGE_KEY, JSON.stringify(state.toJS()));
|
||||
|
||||
const persistSession = state => {
|
||||
const me = state.get('me');
|
||||
if (me && typeof me === 'string') {
|
||||
sessionStorage.setItem('soapbox:auth:me', me);
|
||||
sessionStorage.setItem(SESSION_KEY, me);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -261,7 +271,10 @@ const userSwitched = (oldState, state) => {
|
|||
};
|
||||
|
||||
const maybeReload = (oldState, state, action) => {
|
||||
if (userSwitched(oldState, state)) {
|
||||
const loggedOutStandalone = action.type === AUTH_LOGGED_OUT && action.standalone;
|
||||
const switched = userSwitched(oldState, state);
|
||||
|
||||
if (switched || loggedOutStandalone) {
|
||||
reload(state);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
INSTANCE_FETCH_SUCCESS,
|
||||
INSTANCE_FETCH_FAIL,
|
||||
NODEINFO_FETCH_SUCCESS,
|
||||
} from '../actions/instance';
|
||||
import { PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
||||
|
@ -71,12 +72,22 @@ const importConfigs = (state, configs) => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleAuthFetch = state => {
|
||||
// Authenticated fetch is enabled, so make the instance appear censored
|
||||
return ImmutableMap({
|
||||
title: '██████',
|
||||
description: '████████████',
|
||||
}).merge(state);
|
||||
};
|
||||
|
||||
export default function instance(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case PRELOAD_IMPORT:
|
||||
return preloadImport(state, action, '/api/v1/instance');
|
||||
case INSTANCE_FETCH_SUCCESS:
|
||||
return initialState.mergeDeep(fromJS(action.instance));
|
||||
case INSTANCE_FETCH_FAIL:
|
||||
return action.error.response.status === 401 ? handleAuthFetch(state) : state;
|
||||
case NODEINFO_FETCH_SUCCESS:
|
||||
return nodeinfoToInstance(fromJS(action.nodeinfo)).mergeDeep(state);
|
||||
case ADMIN_CONFIG_UPDATE_REQUEST:
|
||||
|
|
|
@ -24,6 +24,7 @@ export const getFeatures = createSelector([
|
|||
securityAPI: v.software === 'Pleroma',
|
||||
settingsStore: v.software === 'Pleroma',
|
||||
accountAliasesAPI: v.software === 'Pleroma',
|
||||
resetPasswordAPI: v.software === 'Pleroma',
|
||||
exposableReactions: features.includes('exposable_reactions'),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
/**
|
||||
* State: general Redux state utility functions.
|
||||
* @module soapbox/utils/state
|
||||
*/
|
||||
|
||||
import { getSoapboxConfig } from'soapbox/actions/soapbox';
|
||||
import { isPrerendered } from 'soapbox/precheck';
|
||||
import { isURL } from 'soapbox/utils/auth';
|
||||
import { getBaseURL as getAccountBaseURL } from 'soapbox/utils/accounts';
|
||||
import { BACKEND_URL } from 'soapbox/build_config';
|
||||
|
||||
export const displayFqn = state => {
|
||||
const soapbox = getSoapboxConfig(state);
|
||||
|
@ -8,3 +17,26 @@ export const displayFqn = state => {
|
|||
export const federationRestrictionsDisclosed = state => {
|
||||
return state.hasIn(['instance', 'pleroma', 'metadata', 'federation', 'mrf_policies']);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine whether Soapbox FE is running in standalone mode.
|
||||
* Standalone mode runs separately from any backend and can login anywhere.
|
||||
* @param {object} state
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isStandalone = state => {
|
||||
const instanceFetchFailed = state.getIn(['meta', 'instance_fetch_failed'], false);
|
||||
return isURL(BACKEND_URL) ? false : (!isPrerendered && instanceFetchFailed);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the baseURL of the instance.
|
||||
* @param {object} state
|
||||
* @returns {string} url
|
||||
*/
|
||||
export const getBaseURL = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
|
||||
return isURL(BACKEND_URL) ? BACKEND_URL : getAccountBaseURL(account);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { NODE_ENV } from 'soapbox/build_config';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (NODE_ENV === 'development') {
|
||||
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||
whyDidYouRender(React);
|
||||
}
|
||||
|
|
|
@ -179,6 +179,8 @@
|
|||
}
|
||||
|
||||
&__avatar {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
@media (max-width: 405px) { display: none; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (api) => {
|
|||
|
||||
switch (env) {
|
||||
case 'production':
|
||||
envOptions.debug = false;
|
||||
config.plugins.push(...[
|
||||
'lodash',
|
||||
[
|
||||
|
@ -51,7 +50,6 @@ module.exports = (api) => {
|
|||
]);
|
||||
break;
|
||||
case 'development':
|
||||
envOptions.debug = true;
|
||||
config.plugins.push(...[
|
||||
'@babel/transform-react-jsx-source',
|
||||
'@babel/transform-react-jsx-self',
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# Build Configuration
|
||||
|
||||
When compiling Soapbox FE, environment variables may be passed to change the build itself.
|
||||
For example:
|
||||
|
||||
```sh
|
||||
NODE_ENV="production" FE_BUILD_DIR="public" FE_SUBDIRECTORY="/soapbox" yarn build
|
||||
```
|
||||
|
||||
### `NODE_ENV`
|
||||
|
||||
The environment to build Soapbox FE for.
|
||||
|
||||
Options:
|
||||
|
||||
- `"production"` - For live sites
|
||||
- `"development"` - For local development
|
||||
- `"test"` - Bootstraps test environment
|
||||
|
||||
Default: `"development"`
|
||||
|
||||
It's recommended to always build in `"production"` mode for live sites.
|
||||
|
||||
### `BACKEND_URL`
|
||||
|
||||
The base URL for API calls.
|
||||
You only need to set this if Soapbox FE is hosted in a different place than the backend.
|
||||
|
||||
Options:
|
||||
|
||||
- An absolute URL, eg `"https://gleasonator.com"`
|
||||
- Empty string (`""`)`
|
||||
|
||||
Default: `""`
|
||||
|
||||
### `FE_BUILD_DIR`
|
||||
|
||||
The folder to put build files in. This is mostly useful for CI tasks like GitLab Pages.
|
||||
|
||||
Options:
|
||||
|
||||
- Any directory name, eg `"public"`
|
||||
|
||||
Default: `"static"`
|
||||
|
||||
### `FE_SUBDIRECTORY`
|
||||
|
||||
Subdirectory to host Soapbox FE out of.
|
||||
When hosting on a subdirectory, you must create a custom build for it.
|
||||
This option will set the imports in `index.html`, and the basename for routes in React.
|
||||
|
||||
Options:
|
||||
|
||||
- Any path, eg `"/soapbox"` or `"/fe/soapbox"`
|
||||
|
||||
Default: `"/"`
|
||||
|
||||
For example, if you want to host the build on `https://gleasonator.com/soapbox`, you can compile it like this:
|
||||
|
||||
```sh
|
||||
NODE_ENV="production" FE_SUBDIRECTORY="/soapbox" yarn build
|
||||
```
|
|
@ -0,0 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
const { join } = require('path');
|
||||
|
||||
const {
|
||||
FE_BUILD_DIR,
|
||||
FE_SUBDIRECTORY,
|
||||
} = require(join(__dirname, 'app', 'soapbox', 'build_config'));
|
||||
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
recurseDepth: 10,
|
||||
opts: {
|
||||
destination: join(__dirname, FE_BUILD_DIR, FE_SUBDIRECTORY, 'jsdoc'),
|
||||
recurse: true,
|
||||
},
|
||||
source: {
|
||||
include: join(__dirname, 'app'),
|
||||
includePattern: '.+\\.js(doc|x)?$',
|
||||
excludePattern: '(^|\\/|\\\\)_',
|
||||
},
|
||||
sourceType: 'module',
|
||||
tags: {
|
||||
allowUnknownTags: true,
|
||||
dictionaries: ['jsdoc', 'closure'],
|
||||
},
|
||||
templates: {
|
||||
cleverLinks: false,
|
||||
monospaceLinks: false,
|
||||
},
|
||||
};
|
56
package.json
56
package.json
|
@ -19,6 +19,7 @@
|
|||
"start": "npx webpack-dev-server --config webpack",
|
||||
"dev": "${npm_execpath} run start",
|
||||
"build": "npx webpack --config webpack",
|
||||
"jsdoc": "npx jsdoc -c jsdoc.conf.js",
|
||||
"manage:translations": "node ./webpack/translationRunner.js",
|
||||
"test": "${npm_execpath} run test:lint && ${npm_execpath} run test:jest",
|
||||
"test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
|
||||
|
@ -48,6 +49,7 @@
|
|||
"@babel/runtime": "^7.14.6",
|
||||
"@fontsource/montserrat": "^4.5.1",
|
||||
"@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",
|
||||
"array-includes": "^3.0.3",
|
||||
|
@ -62,9 +64,9 @@
|
|||
"blurhash": "^1.0.0",
|
||||
"bowser": "^2.11.0",
|
||||
"browserslist": "^4.16.6",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"classnames": "^2.2.5",
|
||||
"compression-webpack-plugin": "^6.0.2",
|
||||
"copy-webpack-plugin": "6.4.0",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"core-js": "^3.15.2",
|
||||
"cryptocurrency-icons": "^0.17.2",
|
||||
"css-loader": "^5.2.6",
|
||||
|
@ -73,13 +75,14 @@
|
|||
"dotenv": "^8.0.0",
|
||||
"emoji-datasource": "5.0.0",
|
||||
"emoji-mart": "^3.0.1",
|
||||
"entities": "^3.0.1",
|
||||
"es6-symbol": "^3.1.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"exif-js": "^2.3.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"fork-awesome": "https://github.com/alexgleason/Fork-Awesome#c23fd34246a9f33c4bf24ea095a4cf26e7abe265",
|
||||
"html-webpack-harddisk-plugin": "^1.0.1",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"html-webpack-harddisk-plugin": "^2.0.0",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"http-link-header": "^1.0.2",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"imports-loader": "^1.0.0",
|
||||
|
@ -87,8 +90,9 @@
|
|||
"intl": "^1.2.5",
|
||||
"intl-messageformat": "^9.0.0",
|
||||
"intl-messageformat-parser": "^6.0.0",
|
||||
"intl-pluralrules": "^1.1.1",
|
||||
"intl-pluralrules": "^1.3.0",
|
||||
"is-nan": "^1.2.1",
|
||||
"jsdoc": "~3.6.7",
|
||||
"lodash": "^4.7.11",
|
||||
"mark-loader": "^0.1.6",
|
||||
"marky": "^1.2.1",
|
||||
|
@ -96,10 +100,11 @@
|
|||
"object-assign": "^4.1.1",
|
||||
"object-fit-images": "^3.2.3",
|
||||
"object.values": "^1.1.0",
|
||||
"offline-plugin": "^5.0.7",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.1.1",
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-object-fit-images": "^1.1.2",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.5.10",
|
||||
"punycode": "^2.1.0",
|
||||
"qrcode.react": "^1.0.0",
|
||||
|
@ -134,34 +139,35 @@
|
|||
"semver": "^7.3.2",
|
||||
"stringz": "^2.0.0",
|
||||
"substring-trie": "^1.0.2",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"terser-webpack-plugin": "^5.2.3",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-jest": "^27.0.5",
|
||||
"tslib": "^2.3.1",
|
||||
"twemoji": "https://github.com/twitter/twemoji#v13.0.2",
|
||||
"typescript": "^4.0.3",
|
||||
"unused-files-webpack-plugin": "^3.4.0",
|
||||
"uuid": "^8.0.0",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-assets-manifest": "^3.1.1",
|
||||
"webpack-bundle-analyzer": "^4.0.0",
|
||||
"webpack-cli": "^3.3.2",
|
||||
"webpack-merge": "^5.2.0",
|
||||
"util": "^0.12.4",
|
||||
"uuid": "^8.3.2",
|
||||
"webpack": "^5.52.0",
|
||||
"webpack-assets-manifest": "^5.0.6",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"websocket.js": "^0.1.12",
|
||||
"wicg-inert": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^27.0.6",
|
||||
"enzyme": "^3.8.0",
|
||||
"enzyme-adapter-react-16": "^1.7.1",
|
||||
"babel-jest": "^27.1.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-plugin-import": "~2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "~6.4.0",
|
||||
"eslint-plugin-promise": "~4.2.0",
|
||||
"eslint-plugin-react": "~7.21.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.4",
|
||||
"jest": "^27.0.6",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.25.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"jest": "^27.1.0",
|
||||
"raf": "^3.4.1",
|
||||
"react-intl-translations-manager": "^5.0.3",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
|
@ -169,7 +175,7 @@
|
|||
"stylelint": "^13.7.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"webpack-dev-server": "^3.5.1",
|
||||
"webpack-dev-server": "^4.1.0",
|
||||
"yargs": "^16.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
const { join } = require('path');
|
||||
const { env } = require('process');
|
||||
|
||||
const {
|
||||
FE_SUBDIRECTORY,
|
||||
FE_BUILD_DIR,
|
||||
} = require(join(__dirname, '..', 'app', 'soapbox', 'build_config'));
|
||||
|
||||
const settings = {
|
||||
source_path: 'app',
|
||||
public_root_path: 'static',
|
||||
test_root_path: 'static-test',
|
||||
cache_path: 'tmp/cache/webpacker',
|
||||
public_root_path: FE_BUILD_DIR,
|
||||
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' ],
|
||||
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' ],
|
||||
};
|
||||
|
||||
const outputDir = env.NODE_ENV === 'test' ? settings.test_root_path : settings.public_root_path;
|
||||
|
||||
const output = {
|
||||
path: join(__dirname, '..', outputDir),
|
||||
path: join(__dirname, '..', outputDir, FE_SUBDIRECTORY),
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
console.log('Running in development mode'); // eslint-disable-line no-console
|
||||
|
||||
const { join } = require('path');
|
||||
const { merge } = require('webpack-merge');
|
||||
const sharedConfig = require('./shared');
|
||||
|
||||
|
@ -10,6 +11,8 @@ const backendUrl = process.env.BACKEND_URL || 'http://localhost:4000';
|
|||
const patronUrl = process.env.PATRON_URL || 'http://localhost:3037';
|
||||
const secureProxy = !(process.env.PROXY_HTTPS_INSECURE === 'true');
|
||||
|
||||
const { FE_SUBDIRECTORY } = require(join(__dirname, '..', 'app', 'soapbox', 'build_config'));
|
||||
|
||||
const backendEndpoints = [
|
||||
'/api',
|
||||
'/pleroma',
|
||||
|
@ -54,6 +57,7 @@ module.exports = merge(sharedConfig, {
|
|||
devtool: 'source-map',
|
||||
|
||||
stats: {
|
||||
preset: 'errors-warnings',
|
||||
errorDetails: true,
|
||||
},
|
||||
|
||||
|
@ -61,37 +65,31 @@ module.exports = merge(sharedConfig, {
|
|||
pathinfo: true,
|
||||
},
|
||||
|
||||
watchOptions: Object.assign(
|
||||
{},
|
||||
{ ignored: '**/node_modules/**' },
|
||||
watchOptions,
|
||||
),
|
||||
|
||||
devServer: {
|
||||
clientLogLevel: 'none',
|
||||
compress: true,
|
||||
quiet: false,
|
||||
disableHostCheck: true,
|
||||
host: 'localhost',
|
||||
port: 3036,
|
||||
https: false,
|
||||
hot: false,
|
||||
inline: true,
|
||||
useLocalIp: false,
|
||||
public: 'localhost:3036',
|
||||
historyApiFallback: {
|
||||
disableDotRule: true,
|
||||
index: join(FE_SUBDIRECTORY, '/'),
|
||||
},
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
overlay: true,
|
||||
stats: {
|
||||
entrypoints: false,
|
||||
errorDetails: false,
|
||||
modules: false,
|
||||
moduleTrace: false,
|
||||
client: {
|
||||
overlay: true,
|
||||
},
|
||||
static: {
|
||||
serveIndex: true,
|
||||
},
|
||||
watchOptions: Object.assign(
|
||||
{},
|
||||
{ ignored: '**/node_modules/**' },
|
||||
watchOptions,
|
||||
),
|
||||
serveIndex: true,
|
||||
proxy: makeProxyConfig(),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,45 +3,24 @@ console.log('Running in production mode'); // eslint-disable-line no-console
|
|||
|
||||
const { merge } = require('webpack-merge');
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
const OfflinePlugin = require('offline-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
|
||||
const OfflinePlugin = require('@lcdp/offline-plugin');
|
||||
const sharedConfig = require('./shared');
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
stats: 'normal',
|
||||
stats: 'errors-warnings',
|
||||
bail: true,
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
|
||||
terserOptions: {
|
||||
warnings: false,
|
||||
mangle: false,
|
||||
|
||||
output: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new CompressionPlugin({
|
||||
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
||||
}),
|
||||
new BundleAnalyzerPlugin({ // generates report.html
|
||||
// Generates report.html
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
openAnalyzer: false,
|
||||
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
||||
logLevel: 'silent',
|
||||
}),
|
||||
new OfflinePlugin({
|
||||
caches: {
|
||||
|
@ -79,11 +58,15 @@ module.exports = merge(sharedConfig, {
|
|||
'**/*.map',
|
||||
'stats.json',
|
||||
'report.html',
|
||||
'instance/**/*',
|
||||
// any browser that supports ServiceWorker will support woff2
|
||||
'**/*.eot',
|
||||
'**/*.ttf',
|
||||
'**/*-webfont-*.svg',
|
||||
'**/*.woff',
|
||||
// Sounds return a 206 causing sw.js to crash
|
||||
// https://stackoverflow.com/a/66335638
|
||||
'sounds/**/*',
|
||||
// Don't cache index.html
|
||||
'index.html',
|
||||
],
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
const { resolve } = require('path');
|
||||
const { env } = require('../configuration');
|
||||
|
||||
// This is a hack, used to force build_config @preval to recompile
|
||||
// https://github.com/kentcdodds/babel-plugin-preval/issues/19
|
||||
|
||||
module.exports = {
|
||||
test: resolve(__dirname, '../../app/soapbox/build_config.js'),
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: false,
|
||||
cacheCompression: env.NODE_ENV === 'production',
|
||||
compact: env.NODE_ENV === 'production',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -9,9 +9,9 @@ module.exports = {
|
|||
options: {
|
||||
name(file) {
|
||||
if (file.includes(settings.source_path)) {
|
||||
return 'packs/media/[path][name]-[hash].[ext]';
|
||||
return 'packs/media/[path][name]-[contenthash].[ext]';
|
||||
}
|
||||
return 'packs/media/[folder]/[name]-[hash:8].[ext]';
|
||||
return 'packs/media/[folder]/[name]-[contenthash:8].[ext]';
|
||||
},
|
||||
context: join(settings.source_path),
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const babel = require('./babel');
|
||||
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 nodeModules = require('./node_modules');
|
||||
|
@ -15,4 +16,5 @@ module.exports = [
|
|||
babel,
|
||||
git,
|
||||
gitRefresh,
|
||||
buildConfig,
|
||||
];
|
||||
|
|
|
@ -7,10 +7,28 @@ const AssetsManifestPlugin = require('webpack-assets-manifest');
|
|||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const { UnusedFilesWebpackPlugin } = require('unused-files-webpack-plugin');
|
||||
const { env, settings, output } = require('./configuration');
|
||||
const rules = require('./rules');
|
||||
|
||||
const { FE_SUBDIRECTORY } = require(join(__dirname, '..', 'app', 'soapbox', 'build_config'));
|
||||
|
||||
const makeHtmlConfig = (params = {}) => {
|
||||
return Object.assign({
|
||||
template: 'app/index.ejs',
|
||||
chunksSortMode: 'manual',
|
||||
chunks: ['common', 'locale_en', 'application', 'styles'],
|
||||
alwaysWriteToDisk: true,
|
||||
minify: {
|
||||
collapseWhitespace: true,
|
||||
removeComments: false,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true,
|
||||
},
|
||||
}, params);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
entry: Object.assign(
|
||||
{ application: resolve('app/application.js') },
|
||||
|
@ -20,19 +38,21 @@ module.exports = {
|
|||
output: {
|
||||
filename: 'packs/js/[name]-[chunkhash].js',
|
||||
chunkFilename: 'packs/js/[name]-[chunkhash].chunk.js',
|
||||
hotUpdateChunkFilename: 'packs/js/[id]-[hash].hot-update.js',
|
||||
hotUpdateChunkFilename: 'packs/js/[id]-[contenthash].hot-update.js',
|
||||
path: output.path,
|
||||
publicPath: '/',
|
||||
publicPath: join(FE_SUBDIRECTORY, '/'),
|
||||
},
|
||||
|
||||
optimization: {
|
||||
chunkIds: 'total-size',
|
||||
moduleIds: 'size',
|
||||
runtimeChunk: {
|
||||
name: 'common',
|
||||
},
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
vendors: false,
|
||||
defaultVendors: false,
|
||||
common: {
|
||||
name: 'common',
|
||||
chunks: 'all',
|
||||
|
@ -42,7 +62,6 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
occurrenceOrder: true,
|
||||
},
|
||||
|
||||
module: {
|
||||
|
@ -51,13 +70,9 @@ module.exports = {
|
|||
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/^history\//, (resource) => {
|
||||
// temporary fix for https://github.com/ReactTraining/react-router/issues/5576
|
||||
// to reduce bundle size
|
||||
resource.request = resource.request.replace(/^history/, 'history/es');
|
||||
},
|
||||
),
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'packs/css/[name]-[contenthash:8].css',
|
||||
chunkFilename: 'packs/css/[name]-[contenthash:8].chunk.css',
|
||||
|
@ -68,28 +83,9 @@ module.exports = {
|
|||
writeToDisk: true,
|
||||
publicPath: true,
|
||||
}),
|
||||
// https://www.npmjs.com/package/unused-files-webpack-plugin#options
|
||||
new UnusedFilesWebpackPlugin({
|
||||
patterns: ['app/**/*.*'],
|
||||
globOptions: {
|
||||
ignore: ['node_modules/**/*', '**/__*__/**/*'],
|
||||
},
|
||||
}),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'app/index.ejs',
|
||||
chunksSortMode: 'manual',
|
||||
chunks: ['common', 'locale_en', 'application', 'styles'],
|
||||
alwaysWriteToDisk: true,
|
||||
minify: {
|
||||
collapseWhitespace: true,
|
||||
removeComments: false,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true,
|
||||
},
|
||||
}),
|
||||
// https://github.com/jantimon/html-webpack-plugin#options
|
||||
new HtmlWebpackPlugin(makeHtmlConfig()),
|
||||
new HtmlWebpackPlugin(makeHtmlConfig({ filename: '404.html' })),
|
||||
new HtmlWebpackHarddiskPlugin(),
|
||||
new CopyPlugin({
|
||||
patterns: [{
|
||||
|
@ -98,6 +94,12 @@ module.exports = {
|
|||
}, {
|
||||
from: join(__dirname, '../node_modules/emoji-datasource/img/twitter/sheets/32.png'),
|
||||
to: join(output.path, 'emoji/sheet_13.png'),
|
||||
}, {
|
||||
from: join(__dirname, '../app/sounds'),
|
||||
to: join(output.path, 'sounds'),
|
||||
}, {
|
||||
from: join(__dirname, '../app/instance'),
|
||||
to: join(output.path, 'instance'),
|
||||
}],
|
||||
options: {
|
||||
concurrency: 100,
|
||||
|
@ -111,15 +113,13 @@ module.exports = {
|
|||
resolve(settings.source_path),
|
||||
'node_modules',
|
||||
],
|
||||
fallback: {
|
||||
path: require.resolve('path-browserify'),
|
||||
util: require.resolve('util'),
|
||||
},
|
||||
},
|
||||
|
||||
resolveLoader: {
|
||||
modules: ['node_modules'],
|
||||
},
|
||||
|
||||
node: {
|
||||
// Called by http-link-header in an API we never use, increases
|
||||
// bundle size unnecessarily
|
||||
Buffer: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
console.log('Running in test mode'); // eslint-disable-line no-console
|
||||
|
||||
const { merge } = require('webpack-merge');
|
||||
const sharedConfig = require('./shared.js');
|
||||
const sharedConfig = require('./shared');
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
mode: 'development',
|
||||
|
|
Loading…
Reference in New Issue