Add toast with option to open post after publishing in web UI (#25564)
This commit is contained in:
		
							parent
							
								
									a8edbcf963
								
							
						
					
					
						commit
						a7ca33ad96
					
				| @ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS'; | ||||
| export const ALERT_CLEAR   = 'ALERT_CLEAR'; | ||||
| export const ALERT_NOOP    = 'ALERT_NOOP'; | ||||
| 
 | ||||
| export function dismissAlert(alert) { | ||||
|   return { | ||||
| export const dismissAlert = alert => ({ | ||||
|   type: ALERT_DISMISS, | ||||
|   alert, | ||||
|   }; | ||||
| } | ||||
| }); | ||||
| 
 | ||||
| export function clearAlert() { | ||||
|   return { | ||||
| export const clearAlert = () => ({ | ||||
|   type: ALERT_CLEAR, | ||||
|   }; | ||||
| } | ||||
| }); | ||||
| 
 | ||||
| export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) { | ||||
|   return { | ||||
| export const showAlert = alert => ({ | ||||
|   type: ALERT_SHOW, | ||||
|     title, | ||||
|     message, | ||||
|     message_values, | ||||
|   }; | ||||
| } | ||||
|   alert, | ||||
| }); | ||||
| 
 | ||||
| export function showAlertForError(error, skipNotFound = false) { | ||||
| export const showAlertForError = (error, skipNotFound = false) => { | ||||
|   if (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
 | ||||
|     if (skipNotFound && (status === 404 || status === 410)) { | ||||
|       return { type: ALERT_NOOP }; | ||||
|     } | ||||
| 
 | ||||
|     // Rate limit errors
 | ||||
|     if (status === 429 && headers['x-ratelimit-reset']) { | ||||
|       const reset_date = new Date(headers['x-ratelimit-reset']); | ||||
|       return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date }); | ||||
|       return showAlert({ | ||||
|         title: messages.rateLimitedTitle, | ||||
|         message: messages.rateLimitedMessage, | ||||
|         values: { 'retry_time': new Date(headers['x-ratelimit-reset']) }, | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     let message = statusText; | ||||
|     let title   = `${status}`; | ||||
| 
 | ||||
|     if (data.error) { | ||||
|       message = data.error; | ||||
|     return showAlert({ | ||||
|       title: `${status}`, | ||||
|       message: data.error || statusText, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|     return showAlert(title, message); | ||||
|   } else { | ||||
|   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({ | ||||
|   uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, | ||||
|   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) => { | ||||
| @ -240,6 +242,13 @@ export function submitCompose(routerHistory) { | ||||
|         insertIfOnline('public'); | ||||
|         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) { | ||||
|       dispatch(submitComposeFail(error)); | ||||
|     }); | ||||
| @ -272,15 +281,16 @@ export function uploadCompose(files) { | ||||
|     const media = getState().getIn(['compose', 'media_attachments']); | ||||
|     const pending = getState().getIn(['compose', 'pending_media_attachments']); | ||||
|     const progress = new Array(files.length).fill(0); | ||||
| 
 | ||||
|     let total = Array.from(files).reduce((a, v) => a + v.size, 0); | ||||
| 
 | ||||
|     if (files.length + media.size + pending > uploadLimit) { | ||||
|       dispatch(showAlert(undefined, messages.uploadErrorLimit)); | ||||
|       dispatch(showAlert({ message: messages.uploadErrorLimit })); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (getState().getIn(['compose', 'poll'])) { | ||||
|       dispatch(showAlert(undefined, messages.uploadErrorPoll)); | ||||
|       dispatch(showAlert({ message: messages.uploadErrorPoll })); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
|           if (permission === 'granted') { | ||||
|             dispatch(changePushNotifications(path.slice(1), checked)); | ||||
|           } else { | ||||
|             dispatch(showAlert(undefined, messages.permissionDenied)); | ||||
|             dispatch(showAlert({ message: messages.permissionDenied })); | ||||
|           } | ||||
|         })); | ||||
|       } else { | ||||
| @ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
|           if (permission === 'granted') { | ||||
|             dispatch(changeSetting(['notifications', ...path], checked)); | ||||
|           } else { | ||||
|             dispatch(showAlert(undefined, messages.permissionDenied)); | ||||
|             dispatch(showAlert({ message: messages.permissionDenied })); | ||||
|           } | ||||
|         })); | ||||
|       } else { | ||||
|  | ||||
| @ -7,26 +7,27 @@ import { NotificationStack } from 'react-notification'; | ||||
| import { dismissAlert } from '../../../actions/alerts'; | ||||
| import { getAlerts } from '../../../selectors'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { intl }) => { | ||||
|   const notifications = getAlerts(state); | ||||
| 
 | ||||
|   notifications.forEach(notification => ['title', 'message'].forEach(key => { | ||||
|     const value = notification[key]; | ||||
| 
 | ||||
|     if (typeof value === 'object') { | ||||
|       notification[key] = intl.formatMessage(value, notification[`${key}_values`]); | ||||
| const formatIfNeeded = (intl, message, values) => { | ||||
|   if (typeof message === 'object') { | ||||
|     return intl.formatMessage(message, values); | ||||
|   } | ||||
|   })); | ||||
| 
 | ||||
|   return { notifications }; | ||||
|   return message; | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = (dispatch) => { | ||||
|   return { | ||||
|     onDismiss: alert => { | ||||
| const mapStateToProps = (state, { intl }) => ({ | ||||
|   notifications: getAlerts(state).map(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)); | ||||
|   }, | ||||
|   }; | ||||
| }; | ||||
| }); | ||||
| 
 | ||||
| export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack)); | ||||
|  | ||||
| @ -135,6 +135,8 @@ | ||||
|   "community.column_settings.remote_only": "Remote only", | ||||
|   "compose.language.change": "Change language", | ||||
|   "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.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.", | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; | ||||
| import { List as ImmutableList } from 'immutable'; | ||||
| 
 | ||||
| import { | ||||
|   ALERT_SHOW, | ||||
| @ -8,17 +8,20 @@ import { | ||||
| 
 | ||||
| const initialState = ImmutableList([]); | ||||
| 
 | ||||
| let id = 0; | ||||
| 
 | ||||
| const addAlert = (state, alert) => | ||||
|   state.push({ | ||||
|     key: id++, | ||||
|     ...alert, | ||||
|   }); | ||||
| 
 | ||||
| export default function alerts(state = initialState, action) { | ||||
|   switch(action.type) { | ||||
|   case ALERT_SHOW: | ||||
|     return state.push(ImmutableMap({ | ||||
|       key: state.size > 0 ? state.last().get('key') + 1 : 0, | ||||
|       title: action.title, | ||||
|       message: action.message, | ||||
|       message_values: action.message_values, | ||||
|     })); | ||||
|     return addAlert(state, action.alert); | ||||
|   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: | ||||
|     return state.clear(); | ||||
|   default: | ||||
|  | ||||
| @ -84,26 +84,16 @@ export const makeGetPictureInPicture = () => { | ||||
|   })); | ||||
| }; | ||||
| 
 | ||||
| const getAlertsBase = state => state.get('alerts'); | ||||
| 
 | ||||
| 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'), | ||||
| const ALERT_DEFAULTS = { | ||||
|   dismissAfter: 5000, | ||||
|       barStyle: { | ||||
|         zIndex: 200, | ||||
|       }, | ||||
|     }); | ||||
|   }); | ||||
|   style: false, | ||||
| }; | ||||
| 
 | ||||
|   return arr; | ||||
| }); | ||||
| export const getAlerts = createSelector(state => state.get('alerts'), alerts => | ||||
|   alerts.map(item => ({ | ||||
|     ...ALERT_DEFAULTS, | ||||
|     ...item, | ||||
|   })).toArray()); | ||||
| 
 | ||||
| export const makeGetNotification = () => createSelector([ | ||||
|   (_, 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); | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user