Add toast with option to open post after publishing in web UI (#25564)

This commit is contained in:
Eugen Rochko 2023-07-08 20:01:08 +02:00 committed by GitHub
parent a8edbcf963
commit a7ca33ad96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 85 deletions

View file

@ -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, alert,
title, });
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;
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) { if (skipNotFound && (status === 404 || status === 410)) {
// Skip these errors as they are reflected in the UI
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);
return showAlert();
} }
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
} }

View file

@ -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));
}); });
@ -269,18 +278,19 @@ export function submitComposeFail(error) {
export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
const uploadLimit = 4; const uploadLimit = 4;
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;
} }

View file

@ -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 {

View file

@ -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 => { return message;
const value = notification[key];
if (typeof value === 'object') {
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
}
}));
return { notifications };
}; };
const mapDispatchToProps = (dispatch) => { const mapStateToProps = (state, { intl }) => ({
return { notifications: getAlerts(state).map(alert => ({
onDismiss: alert => { ...alert,
dispatch(dismissAlert(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));
},
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack)); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));

View file

@ -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.",

View file

@ -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:

View file

@ -84,26 +84,16 @@ export const makeGetPictureInPicture = () => {
})); }));
}; };
const getAlertsBase = state => state.get('alerts'); const ALERT_DEFAULTS = {
dismissAfter: 5000,
style: false,
};
export const getAlerts = createSelector([getAlertsBase], (base) => { export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
let arr = []; alerts.map(item => ({
...ALERT_DEFAULTS,
base.forEach(item => { ...item,
arr.push({ })).toArray());
message: item.get('message'),
message_values: item.get('message_values'),
title: item.get('title'),
key: item.get('key'),
dismissAfter: 5000,
barStyle: {
zIndex: 200,
},
});
});
return arr;
});
export const makeGetNotification = () => createSelector([ export const makeGetNotification = () => createSelector([
(_, base) => base, (_, base) => base,

View file

@ -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);
}
}