diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js
index a8d6d5f0f..8beb9c1eb 100644
--- a/app/soapbox/actions/auth.js
+++ b/app/soapbox/actions/auth.js
@@ -112,12 +112,32 @@ export function refreshUserToken() {
};
}
+export function otpVerify(code, mfa_token) {
+ return (dispatch, getState) => {
+ const app = getState().getIn(['auth', 'app']);
+ return api(getState, 'app').post('/oauth/mfa/challenge', {
+ client_id: app.get('client_id'),
+ client_secret: app.get('client_secret'),
+ mfa_token: mfa_token,
+ code: code,
+ challenge_type: 'totp',
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
+ }).then(response => {
+ dispatch(authLoggedIn(response.data));
+ });
+ };
+}
+
export function logIn(username, password) {
return (dispatch, getState) => {
return dispatch(createAppAndToken()).then(() => {
return dispatch(createUserToken(username, password));
}).catch(error => {
- dispatch(showAlert('Login failed.', 'Invalid username or password.'));
+ if (error.response.data.error === 'mfa_required') {
+ throw error;
+ } else {
+ dispatch(showAlert('Login failed.', 'Invalid username or password.'));
+ }
throw error;
});
};
diff --git a/app/soapbox/actions/filters.js b/app/soapbox/actions/filters.js
index cff647de3..3448e391c 100644
--- a/app/soapbox/actions/filters.js
+++ b/app/soapbox/actions/filters.js
@@ -1,4 +1,5 @@
import api from '../api';
+import { showAlert } from 'soapbox/actions/alerts';
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
@@ -8,6 +9,10 @@ export const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
+export const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
+export const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
+export const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
+
export const fetchFilters = () => (dispatch, getState) => {
if (!getState().get('me')) return;
@@ -31,13 +36,33 @@ export const fetchFilters = () => (dispatch, getState) => {
}));
};
-export function createFilter(params) {
+export function createFilter(phrase, expires_at, context, whole_word, irreversible) {
return (dispatch, getState) => {
dispatch({ type: FILTERS_CREATE_REQUEST });
- return api(getState).post('/api/v1/filters', params).then(response => {
+ return api(getState).post('/api/v1/filters', {
+ phrase,
+ context,
+ irreversible,
+ whole_word,
+ expires_at,
+ }).then(response => {
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
+ dispatch(showAlert('', 'Filter added'));
}).catch(error => {
dispatch({ type: FILTERS_CREATE_FAIL, error });
});
};
}
+
+
+export function deleteFilter(id) {
+ return (dispatch, getState) => {
+ dispatch({ type: FILTERS_DELETE_REQUEST });
+ return api(getState).delete('/api/v1/filters/'+id).then(response => {
+ dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
+ dispatch(showAlert('', 'Filter deleted'));
+ }).catch(error => {
+ dispatch({ type: FILTERS_DELETE_FAIL, error });
+ });
+ };
+}
diff --git a/app/soapbox/actions/mfa.js b/app/soapbox/actions/mfa.js
new file mode 100644
index 000000000..0a8a706eb
--- /dev/null
+++ b/app/soapbox/actions/mfa.js
@@ -0,0 +1,180 @@
+import api from '../api';
+
+export const TOTP_SETTINGS_FETCH_REQUEST = 'TOTP_SETTINGS_FETCH_REQUEST';
+export const TOTP_SETTINGS_FETCH_SUCCESS = 'TOTP_SETTINGS_FETCH_SUCCESS';
+export const TOTP_SETTINGS_FETCH_FAIL = 'TOTP_SETTINGS_FETCH_FAIL';
+
+export const BACKUP_CODES_FETCH_REQUEST = 'BACKUP_CODES_FETCH_REQUEST';
+export const BACKUP_CODES_FETCH_SUCCESS = 'BACKUP_CODES_FETCH_SUCCESS';
+export const BACKUP_CODES_FETCH_FAIL = 'BACKUP_CODES_FETCH_FAIL';
+
+export const TOTP_SETUP_FETCH_REQUEST = 'TOTP_SETUP_FETCH_REQUEST';
+export const TOTP_SETUP_FETCH_SUCCESS = 'TOTP_SETUP_FETCH_SUCCESS';
+export const TOTP_SETUP_FETCH_FAIL = 'TOTP_SETUP_FETCH_FAIL';
+
+export const CONFIRM_TOTP_REQUEST = 'CONFIRM_TOTP_REQUEST';
+export const CONFIRM_TOTP_SUCCESS = 'CONFIRM_TOTP_SUCCESS';
+export const CONFIRM_TOTP_FAIL = 'CONFIRM_TOTP_FAIL';
+
+export const DISABLE_TOTP_REQUEST = 'DISABLE_TOTP_REQUEST';
+export const DISABLE_TOTP_SUCCESS = 'DISABLE_TOTP_SUCCESS';
+export const DISABLE_TOTP_FAIL = 'DISABLE_TOTP_FAIL';
+
+export function fetchUserMfaSettings() {
+ return (dispatch, getState) => {
+ dispatch({ type: TOTP_SETTINGS_FETCH_REQUEST });
+ return api(getState).get('/api/pleroma/accounts/mfa').then(response => {
+ dispatch({ type: TOTP_SETTINGS_FETCH_SUCCESS, totpEnabled: response.data.totp });
+ return response;
+ }).catch(error => {
+ dispatch({ type: TOTP_SETTINGS_FETCH_FAIL });
+ });
+ };
+}
+
+export function fetchUserMfaSettingsRequest() {
+ return {
+ type: TOTP_SETTINGS_FETCH_REQUEST,
+ };
+};
+
+export function fetchUserMfaSettingsSuccess() {
+ return {
+ type: TOTP_SETTINGS_FETCH_SUCCESS,
+ };
+};
+
+export function fetchUserMfaSettingsFail() {
+ return {
+ type: TOTP_SETTINGS_FETCH_FAIL,
+ };
+};
+
+export function fetchBackupCodes() {
+ return (dispatch, getState) => {
+ dispatch({ type: BACKUP_CODES_FETCH_REQUEST });
+ return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(response => {
+ dispatch({ type: BACKUP_CODES_FETCH_SUCCESS, backup_codes: response.data });
+ return response;
+ }).catch(error => {
+ dispatch({ type: BACKUP_CODES_FETCH_FAIL });
+ });
+ };
+}
+
+export function fetchBackupCodesRequest() {
+ return {
+ type: BACKUP_CODES_FETCH_REQUEST,
+ };
+};
+
+export function fetchBackupCodesSuccess(backup_codes, response) {
+ return {
+ type: BACKUP_CODES_FETCH_SUCCESS,
+ backup_codes: response.data,
+ };
+};
+
+export function fetchBackupCodesFail(error) {
+ return {
+ type: BACKUP_CODES_FETCH_FAIL,
+ error,
+ };
+};
+
+export function fetchToptSetup() {
+ return (dispatch, getState) => {
+ dispatch({ type: TOTP_SETUP_FETCH_REQUEST });
+ return api(getState).get('/api/pleroma/accounts/mfa/setup/totp').then(response => {
+ dispatch({ type: TOTP_SETUP_FETCH_SUCCESS, totp_setup: response.data });
+ return response;
+ }).catch(error => {
+ dispatch({ type: TOTP_SETUP_FETCH_FAIL });
+ });
+ };
+}
+
+export function fetchToptSetupRequest() {
+ return {
+ type: TOTP_SETUP_FETCH_REQUEST,
+ };
+};
+
+export function fetchToptSetupSuccess(totp_setup, response) {
+ return {
+ type: TOTP_SETUP_FETCH_SUCCESS,
+ totp_setup: response.data,
+ };
+};
+
+export function fetchToptSetupFail(error) {
+ return {
+ type: TOTP_SETUP_FETCH_FAIL,
+ error,
+ };
+};
+
+export function confirmToptSetup(code, password) {
+ return (dispatch, getState) => {
+ dispatch({ type: CONFIRM_TOTP_REQUEST, code });
+ return api(getState).post('/api/pleroma/accounts/mfa/confirm/totp', {
+ code,
+ password,
+ }).then(response => {
+ dispatch({ type: CONFIRM_TOTP_SUCCESS });
+ return response;
+ }).catch(error => {
+ dispatch({ type: CONFIRM_TOTP_FAIL });
+ });
+ };
+}
+
+export function confirmToptRequest() {
+ return {
+ type: CONFIRM_TOTP_REQUEST,
+ };
+};
+
+export function confirmToptSuccess(backup_codes, response) {
+ return {
+ type: CONFIRM_TOTP_SUCCESS,
+ };
+};
+
+export function confirmToptFail(error) {
+ return {
+ type: CONFIRM_TOTP_FAIL,
+ error,
+ };
+};
+
+export function disableToptSetup(password) {
+ return (dispatch, getState) => {
+ dispatch({ type: DISABLE_TOTP_REQUEST });
+ return api(getState).delete('/api/pleroma/accounts/mfa/totp', { data: { password } }).then(response => {
+ dispatch({ type: DISABLE_TOTP_SUCCESS });
+ return response;
+ }).catch(error => {
+ dispatch({ type: DISABLE_TOTP_FAIL });
+ });
+ };
+}
+
+export function disableToptRequest() {
+ return {
+ type: DISABLE_TOTP_REQUEST,
+ };
+};
+
+export function disableToptSuccess(backup_codes, response) {
+ return {
+ type: DISABLE_TOTP_SUCCESS,
+ };
+};
+
+export function disableToptFail(error) {
+ return {
+ type: DISABLE_TOTP_FAIL,
+ error,
+ };
+};
diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js
index 0c2f35a8b..e9b3a0c85 100644
--- a/app/soapbox/actions/settings.js
+++ b/app/soapbox/actions/settings.js
@@ -23,6 +23,7 @@ const defaultSettings = ImmutableMap({
themeMode: 'light',
locale: navigator.language.split(/[-_]/)[0] || 'en',
explanationBox: true,
+ otpEnabled: false,
systemFont: false,
dyslexicFont: false,
@@ -32,6 +33,7 @@ const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: true,
reply: true,
+ direct: false,
}),
regex: ImmutableMap({
@@ -72,6 +74,10 @@ const defaultSettings = ImmutableMap({
}),
community: ImmutableMap({
+ shows: ImmutableMap({
+ reblog: true,
+ reply: true,
+ }),
other: ImmutableMap({
onlyMedia: false,
}),
@@ -81,6 +87,10 @@ const defaultSettings = ImmutableMap({
}),
public: ImmutableMap({
+ shows: ImmutableMap({
+ reblog: true,
+ reply: true,
+ }),
other: ImmutableMap({
onlyMedia: false,
}),
diff --git a/app/soapbox/actions/timelines.js b/app/soapbox/actions/timelines.js
index f1213c301..7ae27b8af 100644
--- a/app/soapbox/actions/timelines.js
+++ b/app/soapbox/actions/timelines.js
@@ -148,13 +148,21 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
};
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
+
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
+
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
+
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
+
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
+
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
+
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
+
export const expandGroupTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
+
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
diff --git a/app/soapbox/components/display_name.js b/app/soapbox/components/display_name.js
index 30dbce9a2..7ab8b9e60 100644
--- a/app/soapbox/components/display_name.js
+++ b/app/soapbox/components/display_name.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import VerificationBadge from './verification_badge';
import { acctFull } from '../utils/accounts';
+import { List as ImmutableList } from 'immutable';
export default class DisplayName extends React.PureComponent {
@@ -16,13 +17,14 @@ export default class DisplayName extends React.PureComponent {
const { account, others, children } = this.props;
let displayName, suffix;
+ const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified');
if (others && others.size > 1) {
displayName = others.take(2).map(a => [
,
- a.get('is_verified') &&