Change design of modal loading and error screens in web UI (#33092)

This commit is contained in:
Eugen Rochko 2024-11-29 08:50:08 +01:00 committed by GitHub
parent eef8d2c855
commit 7f2cfcccab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 118 additions and 234 deletions

View 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}
/>
);
};

View file

@ -9,58 +9,7 @@ import { Link } from 'react-router-dom';
import { Button } from 'mastodon/components/button'; import { Button } from 'mastodon/components/button';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import { autoPlayGif } from 'mastodon/initial_state'; import { GIF } from 'mastodon/components/gif';
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}
/>
);
}
}
class CopyButton extends PureComponent { class CopyButton extends PureComponent {

View file

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

View file

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

View file

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

View file

@ -26,7 +26,6 @@ import BundleContainer from '../containers/bundle_container';
import ActionsModal from './actions_modal'; import ActionsModal from './actions_modal';
import AudioModal from './audio_modal'; import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal'; import { BoostModal } from './boost_modal';
import BundleModalError from './bundle_modal_error';
import { import {
ConfirmationModal, ConfirmationModal,
ConfirmDeleteStatusModal, ConfirmDeleteStatusModal,
@ -40,7 +39,7 @@ import {
import FocalPointModal from './focal_point_modal'; import FocalPointModal from './focal_point_modal';
import ImageModal from './image_modal'; import ImageModal from './image_modal';
import MediaModal from './media_modal'; import MediaModal from './media_modal';
import ModalLoading from './modal_loading'; import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal'; import VideoModal from './video_modal';
export const MODAL_COMPONENTS = { export const MODAL_COMPONENTS = {
@ -105,14 +104,16 @@ export default class ModalRoot extends PureComponent {
this.setState({ backgroundColor: color }); this.setState({ backgroundColor: color });
}; };
renderLoading = modalId => () => { renderLoading = () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; const { onClose } = this.props;
return <ModalPlaceholder loading onClose={onClose} />;
}; };
renderError = (props) => { renderError = (props) => {
const { onClose } = this.props; const { onClose } = this.props;
return <BundleModalError {...props} onClose={onClose} />; return <ModalPlaceholder {...props} onClose={onClose} />;
}; };
handleClose = (ignoreFocus = false) => { handleClose = (ignoreFocus = false) => {
@ -134,7 +135,7 @@ export default class ModalRoot extends PureComponent {
<Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}> <Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
{visible && ( {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) => { {(SpecificComponent) => {
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined; const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />; return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;

View file

@ -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.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_column_error.routing.title": "404",
"bundle_modal_error.close": "Close", "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", "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.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.", "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.",

View file

@ -5849,116 +5849,41 @@ a.status-card {
} }
} }
.onboarding-modal, .modal-placeholder {
.error-modal, width: 588px;
.embed-modal { min-height: 478px;
background: $ui-secondary-color; flex-direction: column;
color: $inverted-text-color; background: var(--modal-background-color);
border-radius: 8px; backdrop-filter: var(--background-filter);
overflow: hidden; border: 1px solid var(--modal-border-color);
border-radius: 16px;
&__error {
padding: 24px;
display: flex; display: flex;
flex-direction: column;
}
.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; align-items: center;
justify-content: center; flex-direction: column;
display: flex;
opacity: 0; &__image {
user-select: text; width: 70%;
max-width: 350px;
} }
}
.error-modal__body { &__message {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center; text-align: center;
} text-wrap: balance;
.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-size: 14px;
font-weight: 500; line-height: 20px;
padding: 10px 25px; letter-spacing: 0.25px;
line-height: inherit;
height: auto;
margin: -10px;
border-radius: 4px;
background-color: transparent;
&:hover, &__actions {
&:focus, margin-top: 24px;
&:active { display: flex;
color: darken($lighter-text-color, 4%); gap: 10px;
background-color: darken($ui-secondary-color, 16%); align-items: center;
}
&.onboarding-modal__done,
&.onboarding-modal__next {
color: $inverted-text-color;
&:hover,
&:focus,
&:active {
color: lighten($inverted-text-color, 4%);
}
}
}
}
.error-modal__footer {
justify-content: center; 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;
} }
} }