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/**
|
/node_modules/**
|
||||||
/static/**
|
/static/**
|
||||||
/static-test/**
|
/static-test/**
|
||||||
|
/public/**
|
||||||
/tmp/**
|
/tmp/**
|
||||||
/coverage/**
|
/coverage/**
|
||||||
!.eslintrc.js
|
!.eslintrc.js
|
||||||
|
|
|
@ -8,20 +8,6 @@
|
||||||
/.vs/
|
/.vs/
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
/static/packs
|
/static/
|
||||||
/static/packs-test
|
/static-test/
|
||||||
/static/emoji
|
/public/
|
||||||
/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
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
image: node:12
|
image: node:14
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
|
@ -17,6 +17,7 @@ stages:
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
|
- env
|
||||||
- yarn
|
- yarn
|
||||||
|
|
||||||
lint-js:
|
lint-js:
|
||||||
|
@ -85,10 +86,10 @@ docs-deploy:
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
before_script: []
|
||||||
script:
|
script:
|
||||||
- yarn build
|
# artifacts are kept between jobs
|
||||||
- mv static public
|
- mv static public
|
||||||
- cp public/{index.html,404.html}
|
|
||||||
variables:
|
variables:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
artifacts:
|
artifacts:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
nodejs 14.17.6
|
|
@ -3,6 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
<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-->
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
const api = jest.requireActual('../api').default;
|
const api = jest.requireActual('../api');
|
||||||
let mocks = [];
|
let mocks = [];
|
||||||
|
|
||||||
export const __stub = func => mocks.push(func);
|
export const __stub = func => mocks.push(func);
|
||||||
|
@ -11,8 +11,10 @@ const setupMock = axios => {
|
||||||
mocks.map(func => func(mock));
|
mocks.map(func => func(mock));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const staticClient = api.staticClient;
|
||||||
|
|
||||||
export default (...params) => {
|
export default (...params) => {
|
||||||
const axios = api(...params);
|
const axios = api.default(...params);
|
||||||
setupMock(axios);
|
setupMock(axios);
|
||||||
return axios;
|
return axios;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,15 +5,17 @@ import {
|
||||||
fetchAboutPage,
|
fetchAboutPage,
|
||||||
} from '../about';
|
} from '../about';
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
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';
|
import { mockStore } from 'soapbox/test_helpers';
|
||||||
|
|
||||||
describe('fetchAboutPage()', () => {
|
describe('fetchAboutPage()', () => {
|
||||||
it('creates the expected actions on success', () => {
|
it('creates the expected actions on success', () => {
|
||||||
|
|
||||||
stubApi(mock => {
|
const mock = new MockAdapter(staticClient);
|
||||||
mock.onGet('/instance/about/index.html').reply(200, '<h1>Hello world</h1>');
|
|
||||||
});
|
mock.onGet('/instance/about/index.html')
|
||||||
|
.reply(200, '<h1>Hello world</h1>');
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: FETCH_ABOUT_PAGE_REQUEST, slug: 'index' },
|
{ 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_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST';
|
||||||
export const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS';
|
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) {
|
export function fetchAboutPage(slug = 'index', locale) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale });
|
dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale });
|
||||||
return api(getState).get(`/instance/about/${slug}${locale ? `.${locale}` : ''}.html`).then(response => {
|
const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
|
||||||
dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html: response.data });
|
return staticClient.get(`/instance/about/${filename}`).then(({ data: html }) => {
|
||||||
return response.data;
|
dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html });
|
||||||
|
return html;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error });
|
dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error });
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { createApp } from 'soapbox/actions/apps';
|
||||||
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
||||||
import sourceCode from 'soapbox/utils/code';
|
import sourceCode from 'soapbox/utils/code';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
import { isStandalone } from 'soapbox/utils/state';
|
||||||
|
|
||||||
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||||
|
|
||||||
|
@ -162,10 +163,18 @@ export function logIn(intl, username, password) {
|
||||||
return dispatch(createUserToken(username, password));
|
return dispatch(createUserToken(username, password));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (error.response.data.error === 'mfa_required') {
|
if (error.response.data.error === 'mfa_required') {
|
||||||
|
// If MFA is required, throw the error and handle it in the component.
|
||||||
throw error;
|
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));
|
dispatch(snackbar.error(error.response.data.error));
|
||||||
} else {
|
} else {
|
||||||
|
// Return "wrong password" message.
|
||||||
dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials)));
|
dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials)));
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -177,6 +186,7 @@ export function logOut(intl) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = getLoggedInAccount(state);
|
const account = getLoggedInAccount(state);
|
||||||
|
const standalone = isStandalone(state);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id: state.getIn(['auth', 'app', 'client_id']),
|
client_id: state.getIn(['auth', 'app', 'client_id']),
|
||||||
|
@ -185,7 +195,7 @@ export function logOut(intl) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return dispatch(revokeOAuthToken(params)).finally(() => {
|
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)));
|
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 { baseClient } from '../api';
|
||||||
import { createApp } from 'soapbox/actions/apps';
|
import { createApp } from 'soapbox/actions/apps';
|
||||||
import { obtainOAuthToken } from 'soapbox/actions/oauth';
|
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 { parseBaseURL } from 'soapbox/utils/auth';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
import sourceCode from 'soapbox/utils/code';
|
import sourceCode from 'soapbox/utils/code';
|
||||||
import { fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
const fetchExternalInstance = baseURL => {
|
const fetchExternalInstance = baseURL => {
|
||||||
return baseClient(null, baseURL)
|
return baseClient(null, baseURL)
|
||||||
.get('/api/v1/instance')
|
.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) {
|
export function createAppAndRedirect(host) {
|
||||||
|
@ -73,6 +82,7 @@ export function loginWithCode(code) {
|
||||||
return dispatch(obtainOAuthToken(params, baseURL))
|
return dispatch(obtainOAuthToken(params, baseURL))
|
||||||
.then(token => dispatch(authLoggedIn(token)))
|
.then(token => dispatch(authLoggedIn(token)))
|
||||||
.then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
|
.then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
|
||||||
|
.then(account => dispatch(switchAccount(account.id)))
|
||||||
.then(() => window.location.href = '/');
|
.then(() => window.location.href = '/');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function obtainOAuthToken(params, baseURL) {
|
||||||
dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token });
|
dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token });
|
||||||
return token;
|
return token;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: OAUTH_TOKEN_CREATE_FAIL, params, error });
|
dispatch({ type: OAUTH_TOKEN_CREATE_FAIL, params, error, skipAlert: true });
|
||||||
throw error;
|
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 { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
@ -76,7 +76,7 @@ export function fetchSoapboxConfig() {
|
||||||
|
|
||||||
export function fetchSoapboxJson() {
|
export function fetchSoapboxJson() {
|
||||||
return (dispatch, getState) => {
|
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';
|
if (!isObject(data)) throw 'soapbox.json failed';
|
||||||
dispatch(importSoapboxConfig(data));
|
dispatch(importSoapboxConfig(data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -1,12 +1,23 @@
|
||||||
|
/**
|
||||||
|
* API: HTTP client and utilities.
|
||||||
|
* @see {@link https://github.com/axios/axios}
|
||||||
|
* @module soapbox/api
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LinkHeader from 'http-link-header';
|
import LinkHeader from 'http-link-header';
|
||||||
import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth';
|
import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth';
|
||||||
import { createSelector } from 'reselect';
|
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';
|
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 => {
|
export const getLinks = response => {
|
||||||
const value = response.headers.link;
|
const value = response.headers.link;
|
||||||
if (!value) return { refs: [] };
|
if (!value) return { refs: [] };
|
||||||
|
@ -33,6 +44,12 @@ const getAuthBaseURL = createSelector([
|
||||||
return baseURL !== window.location.origin ? baseURL : '';
|
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 = '') => {
|
export const baseClient = (accessToken, baseURL = '') => {
|
||||||
return axios.create({
|
return axios.create({
|
||||||
// When BACKEND_URL is set, always use it.
|
// 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') => {
|
export default (getState, authType = 'user') => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const accessToken = getToken(state, authType);
|
const accessToken = getToken(state, authType);
|
||||||
|
|
|
@ -4,20 +4,38 @@
|
||||||
* @module soapbox/build_config
|
* @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 => {
|
const sanitizeURL = url => {
|
||||||
try {
|
try {
|
||||||
return new URL(url).toString();
|
return trimEnd(new URL(url).toString(), '/');
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
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
|
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
|
||||||
// inconsistent behavior in dev mode
|
// inconsistent behavior in dev mode
|
||||||
const sanitize = obj => JSON.parse(JSON.stringify(obj));
|
const sanitize = obj => JSON.parse(JSON.stringify(obj));
|
||||||
|
|
||||||
module.exports = sanitize({
|
module.exports = sanitize({
|
||||||
|
NODE_ENV: NODE_ENV || 'development',
|
||||||
BACKEND_URL: sanitizeURL(BACKEND_URL),
|
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]}
|
onClick={[Function]}
|
||||||
onKeyUp={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -30,7 +30,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyUp={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -41,7 +41,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyUp={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -52,7 +52,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyUp={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -63,7 +63,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyUp={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -74,7 +74,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyUp={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
||||||
|
import { join } from 'path';
|
||||||
const assetHost = process.env.CDN_HOST || '';
|
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||||
|
|
||||||
export default class AutosuggestEmoji extends React.PureComponent {
|
export default class AutosuggestEmoji extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export default class AutosuggestEmoji extends React.PureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
url = `${assetHost}/emoji/${mapping.filename}.svg`;
|
url = join(FE_SUBDIRECTORY, 'emoji', `${mapping.filename}.svg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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;
|
const { onUnfocus } = this.props;
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
case 'Tab':
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.shiftKey) this._selectPreviousEmoji(i);
|
||||||
|
else this._selectNextEmoji(i);
|
||||||
|
break;
|
||||||
case 'Left':
|
case 'Left':
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
if (i !== 0) {
|
this._selectPreviousEmoji(i);
|
||||||
this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`).focus();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'Right':
|
case 'Right':
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
if (i !== this.props.allowedEmoji.size - 1) {
|
this._selectNextEmoji(i);
|
||||||
this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`).focus();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
onUnfocus();
|
onUnfocus();
|
||||||
|
@ -94,7 +111,7 @@ class EmojiSelector extends ImmutablePureComponent {
|
||||||
className='emoji-react-selector__emoji'
|
className='emoji-react-selector__emoji'
|
||||||
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
||||||
onClick={this.handleReact(emoji)}
|
onClick={this.handleReact(emoji)}
|
||||||
onKeyUp={this.handleKeyUp(i, emoji)}
|
onKeyDown={this.handleKeyDown(i, emoji)}
|
||||||
tabIndex={(visible || focused) ? 0 : -1}
|
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 { fetchOwnAccounts } from 'soapbox/actions/auth';
|
||||||
import { is as ImmutableIs } from 'immutable';
|
import { is as ImmutableIs } from 'immutable';
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
followers: { id: 'account.followers', defaultMessage: 'Followers' },
|
followers: { id: 'account.followers', defaultMessage: 'Followers' },
|
||||||
|
@ -53,6 +54,9 @@ const makeMapStateToProps = () => {
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const me = state.get('me');
|
const me = state.get('me');
|
||||||
|
const instance = state.get('instance');
|
||||||
|
|
||||||
|
const features = getFeatures(instance);
|
||||||
const soapbox = getSoapboxConfig(state);
|
const soapbox = getSoapboxConfig(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -61,6 +65,7 @@ const makeMapStateToProps = () => {
|
||||||
donateUrl: state.getIn(['patron', 'instance', 'url']),
|
donateUrl: state.getIn(['patron', 'instance', 'url']),
|
||||||
hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string',
|
hasCrypto: typeof soapbox.getIn(['cryptoAddresses', 0, 'ticker']) === 'string',
|
||||||
otherAccounts: getOtherAccounts(state),
|
otherAccounts: getOtherAccounts(state),
|
||||||
|
features,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,6 +98,7 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
otherAccounts: ImmutablePropTypes.list,
|
otherAccounts: ImmutablePropTypes.list,
|
||||||
sidebarOpen: PropTypes.bool,
|
sidebarOpen: PropTypes.bool,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
features: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -149,7 +155,7 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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;
|
const { switcher } = this.state;
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
const acct = account.get('acct');
|
const acct = account.get('acct');
|
||||||
|
@ -231,10 +237,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
<Icon id='ban' />
|
<Icon id='ban' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.blocks)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.blocks)}</span>
|
||||||
</NavLink>
|
</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' />
|
<Icon id='ban' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.domain_blocks)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.domain_blocks)}</span>
|
||||||
</NavLink>
|
</NavLink>}
|
||||||
<NavLink className='sidebar-menu-item' to='/mutes' onClick={this.handleClose}>
|
<NavLink className='sidebar-menu-item' to='/mutes' onClick={this.handleClose}>
|
||||||
<Icon id='times-circle' />
|
<Icon id='times-circle' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
||||||
|
@ -259,10 +265,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
<Icon id='cloud-upload' />
|
<Icon id='cloud-upload' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
||||||
</NavLink>
|
</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' />
|
<Icon id='suitcase' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.account_aliases)}</span>
|
<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}>
|
<NavLink className='sidebar-menu-item' to='/auth/edit' onClick={this.handleClose}>
|
||||||
<Icon id='lock' />
|
<Icon id='lock' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.security)}</span>
|
<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 { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||||
import messages from 'soapbox/locales/messages';
|
import messages from 'soapbox/locales/messages';
|
||||||
|
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||||
|
|
||||||
const validLocale = locale => Object.keys(messages).includes(locale);
|
const validLocale = locale => Object.keys(messages).includes(locale);
|
||||||
|
|
||||||
export const store = configureStore();
|
export const store = configureStore();
|
||||||
|
|
||||||
store.dispatch(preload());
|
store.dispatch(preload());
|
||||||
store.dispatch(fetchMe());
|
|
||||||
store.dispatch(fetchInstance());
|
store.dispatch(fetchMe())
|
||||||
store.dispatch(fetchSoapboxConfig());
|
.then(() => {
|
||||||
|
// Postpone for authenticated fetch
|
||||||
|
store.dispatch(fetchInstance());
|
||||||
|
store.dispatch(fetchSoapboxConfig());
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
const me = state.get('me');
|
const me = state.get('me');
|
||||||
|
@ -142,7 +148,7 @@ class SoapboxMount extends React.PureComponent {
|
||||||
))}
|
))}
|
||||||
<meta name='theme-color' content={this.props.brandColor} />
|
<meta name='theme-color' content={this.props.brandColor} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<BrowserRouter>
|
<BrowserRouter basename={FE_SUBDIRECTORY}>
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{!me && <Route exact path='/' component={PublicLayout} />}
|
{!me && <Route exact path='/' component={PublicLayout} />}
|
||||||
|
|
|
@ -54,6 +54,7 @@ const makeMapStateToProps = () => {
|
||||||
unavailable,
|
unavailable,
|
||||||
accountUsername,
|
accountUsername,
|
||||||
accountApId,
|
accountApId,
|
||||||
|
isBlocked,
|
||||||
isAccount: !!state.getIn(['accounts', accountId]),
|
isAccount: !!state.getIn(['accounts', accountId]),
|
||||||
statusIds: getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
|
statusIds: getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
|
||||||
featuredStatusIds: showPins ? getStatusIds(state, { type: `account:${accountId}:pinned`, prefix: 'account_timeline' }) : ImmutableOrderedSet(),
|
featuredStatusIds: showPins ? getStatusIds(state, { type: `account:${accountId}:pinned`, prefix: 'account_timeline' }) : ImmutableOrderedSet(),
|
||||||
|
@ -142,7 +143,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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;
|
const { collapsed, animating } = this.state;
|
||||||
|
|
||||||
if (!isAccount && accountId !== -1) {
|
if (!isAccount && accountId !== -1) {
|
||||||
|
@ -165,7 +166,8 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<div className='empty-column-indicator'>
|
<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>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,66 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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
|
<form
|
||||||
className="simple_form new_user"
|
className="simple_form new_user"
|
||||||
method="post"
|
method="post"
|
||||||
|
|
|
@ -63,67 +63,3 @@ exports[`<LoginPage /> renders correctly on load 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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 React from 'react';
|
||||||
import LoginForm from '../login_form';
|
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 />', () => {
|
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(
|
expect(createComponent(
|
||||||
<LoginForm />,
|
<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();
|
).toJSON()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import LoginPage from '../login_page';
|
import LoginPage from '../login_page';
|
||||||
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import rootReducer from 'soapbox/reducers';
|
||||||
// import { __stub as stubApi } from 'soapbox/api';
|
|
||||||
// import { logIn } from 'soapbox/actions/auth';
|
|
||||||
|
|
||||||
describe('<LoginPage />', () => {
|
describe('<LoginPage />', () => {
|
||||||
beforeEach(() => {
|
|
||||||
const store = mockStore(ImmutableMap({}));
|
|
||||||
return store;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders correctly on load', () => {
|
it('renders correctly on load', () => {
|
||||||
expect(createComponent(
|
const state = rootReducer(undefined, {})
|
||||||
<LoginPage />,
|
.set('me', '1234')
|
||||||
).toJSON()).toMatchSnapshot();
|
.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(
|
expect(createComponent(
|
||||||
<LoginPage />,
|
<LoginPage />,
|
||||||
{ store },
|
{ store },
|
||||||
|
|
|
@ -3,18 +3,30 @@ import { connect } from 'react-redux';
|
||||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
import { getBaseURL } from 'soapbox/utils/state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
username: { id: 'login.fields.username_placeholder', defaultMessage: 'Username' },
|
username: { id: 'login.fields.username_placeholder', defaultMessage: 'Username' },
|
||||||
password: { id: 'login.fields.password_placeholder', defaultMessage: 'Password' },
|
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
|
@injectIntl
|
||||||
class LoginForm extends ImmutablePureComponent {
|
class LoginForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl, isLoading, handleSubmit } = this.props;
|
const { intl, isLoading, handleSubmit, baseURL, hasResetPasswordAPI } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='simple_form new_user' method='post' onSubmit={handleSubmit}>
|
<form className='simple_form new_user' method='post' onSubmit={handleSubmit}>
|
||||||
|
@ -43,9 +55,15 @@ class LoginForm extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className='hint subtle-hint'>
|
<p className='hint subtle-hint'>
|
||||||
<Link to='/auth/reset_password'>
|
{hasResetPasswordAPI ? (
|
||||||
<FormattedMessage id='login.reset_password_hint' defaultMessage='Trouble logging in?' />
|
<Link to='/auth/reset_password'>
|
||||||
</Link>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -6,10 +6,13 @@ import { injectIntl } from 'react-intl';
|
||||||
import LoginForm from './login_form';
|
import LoginForm from './login_form';
|
||||||
import OtpAuthForm from './otp_auth_form';
|
import OtpAuthForm from './otp_auth_form';
|
||||||
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||||
|
import { fetchInstance } from 'soapbox/actions/instance';
|
||||||
|
import { isStandalone } from 'soapbox/utils/state';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
me: state.get('me'),
|
me: state.get('me'),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
standalone: isStandalone(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -38,7 +41,9 @@ class LoginPage extends ImmutablePureComponent {
|
||||||
const { dispatch, intl, me } = this.props;
|
const { dispatch, intl, me } = this.props;
|
||||||
const { username, password } = this.getFormData(event.target);
|
const { username, password } = this.getFormData(event.target);
|
||||||
dispatch(logIn(intl, username, password)).then(({ access_token }) => {
|
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 => {
|
}).then(account => {
|
||||||
this.setState({ shouldRedirect: true });
|
this.setState({ shouldRedirect: true });
|
||||||
if (typeof me === 'string') {
|
if (typeof me === 'string') {
|
||||||
|
@ -55,8 +60,11 @@ class LoginPage extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { standalone } = this.props;
|
||||||
const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state;
|
const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state;
|
||||||
|
|
||||||
|
if (standalone) return <Redirect to='/auth/external' />;
|
||||||
|
|
||||||
if (shouldRedirect) return <Redirect to='/' />;
|
if (shouldRedirect) return <Redirect to='/' />;
|
||||||
|
|
||||||
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;
|
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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import { buildCustomEmojis } from '../../emoji/emoji';
|
import { buildCustomEmojis } from '../../emoji/emoji';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
@ -25,10 +27,9 @@ const messages = defineMessages({
|
||||||
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
|
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const assetHost = process.env.CDN_HOST || '';
|
|
||||||
let EmojiPicker, Emoji; // load asynchronously
|
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 listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
const categoriesSort = [
|
const categoriesSort = [
|
||||||
|
@ -358,7 +359,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
||||||
<img
|
<img
|
||||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||||
alt='🙂'
|
alt='🙂'
|
||||||
src={`${assetHost}/emoji/1f602.svg`}
|
src={join(FE_SUBDIRECTORY, 'emoji', '1f602.svg')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import unicodeMapping from './emoji_unicode_mapping_light';
|
import unicodeMapping from './emoji_unicode_mapping_light';
|
||||||
import Trie from 'substring-trie';
|
import Trie from 'substring-trie';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||||
|
|
||||||
const trie = new Trie(Object.keys(unicodeMapping));
|
const trie = new Trie(Object.keys(unicodeMapping));
|
||||||
|
|
||||||
const assetHost = process.env.CDN_HOST || '';
|
|
||||||
|
|
||||||
const emojify = (str, customEmojis = {}, autoplay = false) => {
|
const emojify = (str, customEmojis = {}, autoplay = false) => {
|
||||||
const tagCharsWithoutEmojis = '<&';
|
const tagCharsWithoutEmojis = '<&';
|
||||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
||||||
|
@ -62,7 +62,7 @@ const emojify = (str, customEmojis = {}, autoplay = false) => {
|
||||||
} else { // matched to unicode emoji
|
} else { // matched to unicode emoji
|
||||||
const { filename, shortCode } = unicodeMapping[match];
|
const { filename, shortCode } = unicodeMapping[match];
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${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;
|
rend = i + match.length;
|
||||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
||||||
if (str.codePointAt(rend) === 65038) {
|
if (str.codePointAt(rend) === 65038) {
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||||
|
@ -80,13 +81,15 @@ class GroupTimeline extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const acct = account ? account.get('acct') : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{relationships.get('member') && (
|
{relationships.get('member') && (
|
||||||
<div className='timeline-compose-block'>
|
<div className='timeline-compose-block'>
|
||||||
<div className='timeline-compose-block__avatar'>
|
<Link className='timeline-compose-block__avatar' to={`/@${acct}`}>
|
||||||
<Avatar account={account} size={46} />
|
<Avatar account={account} size={46} />
|
||||||
</div>
|
</Link>
|
||||||
<ComposeFormContainer group={group} shouldCondense autoFocus={false} />
|
<ComposeFormContainer group={group} shouldCondense autoFocus={false} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
|
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 OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
|
||||||
import IconButton from 'soapbox/components/icon_button';
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
|
|
||||||
|
@ -55,7 +56,9 @@ class Header extends ImmutablePureComponent {
|
||||||
const { dispatch, intl } = this.props;
|
const { dispatch, intl } = this.props;
|
||||||
const { username, password } = this.getFormData(event.target);
|
const { username, password } = this.getFormData(event.target);
|
||||||
dispatch(logIn(intl, username, password)).then(({ access_token }) => {
|
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 => {
|
}).catch(error => {
|
||||||
if (error.response.data.error === 'mfa_required') {
|
if (error.response.data.error === 'mfa_required') {
|
||||||
this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token });
|
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 LandingPage from '../landing_page';
|
||||||
import AboutPage from '../about';
|
import AboutPage from '../about';
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
import { isPrerendered } from 'soapbox/precheck';
|
import { isStandalone } from 'soapbox/utils/state';
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
soapbox: getSoapboxConfig(state),
|
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>
|
<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><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>}
|
{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>
|
<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>}
|
{(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>
|
<li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li>
|
||||||
|
|
|
@ -85,6 +85,7 @@ import {
|
||||||
Preferences,
|
Preferences,
|
||||||
EditProfile,
|
EditProfile,
|
||||||
SoapboxConfig,
|
SoapboxConfig,
|
||||||
|
ExportData,
|
||||||
ImportData,
|
ImportData,
|
||||||
Backups,
|
Backups,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
|
@ -270,6 +271,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
<Redirect exact from='/settings' to='/settings/preferences' />
|
<Redirect exact from='/settings' to='/settings/preferences' />
|
||||||
<WrappedRoute path='/settings/preferences' page={DefaultPage} component={Preferences} content={children} />
|
<WrappedRoute path='/settings/preferences' page={DefaultPage} component={Preferences} content={children} />
|
||||||
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} 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/import' page={DefaultPage} component={ImportData} content={children} />
|
||||||
<WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />
|
<WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />
|
||||||
<WrappedRoute path='/backups' page={DefaultPage} component={Backups} 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');
|
return import(/* webpackChunkName: "features/soapbox_config" */'../../soapbox_config');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ExportData() {
|
||||||
|
return import(/* webpackChunkName: "features/export_data" */ '../../export_data');
|
||||||
|
}
|
||||||
|
|
||||||
export function ImportData() {
|
export function ImportData() {
|
||||||
return import(/* webpackChunkName: "features/import_data" */'../../import_data');
|
return import(/* webpackChunkName: "features/import_data" */'../../import_data');
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,14 @@ export const getPointerPosition = (el, event) => {
|
||||||
return position;
|
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 => ({
|
const mapStateToProps = state => ({
|
||||||
displayMedia: getSettings(state).get('displayMedia'),
|
displayMedia: getSettings(state).get('displayMedia'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,9 +8,10 @@ import './precheck';
|
||||||
import { default as Soapbox } from './containers/soapbox';
|
import { default as Soapbox } from './containers/soapbox';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime';
|
||||||
|
import * as perf from './performance';
|
||||||
import ready from './ready';
|
import ready from './ready';
|
||||||
|
import { NODE_ENV } from 'soapbox/build_config';
|
||||||
const perf = require('./performance');
|
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
perf.start('main()');
|
perf.start('main()');
|
||||||
|
@ -19,9 +20,10 @@ function main() {
|
||||||
const mountNode = document.getElementById('soapbox');
|
const mountNode = document.getElementById('soapbox');
|
||||||
|
|
||||||
ReactDOM.render(<Soapbox />, mountNode);
|
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
|
// avoid offline in dev mode because it's harder to debug
|
||||||
require('offline-plugin/runtime').install();
|
OfflinePluginRuntime.install();
|
||||||
// FIXME: Push notifications are temporarily removed
|
// FIXME: Push notifications are temporarily removed
|
||||||
// store.dispatch(registerPushNotifications.register());
|
// store.dispatch(registerPushNotifications.register());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||||
|
|
||||||
const createAudio = sources => {
|
const createAudio = sources => {
|
||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
sources.forEach(({ type, src }) => {
|
sources.forEach(({ type, src }) => {
|
||||||
|
@ -28,21 +31,21 @@ export default function soundsMiddleware() {
|
||||||
const soundCache = {
|
const soundCache = {
|
||||||
boop: createAudio([
|
boop: createAudio([
|
||||||
{
|
{
|
||||||
src: '/sounds/boop.ogg',
|
src: join(FE_SUBDIRECTORY, '/sounds/boop.ogg'),
|
||||||
type: 'audio/ogg',
|
type: 'audio/ogg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/sounds/boop.mp3',
|
src: join(FE_SUBDIRECTORY, '/sounds/boop.mp3'),
|
||||||
type: 'audio/mpeg',
|
type: 'audio/mpeg',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
chat: createAudio([
|
chat: createAudio([
|
||||||
{
|
{
|
||||||
src: '/sounds/chat.oga',
|
src: join(FE_SUBDIRECTORY, '/sounds/chat.oga'),
|
||||||
type: 'audio/ogg',
|
type: 'audio/ogg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/sounds/chat.mp3',
|
src: join(FE_SUBDIRECTORY, '/sounds/chat.mp3'),
|
||||||
type: 'audio/mpeg',
|
type: 'audio/mpeg',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
|
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
|
||||||
import Avatar from '../components/avatar';
|
import Avatar from '../components/avatar';
|
||||||
|
@ -46,6 +47,8 @@ class HomePage extends ImmutablePureComponent {
|
||||||
render() {
|
render() {
|
||||||
const { me, children, account, showFundingPanel, showCryptoDonatePanel, cryptoLimit, showTrendsPanel, showWhoToFollowPanel } = this.props;
|
const { me, children, account, showFundingPanel, showCryptoDonatePanel, cryptoLimit, showTrendsPanel, showWhoToFollowPanel } = this.props;
|
||||||
|
|
||||||
|
const acct = account ? account.get('acct') : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<div className='page__columns'>
|
<div className='page__columns'>
|
||||||
|
@ -62,9 +65,9 @@ class HomePage extends ImmutablePureComponent {
|
||||||
<div className='columns-area__panels__main'>
|
<div className='columns-area__panels__main'>
|
||||||
<div className='columns-area columns-area--mobile'>
|
<div className='columns-area columns-area--mobile'>
|
||||||
{me && <div className='timeline-compose-block' ref={this.composeBlock}>
|
{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} />
|
<Avatar account={account} size={46} />
|
||||||
</div>
|
</Link>
|
||||||
<ComposeFormContainer
|
<ComposeFormContainer
|
||||||
shouldCondense
|
shouldCondense
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { NODE_ENV } from 'soapbox/build_config';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Tools for performance debugging, only enabled in development mode.
|
// Tools for performance debugging, only enabled in development mode.
|
||||||
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
|
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
|
||||||
|
@ -8,7 +10,7 @@
|
||||||
|
|
||||||
let marky;
|
let marky;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (NODE_ENV === 'development') {
|
||||||
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
|
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
|
||||||
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
|
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
|
||||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
|
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
|
||||||
|
@ -21,13 +23,13 @@ if (process.env.NODE_ENV === 'development') {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start(name) {
|
export function start(name) {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (NODE_ENV === 'development') {
|
||||||
marky.mark(name);
|
marky.mark(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stop(name) {
|
export function stop(name) {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (NODE_ENV === 'development') {
|
||||||
marky.stop(name);
|
marky.stop(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
import { ME_FETCH_SKIP } from '../actions/me';
|
import { ME_FETCH_SKIP } from '../actions/me';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
import { validId, isURL } from 'soapbox/utils/auth';
|
import { validId, isURL } from 'soapbox/utils/auth';
|
||||||
|
import { trim } from 'lodash';
|
||||||
|
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||||
|
|
||||||
const defaultState = ImmutableMap({
|
const defaultState = ImmutableMap({
|
||||||
app: ImmutableMap(),
|
app: ImmutableMap(),
|
||||||
|
@ -18,13 +20,21 @@ const defaultState = ImmutableMap({
|
||||||
me: null,
|
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 getSessionUser = () => {
|
||||||
const id = sessionStorage.getItem('soapbox:auth:me');
|
const id = sessionStorage.getItem(SESSION_KEY);
|
||||||
return validId(id) ? id : undefined;
|
return validId(id) ? id : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessionUser = getSessionUser();
|
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
|
// Checks if the user has an ID and access token
|
||||||
const validUser = user => {
|
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 persistSession = state => {
|
||||||
const me = state.get('me');
|
const me = state.get('me');
|
||||||
if (me && typeof me === 'string') {
|
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) => {
|
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);
|
reload(state);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
INSTANCE_FETCH_SUCCESS,
|
INSTANCE_FETCH_SUCCESS,
|
||||||
|
INSTANCE_FETCH_FAIL,
|
||||||
NODEINFO_FETCH_SUCCESS,
|
NODEINFO_FETCH_SUCCESS,
|
||||||
} from '../actions/instance';
|
} from '../actions/instance';
|
||||||
import { PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
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) {
|
export default function instance(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case PRELOAD_IMPORT:
|
case PRELOAD_IMPORT:
|
||||||
return preloadImport(state, action, '/api/v1/instance');
|
return preloadImport(state, action, '/api/v1/instance');
|
||||||
case INSTANCE_FETCH_SUCCESS:
|
case INSTANCE_FETCH_SUCCESS:
|
||||||
return initialState.mergeDeep(fromJS(action.instance));
|
return initialState.mergeDeep(fromJS(action.instance));
|
||||||
|
case INSTANCE_FETCH_FAIL:
|
||||||
|
return action.error.response.status === 401 ? handleAuthFetch(state) : state;
|
||||||
case NODEINFO_FETCH_SUCCESS:
|
case NODEINFO_FETCH_SUCCESS:
|
||||||
return nodeinfoToInstance(fromJS(action.nodeinfo)).mergeDeep(state);
|
return nodeinfoToInstance(fromJS(action.nodeinfo)).mergeDeep(state);
|
||||||
case ADMIN_CONFIG_UPDATE_REQUEST:
|
case ADMIN_CONFIG_UPDATE_REQUEST:
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const getFeatures = createSelector([
|
||||||
securityAPI: v.software === 'Pleroma',
|
securityAPI: v.software === 'Pleroma',
|
||||||
settingsStore: v.software === 'Pleroma',
|
settingsStore: v.software === 'Pleroma',
|
||||||
accountAliasesAPI: v.software === 'Pleroma',
|
accountAliasesAPI: v.software === 'Pleroma',
|
||||||
|
resetPasswordAPI: v.software === 'Pleroma',
|
||||||
exposableReactions: features.includes('exposable_reactions'),
|
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 { 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 => {
|
export const displayFqn = state => {
|
||||||
const soapbox = getSoapboxConfig(state);
|
const soapbox = getSoapboxConfig(state);
|
||||||
|
@ -8,3 +17,26 @@ export const displayFqn = state => {
|
||||||
export const federationRestrictionsDisclosed = state => {
|
export const federationRestrictionsDisclosed = state => {
|
||||||
return state.hasIn(['instance', 'pleroma', 'metadata', 'federation', 'mrf_policies']);
|
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 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');
|
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||||
whyDidYouRender(React);
|
whyDidYouRender(React);
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__avatar {
|
&__avatar {
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
@media (max-width: 405px) { display: none; }
|
@media (max-width: 405px) { display: none; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ module.exports = (api) => {
|
||||||
|
|
||||||
switch (env) {
|
switch (env) {
|
||||||
case 'production':
|
case 'production':
|
||||||
envOptions.debug = false;
|
|
||||||
config.plugins.push(...[
|
config.plugins.push(...[
|
||||||
'lodash',
|
'lodash',
|
||||||
[
|
[
|
||||||
|
@ -51,7 +50,6 @@ module.exports = (api) => {
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'development':
|
case 'development':
|
||||||
envOptions.debug = true;
|
|
||||||
config.plugins.push(...[
|
config.plugins.push(...[
|
||||||
'@babel/transform-react-jsx-source',
|
'@babel/transform-react-jsx-source',
|
||||||
'@babel/transform-react-jsx-self',
|
'@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",
|
"start": "npx webpack-dev-server --config webpack",
|
||||||
"dev": "${npm_execpath} run start",
|
"dev": "${npm_execpath} run start",
|
||||||
"build": "npx webpack --config webpack",
|
"build": "npx webpack --config webpack",
|
||||||
|
"jsdoc": "npx jsdoc -c jsdoc.conf.js",
|
||||||
"manage:translations": "node ./webpack/translationRunner.js",
|
"manage:translations": "node ./webpack/translationRunner.js",
|
||||||
"test": "${npm_execpath} run test:lint && ${npm_execpath} run test:jest",
|
"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",
|
"test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
"@babel/runtime": "^7.14.6",
|
"@babel/runtime": "^7.14.6",
|
||||||
"@fontsource/montserrat": "^4.5.1",
|
"@fontsource/montserrat": "^4.5.1",
|
||||||
"@fontsource/roboto": "^4.5.0",
|
"@fontsource/roboto": "^4.5.0",
|
||||||
|
"@lcdp/offline-plugin": "^5.1.0",
|
||||||
"@popperjs/core": "^2.4.4",
|
"@popperjs/core": "^2.4.4",
|
||||||
"@welldone-software/why-did-you-render": "^6.2.0",
|
"@welldone-software/why-did-you-render": "^6.2.0",
|
||||||
"array-includes": "^3.0.3",
|
"array-includes": "^3.0.3",
|
||||||
|
@ -62,9 +64,9 @@
|
||||||
"blurhash": "^1.0.0",
|
"blurhash": "^1.0.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"browserslist": "^4.16.6",
|
"browserslist": "^4.16.6",
|
||||||
|
"cheerio": "^1.0.0-rc.10",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"compression-webpack-plugin": "^6.0.2",
|
"copy-webpack-plugin": "^9.0.1",
|
||||||
"copy-webpack-plugin": "6.4.0",
|
|
||||||
"core-js": "^3.15.2",
|
"core-js": "^3.15.2",
|
||||||
"cryptocurrency-icons": "^0.17.2",
|
"cryptocurrency-icons": "^0.17.2",
|
||||||
"css-loader": "^5.2.6",
|
"css-loader": "^5.2.6",
|
||||||
|
@ -73,13 +75,14 @@
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"emoji-datasource": "5.0.0",
|
"emoji-datasource": "5.0.0",
|
||||||
"emoji-mart": "^3.0.1",
|
"emoji-mart": "^3.0.1",
|
||||||
|
"entities": "^3.0.1",
|
||||||
"es6-symbol": "^3.1.1",
|
"es6-symbol": "^3.1.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"exif-js": "^2.3.0",
|
"exif-js": "^2.3.0",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"fork-awesome": "https://github.com/alexgleason/Fork-Awesome#c23fd34246a9f33c4bf24ea095a4cf26e7abe265",
|
"fork-awesome": "https://github.com/alexgleason/Fork-Awesome#c23fd34246a9f33c4bf24ea095a4cf26e7abe265",
|
||||||
"html-webpack-harddisk-plugin": "^1.0.1",
|
"html-webpack-harddisk-plugin": "^2.0.0",
|
||||||
"html-webpack-plugin": "^4.3.0",
|
"html-webpack-plugin": "^5.3.2",
|
||||||
"http-link-header": "^1.0.2",
|
"http-link-header": "^1.0.2",
|
||||||
"immutable": "^4.0.0-rc.12",
|
"immutable": "^4.0.0-rc.12",
|
||||||
"imports-loader": "^1.0.0",
|
"imports-loader": "^1.0.0",
|
||||||
|
@ -87,8 +90,9 @@
|
||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
"intl-messageformat": "^9.0.0",
|
"intl-messageformat": "^9.0.0",
|
||||||
"intl-messageformat-parser": "^6.0.0",
|
"intl-messageformat-parser": "^6.0.0",
|
||||||
"intl-pluralrules": "^1.1.1",
|
"intl-pluralrules": "^1.3.0",
|
||||||
"is-nan": "^1.2.1",
|
"is-nan": "^1.2.1",
|
||||||
|
"jsdoc": "~3.6.7",
|
||||||
"lodash": "^4.7.11",
|
"lodash": "^4.7.11",
|
||||||
"mark-loader": "^0.1.6",
|
"mark-loader": "^0.1.6",
|
||||||
"marky": "^1.2.1",
|
"marky": "^1.2.1",
|
||||||
|
@ -96,10 +100,11 @@
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"object-fit-images": "^3.2.3",
|
"object-fit-images": "^3.2.3",
|
||||||
"object.values": "^1.1.0",
|
"object.values": "^1.1.0",
|
||||||
"offline-plugin": "^5.0.7",
|
"path-browserify": "^1.0.1",
|
||||||
"postcss": "^8.1.1",
|
"postcss": "^8.1.1",
|
||||||
"postcss-loader": "^4.0.3",
|
"postcss-loader": "^4.0.3",
|
||||||
"postcss-object-fit-images": "^1.1.2",
|
"postcss-object-fit-images": "^1.1.2",
|
||||||
|
"process": "^0.11.10",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"punycode": "^2.1.0",
|
"punycode": "^2.1.0",
|
||||||
"qrcode.react": "^1.0.0",
|
"qrcode.react": "^1.0.0",
|
||||||
|
@ -134,34 +139,35 @@
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"stringz": "^2.0.0",
|
"stringz": "^2.0.0",
|
||||||
"substring-trie": "^1.0.2",
|
"substring-trie": "^1.0.2",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^5.2.3",
|
||||||
"tiny-queue": "^0.2.1",
|
"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",
|
"twemoji": "https://github.com/twitter/twemoji#v13.0.2",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"unused-files-webpack-plugin": "^3.4.0",
|
"util": "^0.12.4",
|
||||||
"uuid": "^8.0.0",
|
"uuid": "^8.3.2",
|
||||||
"webpack": "^4.41.2",
|
"webpack": "^5.52.0",
|
||||||
"webpack-assets-manifest": "^3.1.1",
|
"webpack-assets-manifest": "^5.0.6",
|
||||||
"webpack-bundle-analyzer": "^4.0.0",
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
"webpack-cli": "^3.3.2",
|
"webpack-cli": "^4.8.0",
|
||||||
"webpack-merge": "^5.2.0",
|
"webpack-merge": "^5.8.0",
|
||||||
"websocket.js": "^0.1.12",
|
"websocket.js": "^0.1.12",
|
||||||
"wicg-inert": "^3.1.1"
|
"wicg-inert": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios-mock-adapter": "^1.18.1",
|
"axios-mock-adapter": "^1.18.1",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^27.0.6",
|
"babel-jest": "^27.1.0",
|
||||||
"enzyme": "^3.8.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.7.1",
|
"enzyme-adapter-react-16": "^1.15.6",
|
||||||
"eslint": "^7.0.0",
|
"eslint": "^7.0.0",
|
||||||
"eslint-plugin-import": "~2.22.0",
|
"eslint-plugin-import": "^2.24.2",
|
||||||
"eslint-plugin-jsx-a11y": "~6.4.0",
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-promise": "~4.2.0",
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
"eslint-plugin-react": "~7.21.0",
|
"eslint-plugin-react": "^7.25.1",
|
||||||
"eslint-plugin-react-hooks": "^4.0.4",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"jest": "^27.0.6",
|
"jest": "^27.1.0",
|
||||||
"raf": "^3.4.1",
|
"raf": "^3.4.1",
|
||||||
"react-intl-translations-manager": "^5.0.3",
|
"react-intl-translations-manager": "^5.0.3",
|
||||||
"react-test-renderer": "^16.13.1",
|
"react-test-renderer": "^16.13.1",
|
||||||
|
@ -169,7 +175,7 @@
|
||||||
"stylelint": "^13.7.2",
|
"stylelint": "^13.7.2",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
"stylelint-scss": "^3.18.0",
|
"stylelint-scss": "^3.18.0",
|
||||||
"webpack-dev-server": "^3.5.1",
|
"webpack-dev-server": "^4.1.0",
|
||||||
"yargs": "^16.0.3"
|
"yargs": "^16.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const { env } = require('process');
|
const { env } = require('process');
|
||||||
|
|
||||||
|
const {
|
||||||
|
FE_SUBDIRECTORY,
|
||||||
|
FE_BUILD_DIR,
|
||||||
|
} = require(join(__dirname, '..', 'app', 'soapbox', 'build_config'));
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
source_path: 'app',
|
source_path: 'app',
|
||||||
public_root_path: 'static',
|
public_root_path: FE_BUILD_DIR,
|
||||||
test_root_path: 'static-test',
|
test_root_path: `${FE_BUILD_DIR}-test`,
|
||||||
cache_path: 'tmp/cache/webpacker',
|
cache_path: 'tmp/cache',
|
||||||
resolved_paths: [],
|
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' ],
|
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 outputDir = env.NODE_ENV === 'test' ? settings.test_root_path : settings.public_root_path;
|
||||||
|
|
||||||
const output = {
|
const output = {
|
||||||
path: join(__dirname, '..', outputDir),
|
path: join(__dirname, '..', outputDir, FE_SUBDIRECTORY),
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||||
console.log('Running in development mode'); // eslint-disable-line no-console
|
console.log('Running in development mode'); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
const { join } = require('path');
|
||||||
const { merge } = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const sharedConfig = require('./shared');
|
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 patronUrl = process.env.PATRON_URL || 'http://localhost:3037';
|
||||||
const secureProxy = !(process.env.PROXY_HTTPS_INSECURE === 'true');
|
const secureProxy = !(process.env.PROXY_HTTPS_INSECURE === 'true');
|
||||||
|
|
||||||
|
const { FE_SUBDIRECTORY } = require(join(__dirname, '..', 'app', 'soapbox', 'build_config'));
|
||||||
|
|
||||||
const backendEndpoints = [
|
const backendEndpoints = [
|
||||||
'/api',
|
'/api',
|
||||||
'/pleroma',
|
'/pleroma',
|
||||||
|
@ -54,6 +57,7 @@ module.exports = merge(sharedConfig, {
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
|
preset: 'errors-warnings',
|
||||||
errorDetails: true,
|
errorDetails: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -61,37 +65,31 @@ module.exports = merge(sharedConfig, {
|
||||||
pathinfo: true,
|
pathinfo: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watchOptions: Object.assign(
|
||||||
|
{},
|
||||||
|
{ ignored: '**/node_modules/**' },
|
||||||
|
watchOptions,
|
||||||
|
),
|
||||||
|
|
||||||
devServer: {
|
devServer: {
|
||||||
clientLogLevel: 'none',
|
|
||||||
compress: true,
|
compress: true,
|
||||||
quiet: false,
|
|
||||||
disableHostCheck: true,
|
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3036,
|
port: 3036,
|
||||||
https: false,
|
https: false,
|
||||||
hot: false,
|
hot: false,
|
||||||
inline: true,
|
|
||||||
useLocalIp: false,
|
|
||||||
public: 'localhost:3036',
|
|
||||||
historyApiFallback: {
|
historyApiFallback: {
|
||||||
disableDotRule: true,
|
disableDotRule: true,
|
||||||
|
index: join(FE_SUBDIRECTORY, '/'),
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
},
|
},
|
||||||
overlay: true,
|
client: {
|
||||||
stats: {
|
overlay: true,
|
||||||
entrypoints: false,
|
},
|
||||||
errorDetails: false,
|
static: {
|
||||||
modules: false,
|
serveIndex: true,
|
||||||
moduleTrace: false,
|
|
||||||
},
|
},
|
||||||
watchOptions: Object.assign(
|
|
||||||
{},
|
|
||||||
{ ignored: '**/node_modules/**' },
|
|
||||||
watchOptions,
|
|
||||||
),
|
|
||||||
serveIndex: true,
|
|
||||||
proxy: makeProxyConfig(),
|
proxy: makeProxyConfig(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,45 +3,24 @@ console.log('Running in production mode'); // eslint-disable-line no-console
|
||||||
|
|
||||||
const { merge } = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
const OfflinePlugin = require('offline-plugin');
|
const OfflinePlugin = require('@lcdp/offline-plugin');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
|
||||||
|
|
||||||
const sharedConfig = require('./shared');
|
const sharedConfig = require('./shared');
|
||||||
|
|
||||||
module.exports = merge(sharedConfig, {
|
module.exports = merge(sharedConfig, {
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
stats: 'normal',
|
stats: 'errors-warnings',
|
||||||
bail: true,
|
bail: true,
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: true,
|
minimize: true,
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
cache: true,
|
|
||||||
parallel: true,
|
|
||||||
sourceMap: true,
|
|
||||||
|
|
||||||
terserOptions: {
|
|
||||||
warnings: false,
|
|
||||||
mangle: false,
|
|
||||||
|
|
||||||
output: {
|
|
||||||
comments: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new CompressionPlugin({
|
// Generates report.html
|
||||||
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
new BundleAnalyzerPlugin({
|
||||||
}),
|
|
||||||
new BundleAnalyzerPlugin({ // generates report.html
|
|
||||||
analyzerMode: 'static',
|
analyzerMode: 'static',
|
||||||
openAnalyzer: false,
|
openAnalyzer: false,
|
||||||
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
logLevel: 'silent',
|
||||||
}),
|
}),
|
||||||
new OfflinePlugin({
|
new OfflinePlugin({
|
||||||
caches: {
|
caches: {
|
||||||
|
@ -79,11 +58,15 @@ module.exports = merge(sharedConfig, {
|
||||||
'**/*.map',
|
'**/*.map',
|
||||||
'stats.json',
|
'stats.json',
|
||||||
'report.html',
|
'report.html',
|
||||||
|
'instance/**/*',
|
||||||
// any browser that supports ServiceWorker will support woff2
|
// any browser that supports ServiceWorker will support woff2
|
||||||
'**/*.eot',
|
'**/*.eot',
|
||||||
'**/*.ttf',
|
'**/*.ttf',
|
||||||
'**/*-webfont-*.svg',
|
'**/*-webfont-*.svg',
|
||||||
'**/*.woff',
|
'**/*.woff',
|
||||||
|
// Sounds return a 206 causing sw.js to crash
|
||||||
|
// https://stackoverflow.com/a/66335638
|
||||||
|
'sounds/**/*',
|
||||||
// Don't cache index.html
|
// Don't cache index.html
|
||||||
'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: {
|
options: {
|
||||||
name(file) {
|
name(file) {
|
||||||
if (file.includes(settings.source_path)) {
|
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),
|
context: join(settings.source_path),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const babel = require('./babel');
|
const babel = require('./babel');
|
||||||
const git = require('./babel-git');
|
const git = require('./babel-git');
|
||||||
const gitRefresh = require('./git-refresh');
|
const gitRefresh = require('./git-refresh');
|
||||||
|
const buildConfig = require('./babel-build-config');
|
||||||
const css = require('./css');
|
const css = require('./css');
|
||||||
const file = require('./file');
|
const file = require('./file');
|
||||||
const nodeModules = require('./node_modules');
|
const nodeModules = require('./node_modules');
|
||||||
|
@ -15,4 +16,5 @@ module.exports = [
|
||||||
babel,
|
babel,
|
||||||
git,
|
git,
|
||||||
gitRefresh,
|
gitRefresh,
|
||||||
|
buildConfig,
|
||||||
];
|
];
|
||||||
|
|
|
@ -7,10 +7,28 @@ const AssetsManifestPlugin = require('webpack-assets-manifest');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
|
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
const { UnusedFilesWebpackPlugin } = require('unused-files-webpack-plugin');
|
|
||||||
const { env, settings, output } = require('./configuration');
|
const { env, settings, output } = require('./configuration');
|
||||||
const rules = require('./rules');
|
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 = {
|
module.exports = {
|
||||||
entry: Object.assign(
|
entry: Object.assign(
|
||||||
{ application: resolve('app/application.js') },
|
{ application: resolve('app/application.js') },
|
||||||
|
@ -20,19 +38,21 @@ module.exports = {
|
||||||
output: {
|
output: {
|
||||||
filename: 'packs/js/[name]-[chunkhash].js',
|
filename: 'packs/js/[name]-[chunkhash].js',
|
||||||
chunkFilename: 'packs/js/[name]-[chunkhash].chunk.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,
|
path: output.path,
|
||||||
publicPath: '/',
|
publicPath: join(FE_SUBDIRECTORY, '/'),
|
||||||
},
|
},
|
||||||
|
|
||||||
optimization: {
|
optimization: {
|
||||||
|
chunkIds: 'total-size',
|
||||||
|
moduleIds: 'size',
|
||||||
runtimeChunk: {
|
runtimeChunk: {
|
||||||
name: 'common',
|
name: 'common',
|
||||||
},
|
},
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
cacheGroups: {
|
cacheGroups: {
|
||||||
default: false,
|
default: false,
|
||||||
vendors: false,
|
defaultVendors: false,
|
||||||
common: {
|
common: {
|
||||||
name: 'common',
|
name: 'common',
|
||||||
chunks: 'all',
|
chunks: 'all',
|
||||||
|
@ -42,7 +62,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
occurrenceOrder: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
|
@ -51,13 +70,9 @@ module.exports = {
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
|
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
new webpack.ProvidePlugin({
|
||||||
/^history\//, (resource) => {
|
process: 'process/browser',
|
||||||
// temporary fix for https://github.com/ReactTraining/react-router/issues/5576
|
}),
|
||||||
// to reduce bundle size
|
|
||||||
resource.request = resource.request.replace(/^history/, 'history/es');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'packs/css/[name]-[contenthash:8].css',
|
filename: 'packs/css/[name]-[contenthash:8].css',
|
||||||
chunkFilename: 'packs/css/[name]-[contenthash:8].chunk.css',
|
chunkFilename: 'packs/css/[name]-[contenthash:8].chunk.css',
|
||||||
|
@ -68,28 +83,9 @@ module.exports = {
|
||||||
writeToDisk: true,
|
writeToDisk: true,
|
||||||
publicPath: true,
|
publicPath: true,
|
||||||
}),
|
}),
|
||||||
// https://www.npmjs.com/package/unused-files-webpack-plugin#options
|
// https://github.com/jantimon/html-webpack-plugin#options
|
||||||
new UnusedFilesWebpackPlugin({
|
new HtmlWebpackPlugin(makeHtmlConfig()),
|
||||||
patterns: ['app/**/*.*'],
|
new HtmlWebpackPlugin(makeHtmlConfig({ filename: '404.html' })),
|
||||||
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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new HtmlWebpackHarddiskPlugin(),
|
new HtmlWebpackHarddiskPlugin(),
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [{
|
patterns: [{
|
||||||
|
@ -98,6 +94,12 @@ module.exports = {
|
||||||
}, {
|
}, {
|
||||||
from: join(__dirname, '../node_modules/emoji-datasource/img/twitter/sheets/32.png'),
|
from: join(__dirname, '../node_modules/emoji-datasource/img/twitter/sheets/32.png'),
|
||||||
to: join(output.path, 'emoji/sheet_13.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: {
|
options: {
|
||||||
concurrency: 100,
|
concurrency: 100,
|
||||||
|
@ -111,15 +113,13 @@ module.exports = {
|
||||||
resolve(settings.source_path),
|
resolve(settings.source_path),
|
||||||
'node_modules',
|
'node_modules',
|
||||||
],
|
],
|
||||||
|
fallback: {
|
||||||
|
path: require.resolve('path-browserify'),
|
||||||
|
util: require.resolve('util'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveLoader: {
|
resolveLoader: {
|
||||||
modules: ['node_modules'],
|
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
|
console.log('Running in test mode'); // eslint-disable-line no-console
|
||||||
|
|
||||||
const { merge } = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const sharedConfig = require('./shared.js');
|
const sharedConfig = require('./shared');
|
||||||
|
|
||||||
module.exports = merge(sharedConfig, {
|
module.exports = merge(sharedConfig, {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
|
|
Loading…
Reference in New Issue