Merge commit '134de736dcbc6aa613fd5aec21d983d92d8b0be8' into glitch-soc/merge-upstream

Conflicts:
- `app/javascript/mastodon/features/compose/components/poll_form.jsx`:
  Upstream changed how icons are handled, including on a line modified by
  glitch-soc to bump the number of poll options.
  Applied upstream's change, while keeping the increased number of poll
  options.
th-new
Claire 2023-10-25 12:14:24 +02:00
commit ba527c071f
124 changed files with 928 additions and 726 deletions

View File

@ -28,7 +28,7 @@ RUN apt-get update && \
libgdbm-dev \
libgmp-dev \
libssl-dev \
libyaml-0-2 \
libyaml-dev \
ca-certificates \
libreadline8 \
python3 \

View File

@ -0,0 +1,3 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default 'SvgrURL';
export const ReactComponent = 'div';

View File

@ -18,7 +18,6 @@ import { Avatar } from './avatar';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { IconButton } from './icon_button';
import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({
@ -45,10 +44,7 @@ class Account extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
defaultAction: PropTypes.string,
onActionClick: PropTypes.func,
withBio: PropTypes.bool,
};
@ -76,12 +72,8 @@ class Account extends ImmutablePureComponent {
this.props.onMuteNotifications(this.props.account, false);
};
handleAction = () => {
this.props.onActionClick(this.props.account);
};
render () {
const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
@ -98,9 +90,7 @@ class Account extends ImmutablePureComponent {
let buttons;
if (actionIcon && onActionClick) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
} else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) {
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);

View File

@ -7,6 +7,8 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ReactComponent as LinkIcon } from '@material-symbols/svg-600/outlined/link.svg';
import { Icon } from 'mastodon/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
@ -25,7 +27,7 @@ export default class AttachmentList extends ImmutablePureComponent {
<div className={classNames('attachment-list', { compact })}>
{!compact && (
<div className='attachment-list__icon'>
<Icon id='link' />
<Icon id='link' icon={LinkIcon} />
</div>
)}
@ -36,7 +38,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>
{compact && <Icon id='link' />}
{compact && <Icon id='link' icon={LinkIcon} />}
{compact && ' ' }
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}
</a>

View File

@ -2,9 +2,9 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { ReactComponent as GroupsIcon } from '@material-design-icons/svg/outlined/group.svg';
import { ReactComponent as PersonIcon } from '@material-design-icons/svg/outlined/person.svg';
import { ReactComponent as SmartToyIcon } from '@material-design-icons/svg/outlined/smart_toy.svg';
import { ReactComponent as GroupsIcon } from '@material-symbols/svg-600/outlined/group.svg';
import { ReactComponent as PersonIcon } from '@material-symbols/svg-600/outlined/person.svg';
import { ReactComponent as SmartToyIcon } from '@material-symbols/svg-600/outlined/smart_toy.svg';
export const Badge = ({ icon, label, domain }) => (

View File

@ -1,13 +0,0 @@
export const Check: React.FC = () => (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
fill='currentColor'
>
<path
fillRule='evenodd'
d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'
clipRule='evenodd'
/>
</svg>
);

View File

@ -6,6 +6,8 @@ import { FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router-dom';
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
import { Icon } from 'mastodon/components/icon';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -34,7 +36,7 @@ class ColumnBackButton extends PureComponent {
const component = (
<button onClick={this.handleClick} className='column-back-button'>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);

View File

@ -1,5 +1,7 @@
import { FormattedMessage } from 'react-intl';
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
import { Icon } from 'mastodon/components/icon';
import ColumnBackButton from './column_back_button';
@ -10,7 +12,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
return (
<div className='column-back-button--slim'>
<div role='button' tabIndex={0} onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</div>

View File

@ -7,6 +7,13 @@ import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import { ReactComponent as AddIcon } from '@material-symbols/svg-600/outlined/add.svg';
import { ReactComponent as ArrowBackIcon } from '@material-symbols/svg-600/outlined/arrow_back.svg';
import { ReactComponent as ChevronLeftIcon } from '@material-symbols/svg-600/outlined/chevron_left.svg';
import { ReactComponent as ChevronRightIcon } from '@material-symbols/svg-600/outlined/chevron_right.svg';
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
import { ReactComponent as TuneIcon } from '@material-symbols/svg-600/outlined/tune.svg';
import { Icon } from 'mastodon/components/icon';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -27,6 +34,7 @@ class ColumnHeader extends PureComponent {
intl: PropTypes.object.isRequired,
title: PropTypes.node,
icon: PropTypes.string,
iconComponent: PropTypes.func,
active: PropTypes.bool,
multiColumn: PropTypes.bool,
extraButton: PropTypes.node,
@ -87,7 +95,7 @@ class ColumnHeader extends PureComponent {
};
render () {
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
@ -118,22 +126,22 @@ class ColumnHeader extends PureComponent {
}
if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
</div>
);
} else if (multiColumn && this.props.onPin) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
}
if (!pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
@ -157,21 +165,21 @@ class ColumnHeader extends PureComponent {
onClick={this.handleToggleClick}
>
<i className='icon-with-badge'>
<Icon id='sliders' />
<Icon id='sliders' icon={TuneIcon} />
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
</i>
</button>
);
}
const hasTitle = icon && title;
const hasTitle = (icon || iconComponent) && title;
const component = (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
{hasTitle && (
<button onClick={this.handleTitleClick}>
<Icon id={icon} fixedWidth className='column-header__icon' />
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
{title}
</button>
)}

View File

@ -3,6 +3,8 @@ import { useCallback, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
import { bannerSettings } from 'mastodon/settings';
import { IconButton } from './icon_button';
@ -36,6 +38,7 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
<div className='dismissable-banner__action'>
<IconButton
icon='times'
iconComponent={CloseIcon}
title={intl.formatMessage(messages.dismiss)}
onClick={handleDismiss}
/>

View File

@ -2,6 +2,8 @@ import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/lock_open.svg';
import { IconButton } from './icon_button';
const messages = defineMessages({
@ -34,6 +36,7 @@ export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
<IconButton
active
icon='unlock'
iconComponent={LockOpenIcon}
title={intl.formatMessage(messages.unblockDomain, { domain })}
onClick={handleDomainUnblock}
/>

View File

@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
@ -163,6 +164,7 @@ class Dropdown extends PureComponent {
static propTypes = {
children: PropTypes.node,
icon: PropTypes.string,
iconComponent: PropTypes.func,
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
loading: PropTypes.bool,
size: PropTypes.number,
@ -255,7 +257,7 @@ class Dropdown extends PureComponent {
};
findTarget = () => {
return this.target;
return this.target?.buttonRef?.current;
};
componentWillUnmount = () => {
@ -271,6 +273,7 @@ class Dropdown extends PureComponent {
render () {
const {
icon,
iconComponent,
items,
size,
title,
@ -291,9 +294,11 @@ class Dropdown extends PureComponent {
onMouseDown: this.handleMouseDown,
onKeyDown: this.handleButtonKeyDown,
onKeyPress: this.handleKeyPress,
ref: this.setTargetRef,
}) : (
<IconButton
icon={!open ? icon : 'close'}
iconComponent={!open ? iconComponent : CloseIcon}
title={title}
active={open}
disabled={disabled}
@ -302,14 +307,14 @@ class Dropdown extends PureComponent {
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
ref={this.setTargetRef}
/>
);
return (
<>
<span ref={this.setTargetRef}>
{button}
</span>
{button}
<Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
{({ props, arrowProps, placement }) => (
<div {...props}>

View File

@ -5,6 +5,8 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { ReactComponent as ArrowDropDownIcon } from '@material-symbols/svg-600/outlined/arrow_drop_down.svg';
import { openModal } from 'mastodon/actions/modal';
import { Icon } from 'mastodon/components/icon';
import InlineAccount from 'mastodon/components/inline_account';
@ -66,7 +68,7 @@ class EditedTimestamp extends PureComponent {
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
</button>
</DropdownMenu>
);

View File

@ -1,20 +1,50 @@
import classNames from 'classnames';
interface Props extends React.HTMLAttributes<HTMLImageElement> {
id: string;
className?: string;
fixedWidth?: boolean;
import { ReactComponent as CheckBoxOutlineBlankIcon } from '@material-symbols/svg-600/outlined/check_box_outline_blank.svg';
interface SVGPropsWithTitle extends React.SVGProps<SVGSVGElement> {
title?: string;
}
export type IconProp = React.FC<SVGPropsWithTitle>;
interface Props extends React.SVGProps<SVGSVGElement> {
children?: never;
id: string;
icon: IconProp;
title?: string;
}
export const Icon: React.FC<Props> = ({
id,
icon: IconComponent,
className,
fixedWidth,
title: titleProp,
...other
}) => (
<i
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
{...other}
/>
);
}) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!IconComponent) {
if (process.env.NODE_ENV !== 'production') {
throw new Error(`<Icon id="${id}"> is missing an "icon" prop.`);
}
IconComponent = CheckBoxOutlineBlankIcon;
}
const ariaHidden = titleProp ? undefined : true;
const role = !ariaHidden ? 'img' : undefined;
// Set the title to an empty string to remove the built-in SVG one if any
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const title = titleProp || '';
return (
<IconComponent
className={classNames('icon', `icon-${id}`, className)}
title={title}
aria-hidden={ariaHidden}
role={role}
{...other}
/>
);
};

View File

@ -1,19 +1,20 @@
import { PureComponent } from 'react';
import { PureComponent, createRef } from 'react';
import classNames from 'classnames';
import { AnimatedNumber } from './animated_number';
import type { IconProp } from './icon';
import { Icon } from './icon';
interface Props {
className?: string;
title: string;
icon: string;
iconComponent: IconProp;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
size: number;
active: boolean;
expanded?: boolean;
style?: React.CSSProperties;
@ -32,8 +33,9 @@ interface States {
deactivate: boolean;
}
export class IconButton extends PureComponent<Props, States> {
buttonRef = createRef<HTMLButtonElement>();
static defaultProps = {
size: 18,
active: false,
disabled: false,
animate: false,
@ -85,10 +87,6 @@ export class IconButton extends PureComponent<Props, States> {
render() {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
@ -99,6 +97,7 @@ export class IconButton extends PureComponent<Props, States> {
disabled,
expanded,
icon,
iconComponent,
inverted,
overlay,
tabIndex,
@ -120,13 +119,9 @@ export class IconButton extends PureComponent<Props, States> {
'icon-button--with-counter': typeof counter !== 'undefined',
});
if (typeof counter !== 'undefined') {
style.width = 'auto';
}
let contents = (
<>
<Icon id={icon} fixedWidth aria-hidden='true' />{' '}
<Icon id={icon} icon={iconComponent} aria-hidden='true' />{' '}
{typeof counter !== 'undefined' && (
<span className='icon-button__counter'>
<AnimatedNumber value={counter} />
@ -158,6 +153,7 @@ export class IconButton extends PureComponent<Props, States> {
style={style}
tabIndex={tabIndex}
disabled={disabled}
ref={this.buttonRef}
>
{contents}
</button>

View File

@ -1,21 +1,24 @@
import type { IconProp } from './icon';
import { Icon } from './icon';
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
interface Props {
id: string;
icon: IconProp;
count: number;
issueBadge: boolean;
className: string;
}
export const IconWithBadge: React.FC<Props> = ({
id,
icon,
count,
issueBadge,
className,
}) => (
<i className='icon-with-badge'>
<Icon id={id} fixedWidth className={className} />
<Icon id={id} icon={icon} className={className} />
{count > 0 && (
<i className='icon-with-badge__badge'>{formatNumber(count)}</i>
)}

View File

@ -2,6 +2,8 @@ import { useCallback } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
import { Icon } from 'mastodon/components/icon';
const messages = defineMessages({
@ -28,7 +30,7 @@ export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
onClick={handleClick}
aria-label={intl.formatMessage(messages.load_more)}
>
<Icon id='ellipsis-h' />
<Icon id='ellipsis-h' icon={MoreHorizIcon} />
</button>
);
};

View File

@ -8,6 +8,7 @@ import classNames from 'classnames';
import { is } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
import { debounce } from 'lodash';
import { Blurhash } from 'mastodon/components/blurhash';
@ -323,7 +324,7 @@ class MediaGallery extends PureComponent {
</button>
);
} else if (visible) {
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} ariaHidden />;
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' iconComponent={VisibilityOffIcon} overlay onClick={this.handleOpen} ariaHidden />;
} else {
spoilerButton = (
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>

View File

@ -5,6 +5,8 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { ReactComponent as CancelPresentationIcon } from '@material-symbols/svg-600/outlined/cancel_presentation.svg';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { Icon } from 'mastodon/components/icon';
@ -25,7 +27,7 @@ class PictureInPicturePlaceholder extends PureComponent {
return (
<div className='picture-in-picture-placeholder' style={{ aspectRatio }} role='button' tabIndex={0} onClick={this.handleClick}>
<Icon id='window-restore' />
<Icon id='window-restore' icon={CancelPresentationIcon} />
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
</div>
);

View File

@ -7,6 +7,7 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
import escapeTextContentForBrowser from 'escape-html';
import spring from 'react-motion/lib/spring';
@ -192,7 +193,7 @@ class Poll extends ImmutablePureComponent {
/>
{!!voted && <span className='poll__voted'>
<Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
<Icon id='check' icon={CheckIcon} className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
</span>}
</label>

View File

@ -7,6 +7,10 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ReactComponent as AlternateEmailIcon } from '@material-symbols/svg-600/outlined/alternate_email.svg';
import { ReactComponent as PushPinIcon } from '@material-symbols/svg-600/outlined/push_pin.svg';
import { ReactComponent as RepeatIcon } from '@material-symbols/svg-600/outlined/repeat.svg';
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
import { HotKeys } from 'react-hotkeys';
import { Icon } from 'mastodon/components/icon';
@ -27,6 +31,7 @@ import { getHashtagBarForStatus } from './hashtag_bar';
import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
import { VisibilityIcon } from './visibility_icon';
const domParser = new DOMParser();
@ -405,7 +410,7 @@ class Status extends ImmutablePureComponent {
if (featured) {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' icon={PushPinIcon} className='status__prepend-icon' /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
</div>
);
@ -414,7 +419,7 @@ class Status extends ImmutablePureComponent {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' icon={RepeatIcon} className='status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
@ -426,7 +431,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('visibility') === 'direct') {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='at' className='status__prepend-icon' fixedWidth /></div>
<div className='status__prepend-icon-wrapper'><Icon id='at' icon={AlternateEmailIcon} className='status__prepend-icon' /></div>
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
</div>
);
@ -435,7 +440,7 @@ class Status extends ImmutablePureComponent {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
<div className='status__prepend-icon-wrapper'><Icon id='reply' icon={ReplyIcon} className='status__prepend-icon' /></div>
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
@ -534,15 +539,6 @@ class Status extends ImmutablePureComponent {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
@ -557,7 +553,7 @@ class Status extends ImmutablePureComponent {
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onClick={this.handleClick} className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>

View File

@ -9,6 +9,16 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { ReactComponent as BookmarkIcon } from '@material-symbols/svg-600/outlined/bookmark-fill.svg';
import { ReactComponent as BookmarkBorderIcon } from '@material-symbols/svg-600/outlined/bookmark.svg';
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
import { ReactComponent as RepeatIcon } from '@material-symbols/svg-600/outlined/repeat.svg';
import { ReactComponent as ReplyIcon } from '@material-symbols/svg-600/outlined/reply.svg';
import { ReactComponent as ReplyAllIcon } from '@material-symbols/svg-600/outlined/reply_all.svg';
import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/star-fill.svg';
import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -336,12 +346,15 @@ class StatusActionBar extends ImmutablePureComponent {
}
let replyIcon;
let replyIconComponent;
let replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyIconComponent = ReplyIcon;
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyIconComponent = ReplyAllIcon;
replyTitle = intl.formatMessage(messages.replyAll);
}
@ -359,29 +372,29 @@ class StatusActionBar extends ImmutablePureComponent {
}
const filterButton = this.props.onFilter && (
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} />
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
);
const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
{filterButton}
<div className='status__action-bar__dropdown'>
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
size={18}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>
<DropdownMenuContainer
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>
);
}

View File

@ -9,6 +9,8 @@ import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { ReactComponent as ChevronRightIcon } from '@material-symbols/svg-600/outlined/chevron_right.svg';
import { Icon } from 'mastodon/components/icon';
import PollContainer from 'mastodon/containers/poll_container';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
@ -257,7 +259,7 @@ class StatusContent extends PureComponent {
const readMoreButton = renderReadMore && (
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' icon={ChevronRightIcon} />
</button>
);

View File

@ -1,3 +1,5 @@
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
import { Icon } from './icon';
const domParser = new DOMParser();
@ -21,7 +23,7 @@ interface Props {
}
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' />
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
<span dangerouslySetInnerHTML={stripRelMe(link)} />
</span>
);

View File

@ -0,0 +1,62 @@
import { defineMessages, useIntl } from 'react-intl';
import { ReactComponent as AlternateEmailIcon } from '@material-symbols/svg-600/outlined/alternate_email.svg';
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
import { ReactComponent as LockOpenIcon } from '@material-symbols/svg-600/outlined/lock_open.svg';
import { ReactComponent as PublicIcon } from '@material-symbols/svg-600/outlined/public.svg';
import { Icon } from './icon';
type Visibility = 'public' | 'unlisted' | 'private' | 'direct';
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: {
id: 'privacy.private.short',
defaultMessage: 'Followers only',
},
direct_short: {
id: 'privacy.direct.short',
defaultMessage: 'Mentioned people only',
},
});
export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
visibility,
}) => {
const intl = useIntl();
const visibilityIconInfo = {
public: {
icon: 'globe',
iconComponent: PublicIcon,
text: intl.formatMessage(messages.public_short),
},
unlisted: {
icon: 'unlock',
iconComponent: LockOpenIcon,
text: intl.formatMessage(messages.unlisted_short),
},
private: {
icon: 'lock',
iconComponent: LockIcon,
text: intl.formatMessage(messages.private_short),
},
direct: {
icon: 'at',
iconComponent: AlternateEmailIcon,
text: intl.formatMessage(messages.direct_short),
},
};
const visibilityIcon = visibilityIconInfo[visibility];
return (
<Icon
id={visibilityIcon.icon}
icon={visibilityIcon.iconComponent}
title={visibilityIcon.text}
/>
);
};

View File

@ -10,6 +10,9 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { ReactComponent as ChevronRightIcon } from '@material-symbols/svg-600/outlined/chevron_right.svg';
import { ReactComponent as ExpandMoreIcon } from '@material-symbols/svg-600/outlined/expand_more.svg';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
@ -73,7 +76,7 @@ class Section extends PureComponent {
return (
<div className={classNames('about__section', { active: !collapsed })}>
<div className='about__section__title' role='button' tabIndex={0} onClick={this.handleClick}>
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title}
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} icon={collapsed ? ChevronRightIcon : ExpandMoreIcon} /> {title}
</div>
{!collapsed && (

View File

@ -3,6 +3,9 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';
import { Icon } from 'mastodon/components/icon';
export default class FollowRequestNote extends ImmutablePureComponent {
@ -22,12 +25,12 @@ export default class FollowRequestNote extends ImmutablePureComponent {
<div className='follow-request-banner__action'>
<button type='button' className='button button-tertiary button--confirmation' onClick={onAuthorize}>
<Icon id='check' fixedWidth />
<Icon id='check' icon={CheckIcon} />
<FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' />
</button>
<button type='button' className='button button-tertiary button--destructive' onClick={onReject}>
<Icon id='times' fixedWidth />
<Icon id='times' icon={CloseIcon} />
<FormattedMessage id='follow_request.reject' defaultMessage='Reject' />
</button>
</div>

View File

@ -9,6 +9,12 @@ import { NavLink, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';
import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/lock.svg';
import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
import { ReactComponent as NotificationsIcon } from '@material-symbols/svg-600/outlined/notifications-fill.svg';
import { ReactComponent as NotificationsActiveIcon } from '@material-symbols/svg-600/outlined/notifications_active.svg';
import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
@ -258,7 +264,7 @@ class Header extends ImmutablePureComponent {
}
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsIcon : NotificationsActiveIcon} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
if (me !== account.get('id')) {
@ -280,7 +286,7 @@ class Header extends ImmutablePureComponent {
}
if (account.get('locked')) {
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
}
if (signedIn && account.get('id') !== me) {
@ -410,7 +416,7 @@ class Header extends ImmutablePureComponent {
</>
)}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
</div>
)}
</div>
@ -448,7 +454,7 @@ class Header extends ImmutablePureComponent {
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className='translate' title={pair.get('value_plain')}>
{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') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}

View File

@ -5,6 +5,10 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ReactComponent as AudiotrackIcon } from '@material-symbols/svg-600/outlined/music_note.svg';
import { ReactComponent as PlayArrowIcon } from '@material-symbols/svg-600/outlined/play_arrow.svg';
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
@ -69,7 +73,7 @@ export default class MediaItem extends ImmutablePureComponent {
if (!visible) {
icon = (
<span className='account-gallery__item__icons'>
<Icon id='eye-slash' />
<Icon id='eye-slash' icon={VisibilityOffIcon} />
</span>
);
} else {
@ -84,9 +88,9 @@ export default class MediaItem extends ImmutablePureComponent {
);
if (attachment.get('type') === 'audio') {
label = <Icon id='music' />;
label = <Icon id='music' icon={AudiotrackIcon} />;
} else {
label = <Icon id='play' />;
label = <Icon id='play' icon={PlayArrowIcon} />;
}
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;

View File

@ -7,6 +7,12 @@ import classNames from 'classnames';
import { is } from 'immutable';
import { ReactComponent as DownloadIcon } from '@material-symbols/svg-600/outlined/download.svg';
import { ReactComponent as PauseIcon } from '@material-symbols/svg-600/outlined/pause.svg';
import { ReactComponent as PlayArrowIcon } from '@material-symbols/svg-600/outlined/play_arrow.svg';
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
import { ReactComponent as VolumeOffIcon } from '@material-symbols/svg-600/outlined/volume_off.svg';
import { ReactComponent as VolumeUpIcon } from '@material-symbols/svg-600/outlined/volume_up.svg';
import { throttle, debounce } from 'lodash';
import { Icon } from 'mastodon/components/icon';
@ -554,8 +560,8 @@ class Audio extends PureComponent {
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} icon={paused ? PlayArrowIcon : PauseIcon} /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} icon={muted ? VolumeOffIcon : VolumeUpIcon} /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} />
@ -575,9 +581,9 @@ class Audio extends PureComponent {
</div>
<div className='video-player__buttons right'>
{!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>}
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
<Icon id={'download'} icon={DownloadIcon} />
</a>
</div>
</div>

View File

@ -8,6 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { ReactComponent as BookmarksIcon } from '@material-symbols/svg-600/outlined/bookmarks-fill.svg';
import { debounce } from 'lodash';
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'mastodon/actions/bookmarks';
@ -79,7 +80,8 @@ class Bookmarks extends ImmutablePureComponent {
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='bookmark'