~cytrogen/masto-fe

a7ca33ad96d4ff8ae3b714d7dfbaebc962a86c27 — Eugen Rochko 2 years ago a8edbcf
Add toast with option to open post after publishing in web UI (#25564)

M app/javascript/mastodon/actions/alerts.js => app/javascript/mastodon/actions/alerts.js +29 -33
@@ 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 {
    type: ALERT_DISMISS,
    alert,
  };
}
export const dismissAlert = alert => ({
  type: ALERT_DISMISS,
  alert,
});

export function clearAlert() {
  return {
    type: ALERT_CLEAR,
  };
}
export const clearAlert = () => ({
  type: ALERT_CLEAR,
});

export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
  return {
    type: ALERT_SHOW,
    title,
    message,
    message_values,
  };
}
export const showAlert = alert => ({
  type: ALERT_SHOW,
  alert,
});

export function showAlertForError(error, skipNotFound = false) {
export const showAlertForError = (error, skipNotFound = false) => {
  if (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)) {
      // Skip these errors as they are reflected in the UI
      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}`;
    return showAlert({
      title: `${status}`,
      message: data.error || statusText,
    });
  }

    if (data.error) {
      message = data.error;
    }
  console.error(error);

    return showAlert(title, message);
  } else {
    console.error(error);
    return showAlert();
  }
  return showAlert({
    title: messages.unexpectedTitle,
    message: messages.unexpectedMessage,
  });
}

M app/javascript/mastodon/actions/compose.js => app/javascript/mastodon/actions/compose.js +14 -4
@@ 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));
    });


@@ 269,18 278,19 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
  return function (dispatch, getState) {
    const uploadLimit = 4;
    const media  = getState().getIn(['compose', 'media_attachments']);
    const pending  = getState().getIn(['compose', 'pending_media_attachments']);
    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;
    }


M app/javascript/mastodon/features/notifications/containers/column_settings_container.js => app/javascript/mastodon/features/notifications/containers/column_settings_container.js +2 -2
@@ 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 {

M app/javascript/mastodon/features/ui/containers/notifications_container.js => app/javascript/mastodon/features/ui/containers/notifications_container.js +19 -18
@@ 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);
const formatIfNeeded = (intl, message, values) => {
  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) => {
  return {
    onDismiss: alert => {
      dispatch(dismissAlert(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));

M app/javascript/mastodon/locales/en.json => app/javascript/mastodon/locales/en.json +2 -0
@@ 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.",

M app/javascript/mastodon/reducers/alerts.js => app/javascript/mastodon/reducers/alerts.js +11 -8
@@ 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:

M app/javascript/mastodon/selectors/index.js => app/javascript/mastodon/selectors/index.js +9 -19
@@ 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'),
      dismissAfter: 5000,
      barStyle: {
        zIndex: 200,
      },
    });
  });
const ALERT_DEFAULTS = {
  dismissAfter: 5000,
  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,

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +59 -0
@@ 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);
  }
}