This commit is contained in:
Eugen Rochko 2020-06-16 00:41:27 +02:00
parent b1484cf3ce
commit 4d996426ac
19 changed files with 548 additions and 133 deletions

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Tooltip from 'mastodon/components/tooltip';
export default class Button extends React.PureComponent { export default class Button extends React.PureComponent {
@ -49,16 +50,17 @@ export default class Button extends React.PureComponent {
}); });
return ( return (
<button <Tooltip placement='bottom' overlay={this.props.title}>
className={className} <button
disabled={this.props.disabled} className={className}
onClick={this.handleClick} disabled={this.props.disabled}
ref={this.setRef} onClick={this.handleClick}
style={style} ref={this.setRef}
title={this.props.title} style={style}
> >
{this.props.text || this.props.children} {this.props.text || this.props.children}
</button> </button>
</Tooltip>
); );
} }

View file

@ -1,20 +1,24 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Tooltip from 'mastodon/components/tooltip';
export default class Icon extends React.PureComponent { export default class Icon extends React.PureComponent {
static propTypes = { static propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
className: PropTypes.string, className: PropTypes.string,
title: PropTypes.node,
fixedWidth: PropTypes.bool, fixedWidth: PropTypes.bool,
}; };
render () { render () {
const { id, className, fixedWidth, ...other } = this.props; const { id, className, fixedWidth, title, ...other } = this.props;
return ( return (
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} /> <Tooltip placement='top' overlay={title}>
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
</Tooltip>
); );
} }

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import Tooltip from 'mastodon/components/tooltip';
export default class IconButton extends React.PureComponent { export default class IconButton extends React.PureComponent {
@ -114,22 +115,23 @@ export default class IconButton extends React.PureComponent {
}); });
return ( return (
<button <Tooltip placement='bottom' overlay={title}>
aria-label={title} <button
aria-pressed={pressed} aria-label={title}
aria-expanded={expanded} aria-pressed={pressed}
title={title} aria-expanded={expanded}
className={classes} className={classes}
onClick={this.handleClick} onClick={this.handleClick}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress} onKeyPress={this.handleKeyPress}
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}
disabled={disabled} disabled={disabled}
> >
<Icon id={icon} fixedWidth aria-hidden='true' /> <Icon id={icon} fixedWidth aria-hidden='true' />
</button> </button>
</Tooltip>
); );
} }

View file

@ -8,6 +8,7 @@ import { isIOS } from '../is_mobile';
import classNames from 'classnames'; import classNames from 'classnames';
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state'; import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
import { decode } from 'blurhash'; import { decode } from 'blurhash';
import Tooltip from 'mastodon/components/tooltip';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', toggle_visible: { id: 'media_gallery.toggle_visible',
@ -165,9 +166,11 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> <Tooltip placement='bottom' overlay={attachment.get('description')}>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} aria-label={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
</a> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a>
</Tooltip>
</div> </div>
); );
} else if (attachment.get('type') === 'image') { } else if (attachment.get('type') === 'image') {
@ -188,42 +191,44 @@ class Item extends React.PureComponent {
const y = ((focusY / -2) + .5) * 100; const y = ((focusY / -2) + .5) * 100;
thumbnail = ( thumbnail = (
<a <Tooltip placement='bottom' overlay={attachment.get('description')}>
className='media-gallery__item-thumbnail' <a
href={attachment.get('remote_url') || originalUrl} className='media-gallery__item-thumbnail'
onClick={this.handleClick} href={attachment.get('remote_url') || originalUrl}
target='_blank' onClick={this.handleClick}
rel='noopener noreferrer' target='_blank'
> rel='noopener noreferrer'
<img >
src={previewUrl} <img
srcSet={srcSet} src={previewUrl}
sizes={sizes} srcSet={srcSet}
alt={attachment.get('description')} sizes={sizes}
title={attachment.get('description')} alt={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }} style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
/> />
</a> </a>
</Tooltip>
); );
} else if (attachment.get('type') === 'gifv') { } else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.getAutoPlay(); const autoPlay = !isIOS() && this.getAutoPlay();
thumbnail = ( thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<video <Tooltip placement='bottom' overlay={attachment.get('description')}>
className='media-gallery__item-gifv-thumbnail' <video
aria-label={attachment.get('description')} className='media-gallery__item-gifv-thumbnail'
title={attachment.get('description')} aria-label={attachment.get('description')}
role='application' role='application'
src={attachment.get('url')} src={attachment.get('url')}
onClick={this.handleClick} onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay} autoPlay={autoPlay}
loop loop
muted muted
/> />
</Tooltip>
<span className='media-gallery__gifv__label'>GIF</span> <span className='media-gallery__gifv__label'>GIF</span>
</div> </div>

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Tooltip from 'mastodon/components/tooltip';
export default class Permalink extends React.PureComponent { export default class Permalink extends React.PureComponent {
@ -28,12 +29,14 @@ export default class Permalink extends React.PureComponent {
} }
render () { render () {
const { href, children, className, onInterceptClick, ...other } = this.props; const { href, children, className, onInterceptClick, title, ...other } = this.props;
return ( return (
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> <Tooltip placement='top' overlay={title}>
{children} <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
</a> {children}
</a>
</Tooltip>
); );
} }

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Tooltip from 'mastodon/components/tooltip';
const messages = defineMessages({ const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' }, today: { id: 'relative_time.today', defaultMessage: 'today' },
@ -181,11 +182,14 @@ class RelativeTimestamp extends React.Component {
const timeGiven = timestamp.includes('T'); const timeGiven = timestamp.includes('T');
const date = new Date(timestamp); const date = new Date(timestamp);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven); const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
const formatted = intl.formatDate(date, dateFormatOptions);
return ( return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> <Tooltip placement='top' overlay={formatted}>
{relativeTime} <time dateTime={timestamp} aria-label={formatted}>
</time> {relativeTime}
</time>
</Tooltip>
); );
} }

View file

@ -16,6 +16,7 @@ import { MediaGallery, Video, Audio } from '../features/ui/util/async-components
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import Tooltip from 'mastodon/components/tooltip';
import { displayMedia } from '../initial_state'; import { displayMedia } from '../initial_state';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
@ -424,13 +425,15 @@ class Status extends ImmutablePureComponent {
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> <Tooltip placement='top' overlay={status.getIn(['account', 'acct'])}>
<div className='status__avatar'> <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
{statusAvatar} <div className='status__avatar'>
</div> {statusAvatar}
</div>
<DisplayName account={status.get('account')} others={otherAccounts} /> <DisplayName account={status.get('account')} others={otherAccounts} />
</a> </a>
</Tooltip>
</div> </div>
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} /> <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />

View file

@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import Tooltip from 'rc-tooltip';
const OptionalTooltip = ({ overlay, children, ...other }) => {
if (overlay) {
return (
<Tooltip animation='zoom' mouseEnterDelay={0.2} destroyTooltipOnHide {...other} overlay={overlay}>
{children}
</Tooltip>
);
} else {
return children;
}
};
OptionalTooltip.propTypes = {
children: PropTypes.node,
overlay: PropTypes.node,
};
export default OptionalTooltip;

View file

@ -11,6 +11,7 @@ import Avatar from 'mastodon/components/avatar';
import { shortNumberFormat } from 'mastodon/utils/numbers'; import { shortNumberFormat } from 'mastodon/utils/numbers';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import Tooltip from 'mastodon/components/tooltip';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -293,19 +294,34 @@ class Header extends ImmutablePureComponent {
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
<dd className='verified'> <dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> <Tooltip placement='top' overlay={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' /> <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'>
</span></a> <Icon id='check' className='verified__mark' />
</a>
</Tooltip>
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
</dd> </dd>
</dl> </dl>
))} ))}
{fields.map((pair, i) => ( {fields.map((pair, i) => (
<dl key={i}> <dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> <dt>
<Tooltip placement='top' overlay={pair.get('name')}>
<span dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} />
</Tooltip>
</dt>
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> <dd className={pair.get('verified_at') && 'verified'}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> {pair.get('verified_at') && (
<Tooltip placement='top' overlay={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' />
</Tooltip>
)}
{' '}
<Tooltip placement='top' overlay={pair.get('value_plain')}>
<span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</Tooltip>
</dd> </dd>
</dl> </dl>
))} ))}
@ -316,17 +332,23 @@ class Header extends ImmutablePureComponent {
</div> </div>
<div className='account__header__extra__links'> <div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('statuses_count'))}>
<strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' /> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`}>
</NavLink> <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' />
</NavLink>
</Tooltip>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('following_count'))}>
<strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' /> <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`}>
</NavLink> <strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' />
</NavLink>
</Tooltip>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('followers_count'))}>
<strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' /> <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`}>
</NavLink> <strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' />
</NavLink>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>

View file

@ -3,6 +3,7 @@ import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import DisplayName from '../../../components/display_name';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Tooltip from 'mastodon/components/tooltip';
export default class AutosuggestAccount extends ImmutablePureComponent { export default class AutosuggestAccount extends ImmutablePureComponent {
@ -14,10 +15,12 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
const { account } = this.props; const { account } = this.props;
return ( return (
<div className='autosuggest-account' title={account.get('acct')}> <Tooltip placement='top' overlay={account.get('acct')}>
<div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div> <div className='autosuggest-account'>
<DisplayName account={account} /> <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
</div> <DisplayName account={account} />
</div>
</Tooltip>
); );
} }

View file

@ -7,6 +7,7 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import detectPassiveEvents from 'detect-passive-events'; import detectPassiveEvents from 'detect-passive-events';
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
import Tooltip from 'mastodon/components/tooltip';
const messages = defineMessages({ const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@ -358,13 +359,15 @@ class EmojiPickerDropdown extends React.PureComponent {
return ( return (
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> <Tooltip placement='top' overlay={title}>
{button || <img <div ref={this.setTargetRef} className='emoji-button' aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
className={classNames('emojione', { 'pulse-loading': active && loading })} {button || <img
alt='🙂' className={classNames('emojione', { 'pulse-loading': active && loading })}
src={`${assetHost}/emoji/1f602.svg`} alt='🙂'
/>} src={`${assetHost}/emoji/1f602.svg`}
</div> />}
</div>
</Tooltip>
<Overlay show={active} placement={placement} target={this.findTarget}> <Overlay show={active} placement={placement} target={this.findTarget}>
<EmojiPickerMenu <EmojiPickerMenu

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Tooltip from 'mastodon/components/tooltip';
const iconStyle = { const iconStyle = {
height: null, height: null,
@ -26,16 +27,17 @@ export default class TextIconButton extends React.PureComponent {
const { label, title, active, ariaControls } = this.props; const { label, title, active, ariaControls } = this.props;
return ( return (
<button <Tooltip placement='bottom' overlay={title}>
title={title} <button
aria-label={title} aria-label={title}
className={`text-icon-button ${active ? 'active' : ''}`} className={`text-icon-button ${active ? 'active' : ''}`}
aria-expanded={active} aria-expanded={active}
onClick={this.handleClick} onClick={this.handleClick}
aria-controls={ariaControls} style={iconStyle} aria-controls={ariaControls} style={iconStyle}
> >
{label} {label}
</button> </button>
</Tooltip>
); );
} }

View file

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { changeComposeSensitivity } from 'mastodon/actions/compose'; import { changeComposeSensitivity } from 'mastodon/actions/compose';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import Tooltip from 'mastodon/components/tooltip';
const messages = defineMessages({ const messages = defineMessages({
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
@ -37,19 +38,21 @@ class SensitiveButton extends React.PureComponent {
return ( return (
<div className='compose-form__sensitive-button'> <div className='compose-form__sensitive-button'>
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> <Tooltip placement='top' overlay={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
<input <label className={classNames('icon-button', { active })} aria-label={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
name='mark-sensitive' <input
type='checkbox' name='mark-sensitive'
checked={active} type='checkbox'
onChange={onClick} checked={active}
disabled={disabled} onChange={onClick}
/> disabled={disabled}
/>
<span className={classNames('checkbox', { active })} /> <span className={classNames('checkbox', { active })} />
<FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
</label> </label>
</Tooltip>
</div> </div>
); );
} }

View file

@ -16,6 +16,7 @@ import { openModal } from 'mastodon/actions/modal';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { mascot } from '../../initial_state'; import { mascot } from '../../initial_state';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import Tooltip from 'mastodon/components/tooltip';
import { logOut } from 'mastodon/utils/log_out'; import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({ const messages = defineMessages({
@ -97,21 +98,55 @@ class Compose extends React.PureComponent {
const { columns } = this.props; const { columns } = this.props;
header = ( header = (
<nav className='drawer__header'> <nav className='drawer__header'>
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link> <Tooltip placement='bottom' overlay={intl.formatMessage(messages.start)}>
<Link to='/getting-started' className='drawer__tab' aria-label={intl.formatMessage(messages.start)}>
<Icon id='bars' fixedWidth />
</Link>
</Tooltip>
{!columns.some(column => column.get('id') === 'HOME') && ( {!columns.some(column => column.get('id') === 'HOME') && (
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link> <Tooltip placement='bottom' overlay={intl.formatMessage(messages.home_timeline)}>
<Link to='/timelines/home' className='drawer__tab' aria-label={intl.formatMessage(messages.home_timeline)}>
<Icon id='home' fixedWidth />
</Link>
</Tooltip>
)} )}
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link> <Tooltip placement='bottom' overlay={intl.formatMessage(messages.notifications)}>
<Link to='/notifications' className='drawer__tab' aria-label={intl.formatMessage(messages.notifications)}>
<Icon id='bell' fixedWidth />
</Link>
</Tooltip>
)} )}
{!columns.some(column => column.get('id') === 'COMMUNITY') && ( {!columns.some(column => column.get('id') === 'COMMUNITY') && (
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link> <Tooltip placement='bottom' overlay={intl.formatMessage(messages.community)}>
<Link to='/timelines/public/local' className='drawer__tab' aria-label={intl.formatMessage(messages.community)}>
<Icon id='users' fixedWidth />
</Link>
</Tooltip>
)} )}
{!columns.some(column => column.get('id') === 'PUBLIC') && ( {!columns.some(column => column.get('id') === 'PUBLIC') && (
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> <Tooltip placement='bottom' overlay={intl.formatMessage(messages.public)}>
<Link to='/timelines/public' className='drawer__tab' aria-label={intl.formatMessage(messages.public)}>
<Icon id='globe' fixedWidth />
</Link>
</Tooltip>
)} )}
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
<a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a> <Tooltip placement='bottom' overlay={intl.formatMessage(messages.preferences)}>
<a href='/settings/preferences' className='drawer__tab' aria-label={intl.formatMessage(messages.preferences)}>
<Icon id='cog' fixedWidth />
</a>
</Tooltip>
<Tooltip placement='bottom' overlay={intl.formatMessage(messages.logout)}>
<a href='/auth/sign_out' className='drawer__tab' aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}>
<Icon id='sign-out' fixedWidth />
</a>
</Tooltip>
</nav> </nav>
); );
} }

View file

@ -24,5 +24,6 @@
@import 'mastodon/tables'; @import 'mastodon/tables';
@import 'mastodon/admin'; @import 'mastodon/admin';
@import 'mastodon/dashboard'; @import 'mastodon/dashboard';
@import 'mastodon/tooltips';
@import 'mastodon/rtl'; @import 'mastodon/rtl';
@import 'mastodon/accessibility'; @import 'mastodon/accessibility';

View file

@ -1076,8 +1076,7 @@
} }
.status__info .status__display-name { .status__info .status__display-name {
display: block; display: inline-flex;
max-width: 100%;
padding-right: 25px; padding-right: 25px;
} }

View file

@ -0,0 +1,233 @@
//
// Tooltips
// --------------------------------------------------
$font-size-base: 14px;
$line-height-base: 1.5;
$border-radius-base: 5px;
$overlay-shadow: 0 0 4px rgba(0, 0, 0, 0.17);
//** Tooltip text color
$tooltip-color: #fff;
//** Tooltip background color
$tooltip-bg: #000;
$tooltip-opacity: 0.9;
//** Tooltip arrow width
$tooltip-arrow-width: 5px;
//** Tooltip distance with trigger
$tooltip-distance: $tooltip-arrow-width + 4;
//** Tooltip arrow color
$tooltip-arrow-color: $tooltip-bg;
// Base class
.rc-tooltip {
position: absolute;
z-index: 1070;
display: block;
visibility: visible;
// remove left/top by yiminghe
// left: -9999px;
// top: -9999px;
font-size: $font-size-base;
line-height: $line-height-base;
font-weight: 500;
opacity: $tooltip-opacity;
will-change: opacity, transform;
pointer-events: none;
&-hidden {
display: none;
}
&-placement-top, &-placement-topLeft, &-placement-topRight {
padding: $tooltip-arrow-width 0 $tooltip-distance 0;
}
&-placement-right, &-placement-rightTop, &-placement-rightBottom {
padding: 0 $tooltip-arrow-width 0 $tooltip-distance;
}
&-placement-bottom, &-placement-bottomLeft, &-placement-bottomRight {
padding: $tooltip-distance 0 $tooltip-arrow-width 0;
}
&-placement-left, &-placement-leftTop, &-placement-leftBottom {
padding: 0 $tooltip-distance 0 $tooltip-arrow-width;
}
}
// Wrapper for the tooltip content
.rc-tooltip-inner {
padding: 8px 12px;
color: $tooltip-color;
text-align: left;
text-decoration: none;
background-color: $tooltip-bg;
border-radius: $border-radius-base;
box-shadow: $overlay-shadow;
max-width: 290px;
box-sizing: border-box;
min-height: 34px;
}
// Arrows
.rc-tooltip-arrow {
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.rc-tooltip {
&-placement-top &-arrow,
&-placement-topLeft &-arrow,
&-placement-topRight &-arrow {
bottom: $tooltip-distance - $tooltip-arrow-width;
margin-left: -$tooltip-arrow-width;
border-width: $tooltip-arrow-width $tooltip-arrow-width 0;
border-top-color: $tooltip-arrow-color;
}
&-placement-top &-arrow {
left: 50%;
}
&-placement-topLeft &-arrow {
left: 15%;
}
&-placement-topRight &-arrow {
right: 15%;
}
&-placement-right &-arrow,
&-placement-rightTop &-arrow,
&-placement-rightBottom &-arrow {
left: $tooltip-distance - $tooltip-arrow-width;
margin-top: -$tooltip-arrow-width;
border-width: $tooltip-arrow-width $tooltip-arrow-width $tooltip-arrow-width 0;
border-right-color: $tooltip-arrow-color;
}
&-placement-right &-arrow {
top: 50%;
}
&-placement-rightTop &-arrow {
top: 15%;
margin-top: 0;
}
&-placement-rightBottom &-arrow {
bottom: 15%;
}
&-placement-left &-arrow,
&-placement-leftTop &-arrow,
&-placement-leftBottom &-arrow {
right: $tooltip-distance - $tooltip-arrow-width;
margin-top: -$tooltip-arrow-width;
border-width: $tooltip-arrow-width 0 $tooltip-arrow-width $tooltip-arrow-width;
border-left-color: $tooltip-arrow-color;
}
&-placement-left &-arrow {
top: 50%;
}
&-placement-leftTop &-arrow {
top: 15%;
margin-top: 0;
}
&-placement-leftBottom &-arrow {
bottom: 15%;
}
&-placement-bottom &-arrow,
&-placement-bottomLeft &-arrow,
&-placement-bottomRight &-arrow {
top: $tooltip-distance - $tooltip-arrow-width;
margin-left: -$tooltip-arrow-width;
border-width: 0 $tooltip-arrow-width $tooltip-arrow-width;
border-bottom-color: $tooltip-arrow-color;
}
&-placement-bottom &-arrow {
left: 50%;
}
&-placement-bottomLeft &-arrow {
left: 15%;
}
&-placement-bottomRight &-arrow {
right: 15%;
}
@mixin effect(){
animation-duration: 50ms;
animation-fill-mode: both;
}
& {
&-zoom-enter,
&-zoom-leave {
display: block;
}
}
&-zoom-enter,
&-zoom-appear {
opacity: 0;
@include effect();
animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28);
animation-play-state: paused;
}
&-zoom-leave {
@include effect();
animation-timing-function: cubic-bezier(0.6, -0.3, 0.74, 0.05);
animation-play-state: paused;
}
&-zoom-enter,
&-zoom-appear {
&-active {
animation-name: rcToolTipZoomIn;
animation-play-state: running;
}
}
&-zoom-leave {
&-active {
animation-name: rcToolTipZoomOut;
animation-play-state: running;
}
}
@keyframes rcToolTipZoomIn {
0% {
opacity: 0;
transform-origin: 50% 50%;
transform: scale(0, 0);
}
100% {
opacity: 1;
transform-origin: 50% 50%;
transform: scale(1, 1);
}
}
@keyframes rcToolTipZoomOut {
0% {
opacity: 1;
transform-origin: 50% 50%;
transform: scale(1, 1);
}
100% {
opacity: 0;
transform-origin: 50% 50%;
transform: scale(0, 0);
}
}
}

View file

@ -124,6 +124,7 @@
"promise.prototype.finally": "^3.1.2", "promise.prototype.finally": "^3.1.2",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"punycode": "^2.1.0", "punycode": "^2.1.0",
"rc-tooltip": "^4.2.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-hotkeys": "^1.1.4", "react-hotkeys": "^1.1.4",

View file

@ -2,6 +2,11 @@
# yarn lockfile v1 # yarn lockfile v1
"@ant-design/css-animation@^1.7.2":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.2.tgz#4ee5d2ec0fb7cc0a78b44e1c82628bd4621ac7e3"
integrity sha512-bvVOe7A+r7lws58B7r+fgnQDK90cV45AXuvGx6i5CCSX1W/M3AJnHsNggDANBxEtWdNdFWcDd5LorB+RdSIlBw==
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1": "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1":
version "7.10.1" version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff"
@ -988,7 +993,7 @@
dependencies: dependencies:
regenerator-runtime "^0.12.0" regenerator-runtime "^0.12.0"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.10.2" version "7.10.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
@ -2840,7 +2845,7 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@^2.2.5: classnames@2.x, classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -3744,6 +3749,11 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-align@^1.7.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.0.tgz#56fb7156df0b91099830364d2d48f88963f5a29c"
integrity sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA==
dom-helpers@^3.2.1, dom-helpers@^3.4.0: dom-helpers@^3.2.1, dom-helpers@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
@ -8769,7 +8779,7 @@ quote@^0.4.0:
resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01" resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01"
integrity sha1-EIOSF/bBNiuJGUBE0psjP9fzLwE= integrity sha1-EIOSF/bBNiuJGUBE0psjP9fzLwE=
raf@^3.1.0, raf@^3.4.1: raf@^3.1.0, raf@^3.4.0, raf@^3.4.1:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
@ -8819,6 +8829,54 @@ raw-body@2.4.0:
iconv-lite "0.4.24" iconv-lite "0.4.24"
unpipe "1.0.0" unpipe "1.0.0"
rc-align@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.0.tgz#7a5b212051bdd840b406a6ad547076534a843691"
integrity sha512-0mKKfiZGo7VNiRCmnI4MTOG72pBFF0H08zebqcJyXcAm2hgAqTUtvt4I0pjMHh1WdYg+iQDjowpB5X8mZTN2vw==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "2.x"
dom-align "^1.7.0"
rc-util "^5.0.1"
resize-observer-polyfill "^1.5.1"
rc-animate@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.1.0.tgz#051b689c2c7194e4c8ae016d32a0e5f9de6c8baa"
integrity sha512-8FsM+3B1H+0AyTyGggY6JyVldHTs1CyYT8CfTmG/nGHHXlecvSLeICJhcKgRLjUiQlctNnRtB1rwz79cvBVmrw==
dependencies:
"@ant-design/css-animation" "^1.7.2"
classnames "^2.2.6"
raf "^3.4.0"
rc-util "^5.0.1"
rc-tooltip@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-4.2.1.tgz#c1a2d5017ee03a771a9301c0dfdb46dfdf8fef94"
integrity sha512-oykuaGsHg7RFvPUaxUpxo7ScEqtH61C66x4JUmjlFlSS8gSx2L8JFtfwM1D68SLBxUqGqJObtxj4TED75gQTiA==
dependencies:
rc-trigger "^4.2.1"
rc-trigger@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.3.0.tgz#94ea1851d123359716d1dc3030083c015a92ecfb"
integrity sha512-jnGNzosXmDdivMBjPCYe/AfOXTpJU2/xQ9XukgoXDQEoZq/9lcI1r7eUIfq70WlWpLxlUEqQktiV3hwyy6Nw9g==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.6"
raf "^3.4.1"
rc-align "^4.0.0"
rc-animate "^3.0.0"
rc-util "^5.0.1"
rc-util@^5.0.1:
version "5.0.4"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.0.4.tgz#297bd719b1bd00b3c947a884ab7ef0a07c55dce6"
integrity sha512-cd19RCrE0DJH6UcJ9+V3eaXA/5sNWyVKOKkWl8ZM2OqgNzVb8fv0obf/TkuvSN43tmTsgqY8k7OqpFYHhmef8g==
dependencies:
react-is "^16.12.0"
shallowequal "^1.1.0"
react-dom@^16.13.1: react-dom@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
@ -9425,6 +9483,11 @@ reselect@^4.0.0:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@ -9872,6 +9935,11 @@ shallow-equal@^1.2.1:
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
shallowequal@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
shebang-command@^1.2.0: shebang-command@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"