Change design of modal loading and error screens in web UI (#33092)
This commit is contained in:
		
					parent
					
						
							
								eef8d2c855
							
						
					
				
			
			
				commit
				
					
						7f2cfcccab
					
				
			
		
					 8 changed files with 118 additions and 234 deletions
				
			
		
							
								
								
									
										22
									
								
								app/javascript/mastodon/components/gif.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/javascript/mastodon/components/gif.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
import { useHovering } from '@/hooks/useHovering';
 | 
			
		||||
import { autoPlayGif } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
export const GIF: React.FC<{
 | 
			
		||||
  src: string;
 | 
			
		||||
  staticSrc: string;
 | 
			
		||||
  className: string;
 | 
			
		||||
  animate?: boolean;
 | 
			
		||||
}> = ({ src, staticSrc, className, animate = autoPlayGif }) => {
 | 
			
		||||
  const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <img
 | 
			
		||||
      className={className}
 | 
			
		||||
      src={hovering || animate ? src : staticSrc}
 | 
			
		||||
      alt=''
 | 
			
		||||
      role='presentation'
 | 
			
		||||
      onMouseEnter={handleMouseEnter}
 | 
			
		||||
      onMouseLeave={handleMouseLeave}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -9,58 +9,7 @@ import { Link } from 'react-router-dom';
 | 
			
		|||
 | 
			
		||||
import { Button } from 'mastodon/components/button';
 | 
			
		||||
import Column from 'mastodon/components/column';
 | 
			
		||||
import { autoPlayGif } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
class GIF extends PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    src: PropTypes.string.isRequired,
 | 
			
		||||
    staticSrc: PropTypes.string.isRequired,
 | 
			
		||||
    className: PropTypes.string,
 | 
			
		||||
    animate: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
    animate: autoPlayGif,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    hovering: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleMouseEnter = () => {
 | 
			
		||||
    const { animate } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!animate) {
 | 
			
		||||
      this.setState({ hovering: true });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleMouseLeave = () => {
 | 
			
		||||
    const { animate } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!animate) {
 | 
			
		||||
      this.setState({ hovering: false });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { src, staticSrc, className, animate } = this.props;
 | 
			
		||||
    const { hovering } = this.state;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <img
 | 
			
		||||
        className={className}
 | 
			
		||||
        src={(hovering || animate) ? src : staticSrc}
 | 
			
		||||
        alt=''
 | 
			
		||||
        role='presentation'
 | 
			
		||||
        onMouseEnter={this.handleMouseEnter}
 | 
			
		||||
        onMouseLeave={this.handleMouseLeave}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
import { GIF } from 'mastodon/components/gif';
 | 
			
		||||
 | 
			
		||||
class CopyButton extends PureComponent {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import { PureComponent } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
 | 
			
		||||
 | 
			
		||||
import { IconButton } from '../../../components/icon_button';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
 | 
			
		||||
  retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
 | 
			
		||||
  close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class BundleModalError extends PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    onRetry: PropTypes.func.isRequired,
 | 
			
		||||
    onClose: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleRetry = () => {
 | 
			
		||||
    this.props.onRetry();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { onClose, intl: { formatMessage } } = this.props;
 | 
			
		||||
 | 
			
		||||
    // Keep the markup in sync with <ModalLoading />
 | 
			
		||||
    // (make sure they have the same dimensions)
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='modal-root__modal error-modal'>
 | 
			
		||||
        <div className='error-modal__body'>
 | 
			
		||||
          <IconButton title={formatMessage(messages.retry)} icon='refresh' iconComponent={RefreshIcon} onClick={this.handleRetry} size={64} />
 | 
			
		||||
          {formatMessage(messages.error)}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='error-modal__footer'>
 | 
			
		||||
          <div>
 | 
			
		||||
            <button
 | 
			
		||||
              onClick={onClose}
 | 
			
		||||
              className='error-modal__nav onboarding-modal__skip'
 | 
			
		||||
            >
 | 
			
		||||
              {formatMessage(messages.close)}
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default injectIntl(BundleModalError);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
import { LoadingIndicator } from '../../../components/loading_indicator';
 | 
			
		||||
 | 
			
		||||
// Keep the markup in sync with <BundleModalError />
 | 
			
		||||
// (make sure they have the same dimensions)
 | 
			
		||||
const ModalLoading = () => (
 | 
			
		||||
  <div className='modal-root__modal error-modal'>
 | 
			
		||||
    <div className='error-modal__body'>
 | 
			
		||||
      <LoadingIndicator />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div className='error-modal__footer'>
 | 
			
		||||
      <div>
 | 
			
		||||
        <button className='error-modal__nav onboarding-modal__skip' />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default ModalLoading;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
import { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { Button } from 'mastodon/components/button';
 | 
			
		||||
import { GIF } from 'mastodon/components/gif';
 | 
			
		||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 | 
			
		||||
 | 
			
		||||
export const ModalPlaceholder: React.FC<{
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  onClose: (arg0: string | undefined, arg1: boolean) => void;
 | 
			
		||||
  onRetry?: () => void;
 | 
			
		||||
}> = ({ loading, onClose, onRetry }) => {
 | 
			
		||||
  const handleClose = useCallback(() => {
 | 
			
		||||
    onClose(undefined, false);
 | 
			
		||||
  }, [onClose]);
 | 
			
		||||
 | 
			
		||||
  const handleRetry = useCallback(() => {
 | 
			
		||||
    if (onRetry) onRetry();
 | 
			
		||||
  }, [onRetry]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='modal-root__modal modal-placeholder' aria-busy={loading}>
 | 
			
		||||
      {loading ? (
 | 
			
		||||
        <LoadingIndicator />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div className='modal-placeholder__error'>
 | 
			
		||||
          <GIF
 | 
			
		||||
            src='/oops.gif'
 | 
			
		||||
            staticSrc='/oops.png'
 | 
			
		||||
            className='modal-placeholder__error__image'
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <div className='modal-placeholder__error__message'>
 | 
			
		||||
            <p>
 | 
			
		||||
              <FormattedMessage
 | 
			
		||||
                id='bundle_modal_error.message'
 | 
			
		||||
                defaultMessage='Something went wrong while loading this screen.'
 | 
			
		||||
              />
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
            <div className='modal-placeholder__error__message__actions'>
 | 
			
		||||
              <Button onClick={handleRetry}>
 | 
			
		||||
                <FormattedMessage
 | 
			
		||||
                  id='bundle_modal_error.retry'
 | 
			
		||||
                  defaultMessage='Try again'
 | 
			
		||||
                />
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button onClick={handleClose} className='button button-tertiary'>
 | 
			
		||||
                <FormattedMessage
 | 
			
		||||
                  id='bundle_modal_error.close'
 | 
			
		||||
                  defaultMessage='Close'
 | 
			
		||||
                />
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +26,6 @@ import BundleContainer from '../containers/bundle_container';
 | 
			
		|||
import ActionsModal from './actions_modal';
 | 
			
		||||
import AudioModal from './audio_modal';
 | 
			
		||||
import { BoostModal } from './boost_modal';
 | 
			
		||||
import BundleModalError from './bundle_modal_error';
 | 
			
		||||
import {
 | 
			
		||||
  ConfirmationModal,
 | 
			
		||||
  ConfirmDeleteStatusModal,
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +39,7 @@ import {
 | 
			
		|||
import FocalPointModal from './focal_point_modal';
 | 
			
		||||
import ImageModal from './image_modal';
 | 
			
		||||
import MediaModal from './media_modal';
 | 
			
		||||
import ModalLoading from './modal_loading';
 | 
			
		||||
import { ModalPlaceholder } from './modal_placeholder';
 | 
			
		||||
import VideoModal from './video_modal';
 | 
			
		||||
 | 
			
		||||
export const MODAL_COMPONENTS = {
 | 
			
		||||
| 
						 | 
				
			
			@ -105,14 +104,16 @@ export default class ModalRoot extends PureComponent {
 | 
			
		|||
    this.setState({ backgroundColor: color });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  renderLoading = modalId => () => {
 | 
			
		||||
    return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
 | 
			
		||||
  renderLoading = () => {
 | 
			
		||||
    const { onClose } = this.props;
 | 
			
		||||
 | 
			
		||||
    return <ModalPlaceholder loading onClose={onClose} />;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  renderError = (props) => {
 | 
			
		||||
    const { onClose } = this.props;
 | 
			
		||||
 | 
			
		||||
    return <BundleModalError {...props} onClose={onClose} />;
 | 
			
		||||
    return <ModalPlaceholder {...props} onClose={onClose} />;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleClose = (ignoreFocus = false) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +135,7 @@ export default class ModalRoot extends PureComponent {
 | 
			
		|||
      <Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
 | 
			
		||||
        {visible && (
 | 
			
		||||
          <>
 | 
			
		||||
            <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
 | 
			
		||||
            <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
 | 
			
		||||
              {(SpecificComponent) => {
 | 
			
		||||
                const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
 | 
			
		||||
                return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -129,7 +129,7 @@
 | 
			
		|||
  "bundle_column_error.routing.body": "The requested page could not be found. Are you sure the URL in the address bar is correct?",
 | 
			
		||||
  "bundle_column_error.routing.title": "404",
 | 
			
		||||
  "bundle_modal_error.close": "Close",
 | 
			
		||||
  "bundle_modal_error.message": "Something went wrong while loading this component.",
 | 
			
		||||
  "bundle_modal_error.message": "Something went wrong while loading this screen.",
 | 
			
		||||
  "bundle_modal_error.retry": "Try again",
 | 
			
		||||
  "closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.",
 | 
			
		||||
  "closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5849,119 +5849,44 @@ a.status-card {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.onboarding-modal,
 | 
			
		||||
.error-modal,
 | 
			
		||||
.embed-modal {
 | 
			
		||||
  background: $ui-secondary-color;
 | 
			
		||||
  color: $inverted-text-color;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
.modal-placeholder {
 | 
			
		||||
  width: 588px;
 | 
			
		||||
  min-height: 478px;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
  background: var(--modal-background-color);
 | 
			
		||||
  backdrop-filter: var(--background-filter);
 | 
			
		||||
  border: 1px solid var(--modal-border-color);
 | 
			
		||||
  border-radius: 16px;
 | 
			
		||||
 | 
			
		||||
.error-modal__body {
 | 
			
		||||
  height: 80vh;
 | 
			
		||||
  width: 80vw;
 | 
			
		||||
  max-width: 520px;
 | 
			
		||||
  max-height: 420px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  & > div {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    inset-inline-start: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    padding: 25px;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  &__error {
 | 
			
		||||
    padding: 24px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    user-select: text;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
 | 
			
		||||
.error-modal__body {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.onboarding-modal__paginator,
 | 
			
		||||
.error-modal__footer {
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  background: darken($ui-secondary-color, 8%);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  padding: 25px;
 | 
			
		||||
 | 
			
		||||
  & > div {
 | 
			
		||||
    min-width: 33px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .onboarding-modal__nav,
 | 
			
		||||
  .error-modal__nav {
 | 
			
		||||
    color: $lighter-text-color;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    padding: 10px 25px;
 | 
			
		||||
    line-height: inherit;
 | 
			
		||||
    height: auto;
 | 
			
		||||
    margin: -10px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
 | 
			
		||||
    &:hover,
 | 
			
		||||
    &:focus,
 | 
			
		||||
    &:active {
 | 
			
		||||
      color: darken($lighter-text-color, 4%);
 | 
			
		||||
      background-color: darken($ui-secondary-color, 16%);
 | 
			
		||||
    &__image {
 | 
			
		||||
      width: 70%;
 | 
			
		||||
      max-width: 350px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.onboarding-modal__done,
 | 
			
		||||
    &.onboarding-modal__next {
 | 
			
		||||
      color: $inverted-text-color;
 | 
			
		||||
    &__message {
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      text-wrap: balance;
 | 
			
		||||
      font-size: 14px;
 | 
			
		||||
      line-height: 20px;
 | 
			
		||||
      letter-spacing: 0.25px;
 | 
			
		||||
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:focus,
 | 
			
		||||
      &:active {
 | 
			
		||||
        color: lighten($inverted-text-color, 4%);
 | 
			
		||||
      &__actions {
 | 
			
		||||
        margin-top: 24px;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        gap: 10px;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.error-modal__footer {
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.display-case {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
  margin-bottom: 15px;
 | 
			
		||||
 | 
			
		||||
  &__label {
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    color: $inverted-text-color;
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__case {
 | 
			
		||||
    background: $ui-base-color;
 | 
			
		||||
    color: $secondary-text-color;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.safety-action-modal {
 | 
			
		||||
  width: 600px;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue