Add toast with option to open post after publishing in web UI (#25564)
parent
a8edbcf963
commit
a7ca33ad96
|
@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||||
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||||
export const ALERT_NOOP = 'ALERT_NOOP';
|
export const ALERT_NOOP = 'ALERT_NOOP';
|
||||||
|
|
||||||
export function dismissAlert(alert) {
|
export const dismissAlert = alert => ({
|
||||||
return {
|
|
||||||
type: ALERT_DISMISS,
|
type: ALERT_DISMISS,
|
||||||
alert,
|
alert,
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAlert() {
|
export const clearAlert = () => ({
|
||||||
return {
|
|
||||||
type: ALERT_CLEAR,
|
type: ALERT_CLEAR,
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
|
export const showAlert = alert => ({
|
||||||
return {
|
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
title,
|
alert,
|
||||||
message,
|
});
|
||||||
message_values,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showAlertForError(error, skipNotFound = false) {
|
export const showAlertForError = (error, skipNotFound = false) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { data, status, statusText, headers } = error.response;
|
const { data, status, statusText, headers } = error.response;
|
||||||
|
|
||||||
if (skipNotFound && (status === 404 || status === 410)) {
|
|
||||||
// Skip these errors as they are reflected in the UI
|
// Skip these errors as they are reflected in the UI
|
||||||
|
if (skipNotFound && (status === 404 || status === 410)) {
|
||||||
return { type: ALERT_NOOP };
|
return { type: ALERT_NOOP };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limit errors
|
||||||
if (status === 429 && headers['x-ratelimit-reset']) {
|
if (status === 429 && headers['x-ratelimit-reset']) {
|
||||||
const reset_date = new Date(headers['x-ratelimit-reset']);
|
return showAlert({
|
||||||
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
|
title: messages.rateLimitedTitle,
|
||||||
|
message: messages.rateLimitedMessage,
|
||||||
|
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = statusText;
|
return showAlert({
|
||||||
let title = `${status}`;
|
title: `${status}`,
|
||||||
|
message: data.error || statusText,
|
||||||
if (data.error) {
|
});
|
||||||
message = data.error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return showAlert(title, message);
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return showAlert();
|
|
||||||
}
|
return showAlert({
|
||||||
|
title: messages.unexpectedTitle,
|
||||||
|
message: messages.unexpectedMessage,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,8 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
||||||
|
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
|
@ -240,6 +242,13 @@ export function submitCompose(routerHistory) {
|
||||||
insertIfOnline('public');
|
insertIfOnline('public');
|
||||||
insertIfOnline(`account:${response.data.account.id}`);
|
insertIfOnline(`account:${response.data.account.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(showAlert({
|
||||||
|
message: messages.published,
|
||||||
|
action: messages.open,
|
||||||
|
dismissAfter: 10000,
|
||||||
|
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||||
|
}));
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
});
|
});
|
||||||
|
@ -272,15 +281,16 @@ export function uploadCompose(files) {
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||||
const progress = new Array(files.length).fill(0);
|
const progress = new Array(files.length).fill(0);
|
||||||
|
|
||||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||||
|
|
||||||
if (files.length + media.size + pending > uploadLimit) {
|
if (files.length + media.size + pending > uploadLimit) {
|
||||||
dispatch(showAlert(undefined, messages.uploadErrorLimit));
|
dispatch(showAlert({ message: messages.uploadErrorLimit }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getState().getIn(['compose', 'poll'])) {
|
if (getState().getIn(['compose', 'poll'])) {
|
||||||
dispatch(showAlert(undefined, messages.uploadErrorPoll));
|
dispatch(showAlert({ message: messages.uploadErrorPoll }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
dispatch(changePushNotifications(path.slice(1), checked));
|
dispatch(changePushNotifications(path.slice(1), checked));
|
||||||
} else {
|
} else {
|
||||||
dispatch(showAlert(undefined, messages.permissionDenied));
|
dispatch(showAlert({ message: messages.permissionDenied }));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
} else {
|
} else {
|
||||||
dispatch(showAlert(undefined, messages.permissionDenied));
|
dispatch(showAlert({ message: messages.permissionDenied }));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,26 +7,27 @@ import { NotificationStack } from 'react-notification';
|
||||||
import { dismissAlert } from '../../../actions/alerts';
|
import { dismissAlert } from '../../../actions/alerts';
|
||||||
import { getAlerts } from '../../../selectors';
|
import { getAlerts } from '../../../selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, { intl }) => {
|
const formatIfNeeded = (intl, message, values) => {
|
||||||
const notifications = getAlerts(state);
|
if (typeof message === 'object') {
|
||||||
|
return intl.formatMessage(message, values);
|
||||||
notifications.forEach(notification => ['title', 'message'].forEach(key => {
|
|
||||||
const value = notification[key];
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
|
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
return { notifications };
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapStateToProps = (state, { intl }) => ({
|
||||||
return {
|
notifications: getAlerts(state).map(alert => ({
|
||||||
onDismiss: alert => {
|
...alert,
|
||||||
|
action: formatIfNeeded(intl, alert.action, alert.values),
|
||||||
|
title: formatIfNeeded(intl, alert.title, alert.values),
|
||||||
|
message: formatIfNeeded(intl, alert.message, alert.values),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onDismiss (alert) {
|
||||||
dispatch(dismissAlert(alert));
|
dispatch(dismissAlert(alert));
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
||||||
|
|
|
@ -135,6 +135,8 @@
|
||||||
"community.column_settings.remote_only": "Remote only",
|
"community.column_settings.remote_only": "Remote only",
|
||||||
"compose.language.change": "Change language",
|
"compose.language.change": "Change language",
|
||||||
"compose.language.search": "Search languages...",
|
"compose.language.search": "Search languages...",
|
||||||
|
"compose.published.body": "Post published.",
|
||||||
|
"compose.published.open": "Open",
|
||||||
"compose_form.direct_message_warning_learn_more": "Learn more",
|
"compose_form.direct_message_warning_learn_more": "Learn more",
|
||||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
|
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
|
||||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
|
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ALERT_SHOW,
|
ALERT_SHOW,
|
||||||
|
@ -8,17 +8,20 @@ import {
|
||||||
|
|
||||||
const initialState = ImmutableList([]);
|
const initialState = ImmutableList([]);
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
const addAlert = (state, alert) =>
|
||||||
|
state.push({
|
||||||
|
key: id++,
|
||||||
|
...alert,
|
||||||
|
});
|
||||||
|
|
||||||
export default function alerts(state = initialState, action) {
|
export default function alerts(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ALERT_SHOW:
|
case ALERT_SHOW:
|
||||||
return state.push(ImmutableMap({
|
return addAlert(state, action.alert);
|
||||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
|
||||||
title: action.title,
|
|
||||||
message: action.message,
|
|
||||||
message_values: action.message_values,
|
|
||||||
}));
|
|
||||||
case ALERT_DISMISS:
|
case ALERT_DISMISS:
|
||||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
return state.filterNot(item => item.key === action.alert.key);
|
||||||
case ALERT_CLEAR:
|
case ALERT_CLEAR:
|
||||||
return state.clear();
|
return state.clear();
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -84,26 +84,16 @@ export const makeGetPictureInPicture = () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlertsBase = state => state.get('alerts');
|
const ALERT_DEFAULTS = {
|
||||||
|
|
||||||
export const getAlerts = createSelector([getAlertsBase], (base) => {
|
|
||||||
let arr = [];
|
|
||||||
|
|
||||||
base.forEach(item => {
|
|
||||||
arr.push({
|
|
||||||
message: item.get('message'),
|
|
||||||
message_values: item.get('message_values'),
|
|
||||||
title: item.get('title'),
|
|
||||||
key: item.get('key'),
|
|
||||||
dismissAfter: 5000,
|
dismissAfter: 5000,
|
||||||
barStyle: {
|
style: false,
|
||||||
zIndex: 200,
|
};
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return arr;
|
export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
|
||||||
});
|
alerts.map(item => ({
|
||||||
|
...ALERT_DEFAULTS,
|
||||||
|
...item,
|
||||||
|
})).toArray());
|
||||||
|
|
||||||
export const makeGetNotification = () => createSelector([
|
export const makeGetNotification = () => createSelector([
|
||||||
(_, base) => base,
|
(_, base) => base,
|
||||||
|
|
|
@ -9077,3 +9077,62 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-list {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
inset-inline-start: -100%;
|
||||||
|
width: auto;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 0;
|
||||||
|
color: $primary-text-color;
|
||||||
|
background: rgba($black, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(lighten($ui-base-color, 4%), 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba($base-shadow-color, 0.25),
|
||||||
|
0 4px 6px -4px rgba($base-shadow-color, 0.25);
|
||||||
|
cursor: default;
|
||||||
|
transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1);
|
||||||
|
transform: translateZ(0);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 21px;
|
||||||
|
|
||||||
|
&.notification-bar-active {
|
||||||
|
inset-inline-start: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar-title {
|
||||||
|
margin-inline-end: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar-title,
|
||||||
|
.notification-bar-action {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar-action {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-inline-start: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $highlight-text-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: rgba($ui-base-color, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in New Issue