Merge branch 'snackbar' into 'develop'

Snackbars

See merge request soapbox-pub/soapbox-fe!243
This commit is contained in:
Alex Gleason 2020-09-30 00:43:11 +00:00
commit d022978078
18 changed files with 122 additions and 44 deletions

View file

@ -10,7 +10,7 @@ describe('logOut()', () => {
it('creates expected actions', () => {
const expectedActions = [
{ type: AUTH_LOGGED_OUT },
{ type: ALERT_SHOW, title: 'Successfully logged out.', message: '' },
{ type: ALERT_SHOW, message: 'Logged out.', severity: 'success' },
];
const store = mockStore(ImmutableMap());

View file

@ -1,4 +1,3 @@
//test
import { defineMessages } from 'react-intl';
const messages = defineMessages({
@ -23,11 +22,12 @@ export function clearAlert() {
};
};
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, severity = 'info') {
return {
type: ALERT_SHOW,
title,
message,
severity,
};
};
@ -47,9 +47,9 @@ export function showAlertForError(error) {
message = data.error;
}
return showAlert(title, message);
return showAlert(title, message, 'error');
} else {
console.error(error);
return showAlert();
return showAlert(undefined, undefined, 'error');
}
}

View file

@ -1,5 +1,5 @@
import api from '../api';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
import { fetchMe } from 'soapbox/actions/me';
export const AUTH_APP_CREATED = 'AUTH_APP_CREATED';
@ -136,7 +136,7 @@ export function logIn(username, password) {
if (error.response.data.error === 'mfa_required') {
throw error;
} else {
dispatch(showAlert('Login failed.', 'Invalid username or password.'));
dispatch(snackbar.error('Invalid username or password.'));
}
throw error;
});
@ -156,7 +156,7 @@ export function logOut() {
token: state.getIn(['auth', 'user', 'access_token']),
});
dispatch(showAlert('Successfully logged out.', ''));
dispatch(snackbar.success('Logged out.'));
};
}
@ -172,9 +172,9 @@ export function register(params) {
dispatch({ type: AUTH_REGISTER_SUCCESS, token: response.data });
dispatch(authLoggedIn(response.data));
if (needsConfirmation) {
return dispatch(showAlert('', 'Check your email for further instructions.'));
return dispatch(snackbar.info('You must confirm your email.'));
} else if (needsApproval) {
return dispatch(showAlert('', 'Your account has been submitted for approval.'));
return dispatch(snackbar.info('Your account is being reviewed.'));
} else {
return dispatch(fetchMe());
}
@ -232,7 +232,7 @@ export function deleteAccount(password) {
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
dispatch({ type: DELETE_ACCOUNT_SUCCESS, response });
dispatch({ type: AUTH_LOGGED_OUT });
dispatch(showAlert('Successfully logged out.', ''));
dispatch(snackbar.success('Logged out.'));
}).catch(error => {
dispatch({ type: DELETE_ACCOUNT_FAIL, error, skipAlert: true });
throw error;

View file

@ -224,7 +224,7 @@ export function uploadCompose(files) {
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit));
dispatch(showAlert(undefined, messages.uploadErrorLimit, 'error'));
return;
}

View file

@ -1,5 +1,5 @@
import api from '../api';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
@ -47,7 +47,7 @@ export function createFilter(phrase, expires_at, context, whole_word, irreversib
expires_at,
}).then(response => {
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
dispatch(showAlert('', 'Filter added'));
dispatch(snackbar.success('Filter added.'));
}).catch(error => {
dispatch({ type: FILTERS_CREATE_FAIL, error });
});
@ -60,7 +60,7 @@ export function deleteFilter(id) {
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'));
dispatch(snackbar.success('Filter deleted.'));
}).catch(error => {
dispatch({ type: FILTERS_DELETE_FAIL, error });
});

View file

@ -1,5 +1,5 @@
import api from '../api';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
export const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST';
export const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS';
@ -19,7 +19,7 @@ export function importFollows(params) {
return api(getState)
.post('/api/pleroma/follow_import', params)
.then(response => {
dispatch(showAlert('', 'Followers imported successfully'));
dispatch(snackbar.success('Followers imported successfully'));
dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_FOLLOWS_FAIL, error });
@ -33,7 +33,7 @@ export function importBlocks(params) {
return api(getState)
.post('/api/pleroma/blocks_import', params)
.then(response => {
dispatch(showAlert('', 'Blocks imported successfully'));
dispatch(snackbar.success('Blocks imported successfully'));
dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_BLOCKS_FAIL, error });
@ -47,7 +47,7 @@ export function importMutes(params) {
return api(getState)
.post('/api/pleroma/mutes_import', params)
.then(response => {
dispatch(showAlert('', 'Mutes imported successfully'));
dispatch(snackbar.success('Mutes imported successfully'));
dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_MUTES_FAIL, error });

View file

@ -1,6 +1,6 @@
import api from '../api';
import { importFetchedAccounts, importFetchedStatus } from './importer';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
@ -211,7 +211,7 @@ export function bookmark(status) {
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status, response.data));
dispatch(showAlert('', 'Bookmark added'));
dispatch(snackbar.success('Bookmark added'));
}).catch(function(error) {
dispatch(bookmarkFail(status, error));
});
@ -225,7 +225,7 @@ export function unbookmark(status) {
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status, response.data));
dispatch(showAlert('', 'Bookmark removed'));
dispatch(snackbar.success('Bookmark removed'));
}).catch(error => {
dispatch(unbookmarkFail(status, error));
});

View file

@ -0,0 +1,25 @@
import { ALERT_SHOW } from './alerts';
const show = (severity, message) => ({
type: ALERT_SHOW,
message,
severity,
});
export function info(message) {
return show('info', message);
};
export function success(message) {
return show('success', message);
};
export function error(message) {
return show('error', message);
};
export default {
info,
success,
error,
};

View file

@ -4,7 +4,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { resetPassword } from 'soapbox/actions/auth';
import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
import { Redirect } from 'react-router-dom';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
export default @connect()
class PasswordReset extends ImmutablePureComponent {
@ -20,7 +20,7 @@ class PasswordReset extends ImmutablePureComponent {
this.setState({ isLoading: true });
dispatch(resetPassword(nicknameOrEmail)).then(() => {
this.setState({ isLoading: false, success: true });
dispatch(showAlert('Password reset received. Check your email for further instructions.', ''));
dispatch(snackbar.info('Check your email for confirmation.'));
}).catch(error => {
this.setState({ isLoading: false });
});

View file

@ -4,7 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
import Column from '../ui/components/column';
import {
SimpleForm,
@ -124,7 +124,7 @@ class EditProfile extends ImmutablePureComponent {
const { dispatch } = this.props;
dispatch(patchMe(this.getFormdata())).then(() => {
this.setState({ isLoading: false });
dispatch(showAlert('', 'Profile saved!'));
dispatch(snackbar.success('Profile saved!'));
}).catch((error) => {
this.setState({ isLoading: false });
});

View file

@ -14,7 +14,7 @@ import {
SelectDropdown,
Checkbox,
} from 'soapbox/features/forms';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
import Icon from 'soapbox/components/icon';
import ColumnSubheading from '../ui/components/column_subheading';
@ -114,7 +114,7 @@ class Filters extends ImmutablePureComponent {
dispatch(createFilter(phrase, expires_at, context, whole_word, irreversible)).then(response => {
return dispatch(fetchFilters());
}).catch(error => {
dispatch(showAlert('', intl.formatMessage(messages.create_error)));
dispatch(snackbar.error(intl.formatMessage(messages.create_error)));
});
}
@ -123,7 +123,7 @@ class Filters extends ImmutablePureComponent {
dispatch(deleteFilter(e.currentTarget.dataset.value)).then(response => {
return dispatch(fetchFilters());
}).catch(error => {
dispatch(showAlert('', intl.formatMessage(messages.delete_error)));
dispatch(snackbar.error(intl.formatMessage(messages.delete_error)));
});
}

View file

@ -20,7 +20,7 @@ import {
deleteAccount,
} from 'soapbox/actions/auth';
import { fetchUserMfaSettings } from '../../actions/mfa';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
import { changeSetting, getSettings } from 'soapbox/actions/settings';
/*
@ -119,10 +119,10 @@ class ChangeEmailForm extends ImmutablePureComponent {
this.setState({ isLoading: true });
return dispatch(changeEmail(email, password)).then(() => {
this.setState({ email: '', password: '' }); // TODO: Maybe redirect user
dispatch(showAlert('', intl.formatMessage(messages.updateEmailSuccess)));
dispatch(snackbar.success(intl.formatMessage(messages.updateEmailSuccess)));
}).catch(error => {
this.setState({ password: '' });
dispatch(showAlert('', intl.formatMessage(messages.updateEmailFail)));
dispatch(snackbar.error(intl.formatMessage(messages.updateEmailFail)));
}).then(() => {
this.setState({ isLoading: false });
});
@ -193,10 +193,10 @@ class ChangePasswordForm extends ImmutablePureComponent {
this.setState({ isLoading: true });
return dispatch(changePassword(oldPassword, newPassword, confirmation)).then(() => {
this.clearForm(); // TODO: Maybe redirect user
dispatch(showAlert('', intl.formatMessage(messages.updatePasswordSuccess)));
dispatch(snackbar.success(intl.formatMessage(messages.updatePasswordSuccess)));
}).catch(error => {
this.clearForm();
dispatch(showAlert('', intl.formatMessage(messages.updatePasswordFail)));
dispatch(snackbar.error(intl.formatMessage(messages.updatePasswordFail)));
}).then(() => {
this.setState({ isLoading: false });
});
@ -374,10 +374,10 @@ class DeactivateAccount extends ImmutablePureComponent {
this.setState({ isLoading: true });
return dispatch(deleteAccount(password)).then(() => {
//this.setState({ email: '', password: '' }); // TODO: Maybe redirect user
dispatch(showAlert('', intl.formatMessage(messages.deleteAccountSuccess)));
dispatch(snackbar.success(intl.formatMessage(messages.deleteAccountSuccess)));
}).catch(error => {
this.setState({ password: '' });
dispatch(showAlert('', intl.formatMessage(messages.deleteAccountFail)));
dispatch(snackbar.error(intl.formatMessage(messages.deleteAccountFail)));
}).then(() => {
this.setState({ isLoading: false });
});

View file

@ -10,7 +10,7 @@ import ColumnSubheading from '../ui/components/column_subheading';
import LoadingIndicator from 'soapbox/components/loading_indicator';
import Button from 'soapbox/components/button';
import { changeSetting, getSettings } from 'soapbox/actions/settings';
import { showAlert } from 'soapbox/actions/alerts';
import snackbar from 'soapbox/actions/snackbar';
import {
SimpleForm,
SimpleInput,
@ -129,7 +129,7 @@ class DisableOtpForm extends ImmutablePureComponent {
this.context.router.history.push('../auth/edit');
dispatch(changeSetting(['otpEnabled'], false));
}).catch(error => {
dispatch(showAlert('', intl.formatMessage(messages.disableFail)));
dispatch(snackbar.error(intl.formatMessage(messages.disableFail)));
});
}
@ -180,7 +180,7 @@ class EnableOtpForm extends ImmutablePureComponent {
dispatch(fetchBackupCodes()).then(response => {
this.setState({ backupCodes: response.data.codes });
}).catch(error => {
dispatch(showAlert('', intl.formatMessage(messages.codesFail)));
dispatch(snackbar.error(intl.formatMessage(messages.codesFail)));
});
}
@ -261,7 +261,7 @@ class OtpConfirmForm extends ImmutablePureComponent {
dispatch(fetchToptSetup()).then(response => {
this.setState({ qrCodeURI: response.data.provisioning_uri, confirm_key: response.data.key });
}).catch(error => {
dispatch(showAlert('', intl.formatMessage(messages.qrFail)));
dispatch(snackbar.error(intl.formatMessage(messages.qrFail)));
});
}
@ -276,7 +276,7 @@ class OtpConfirmForm extends ImmutablePureComponent {
dispatch(confirmToptSetup(code, password)).then(response => {
dispatch(changeSetting(['otpEnabled'], true));
}).catch(error => {
dispatch(showAlert('', intl.formatMessage(messages.confirmFail)));
dispatch(snackbar.error(intl.formatMessage(messages.confirmFail)));
});
}

View file

@ -4,6 +4,14 @@ import { NotificationStack } from 'react-notification';
import { dismissAlert } from '../../../actions/alerts';
import { getAlerts } from '../../../selectors';
const defaultBarStyleFactory = (index, style, notification) => {
return Object.assign(
{},
style,
{ bottom: `${14 + index * 12 + index * 42}px` }
);
};
const mapStateToProps = (state, { intl }) => {
const notifications = getAlerts(state);
@ -23,6 +31,8 @@ const mapDispatchToProps = (dispatch) => {
onDismiss: alert => {
dispatch(dismissAlert(alert));
},
barStyleFactory: defaultBarStyleFactory,
activeBarStyleFactory: defaultBarStyleFactory,
};
};

View file

@ -14,6 +14,7 @@ export default function alerts(state = initialState, action) {
key: state.size > 0 ? state.last().get('key') + 1 : 0,
title: action.title,
message: action.message,
severity: action.severity || 'info',
}));
case ALERT_DISMISS:
return state.filterNot(item => item.get('key') === action.alert.key);

View file

@ -124,10 +124,9 @@ export const getAlerts = createSelector([getAlertsBase], (base) => {
message: item.get('message'),
title: item.get('title'),
key: item.get('key'),
className: `snackbar snackbar--${item.get('severity', 'info')}`,
activeClassName: 'snackbar--active',
dismissAfter: 5000,
barStyle: {
zIndex: 200,
},
});
});

View file

@ -70,6 +70,7 @@
@import 'components/profile_hover_card';
@import 'components/filters';
@import 'components/mfa_form';
@import 'components/snackbar';
// Holiday
@import 'holiday/halloween';

View file

@ -0,0 +1,42 @@
.snackbar {
font-size: 16px !important;
padding: 10px 20px 10px 14px !important;
z-index: 9999 !important;
display: flex;
align-items: center;
justify-content: center;
&::before {
font-family: ForkAwesome;
font-size: 20px;
margin-right: 8px;
}
&--info {
background-color: #19759e !important;
&::before {
content: '';
}
}
&--success {
background-color: #199e5a !important;
&::before {
content: '';
}
}
&--error {
background-color: #9e1919 !important;
&::before {
content: '';
}
}
.notification-bar-wrapper {
transform: translateY(1px);
}
}