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_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); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user