Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- README.md
- app/helpers/statuses_helper.rb
  Upstream moved account helpers to their own file, we had extra
  helpers there, moved too.
- app/lib/sanitize_config.rb
- app/models/user.rb
- app/serializers/initial_state_serializer.rb
- config/locales/simple_form.en.yml
- spec/lib/sanitize_config_spec.rb
This commit is contained in:
Thibaut Girka
2019-10-27 12:14:01 +01:00
68 changed files with 631 additions and 454 deletions

View File

@@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a>
</li>
);
})}
@@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a>
</li>
);
})}

View File

@@ -143,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>

View File

@@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {
<div>
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div>
</div>
);

View File

@@ -1,6 +1,4 @@
import React from 'react';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
@@ -37,6 +35,21 @@ export default class IconButton extends React.PureComponent {
tabIndex: '0',
};
state = {
activate: false,
deactivate: false,
}
componentWillReceiveProps (nextProps) {
if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) {
this.setState({ activate: false, deactivate: true });
} else if (!this.props.active && nextProps.active) {
this.setState({ activate: true, deactivate: false });
}
}
handleClick = (e) => {
e.preventDefault();
@@ -75,7 +88,6 @@ export default class IconButton extends React.PureComponent {
const {
active,
animate,
className,
disabled,
expanded,
@@ -87,57 +99,37 @@ export default class IconButton extends React.PureComponent {
title,
} = this.props;
const {
activate,
deactivate,
} = this.state;
const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
activate,
deactivate,
overlayed: overlay,
});
if (!animate) {
// Perf optimization: avoid unnecessary <Motion> components unless
// we actually need to animate.
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}
return (
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) => (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button>
)}
</Motion>
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}

View File

@@ -6,7 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
import classNames from 'classnames';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
import { decode } from 'blurhash';
const messages = defineMessages({
@@ -159,7 +159,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') {
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}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
<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'>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a>
</div>
@@ -187,6 +187,7 @@ class Item extends React.PureComponent {
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
rel='noopener noreferrer'
>
<img
src={previewUrl}
@@ -280,7 +281,7 @@ class MediaGallery extends React.PureComponent {
}
handleRef = (node) => {
if (node /*&& this.isStandaloneEligible()*/) {
if (node) {
// offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
@@ -290,13 +291,13 @@ class MediaGallery extends React.PureComponent {
}
}
isStandaloneEligible() {
const { media, standalone } = this.props;
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
isFullSizeEligible() {
const { media } = this.props;
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
}
render () {
const { media, intl, sensitive, height, defaultWidth } = this.props;
const { media, intl, sensitive, height, defaultWidth, standalone } = this.props;
const { visible } = this.state;
const width = this.state.width || defaultWidth;
@@ -305,7 +306,7 @@ class MediaGallery extends React.PureComponent {
const style = {};
if (this.isStandaloneEligible()) {
if (this.isFullSizeEligible() && (standalone || !cropImages)) {
if (width) {
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
}
@@ -318,7 +319,7 @@ class MediaGallery extends React.PureComponent {
const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
if (this.isStandaloneEligible()) {
if (standalone && this.isFullSizeEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />);

View File

@@ -437,9 +437,9 @@ class Status extends ImmutablePureComponent {
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><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} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'>
<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'>
<div className='status__avatar'>
{statusAvatar}
</div>

View File

@@ -59,7 +59,7 @@ export default class StatusContent extends React.PureComponent {
}
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
link.setAttribute('rel', 'noopener noreferrer');
}
if (

View File

@@ -253,7 +253,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener' target='_blank'>
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
<Avatar account={account} size={90} />
</a>
@@ -282,10 +282,10 @@ class Header extends ImmutablePureComponent {
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
<dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<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) })}>
<Icon id='check' className='verified__mark' />
</span></a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener'><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>
</dl>
))}

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { decode } from 'blurhash';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
import classNames from 'classnames';
import { decode } from 'blurhash';
import { isIOS } from 'mastodon/is_mobile';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
export default class MediaItem extends ImmutablePureComponent {
@@ -151,7 +151,7 @@ export default class MediaItem extends ImmutablePureComponent {
return (
<div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
{visible && thumbnail}
{!visible && icon}

View File

@@ -12,6 +12,7 @@ const messages = defineMessages({
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
download: { id: 'video.download', defaultMessage: 'Download file' },
});
export default @injectIntl
@@ -202,6 +203,7 @@ class Audio extends React.PureComponent {
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span
@@ -217,6 +219,14 @@ class Audio extends React.PureComponent {
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
</span>
</div>
<div className='video-player__buttons right'>
<button type='button' aria-label={intl.formatMessage(messages.download)}>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</button>
</div>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/lib/Async';
import AsyncSelect from 'react-select/async';
const messages = defineMessages({
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },

View File

@@ -148,7 +148,7 @@ export default class Card extends React.PureComponent {
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const ratio = card.get('width') / card.get('height');
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
@@ -180,7 +180,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__actions'>
<div>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>}
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
</div>
</div>
</div>
@@ -208,7 +208,7 @@ export default class Card extends React.PureComponent {
}
return (
<a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
<a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
{embed}
{description}
</a>

View File

@@ -156,7 +156,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
}
if (status.get('application')) {
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>;
}
if (status.get('visibility') === 'direct') {
@@ -220,7 +220,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
{media}
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{applicationLink} · {reblogLink} · {favouriteLink}
</div>

View File

@@ -26,7 +26,7 @@ export default class ActionsModal extends ImmutablePureComponent {
return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
@@ -42,7 +42,7 @@ export default class ActionsModal extends ImmutablePureComponent {
<div className='status light'>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<RelativeTimestamp timestamp={this.props.status.get('created_at')} />
</a>
</div>

View File

@@ -61,7 +61,7 @@ class BoostModal extends ImmutablePureComponent {
<div className='status light'>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><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>
</div>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>

View File

@@ -182,7 +182,7 @@ class ColumnsArea extends ImmutablePureComponent {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
const content = columnIndex !== -1 ? (
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
{links.map(this.renderView)}
</ReactSwipeableViews>
) : (

View File

@@ -62,7 +62,7 @@ class LinkFooter extends React.PureComponent {
<FormattedMessage
id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
values={{ github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }}
/>
</p>
</div>

View File

@@ -19,6 +19,7 @@ const messages = defineMessages({
close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
download: { id: 'video.download', defaultMessage: 'Download file' },
});
export const formatTime = secondsNum => {
@@ -470,6 +471,7 @@ class Video extends React.PureComponent {
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span
className={classNames('video-player__volume__handle')}
@@ -493,7 +495,13 @@ class Video extends React.PureComponent {
{(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
<button type='button' aria-label={intl.formatMessage(messages.download)}>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</button>
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
</div>
</div>
</div>

View File

@@ -25,5 +25,6 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const showTrends = getMeta('trends');
export const title = getMeta('title');
export const cropImages = getMeta('crop_images');
export default initialState;

View File

@@ -776,6 +776,10 @@
{
"defaultMessage": "Unmute sound",
"id": "video.unmute"
},
{
"defaultMessage": "Download file",
"id": "video.download"
}
],
"path": "app/javascript/mastodon/features/audio/index.json"

View File

@@ -3,6 +3,7 @@ import {
REBLOG_FAIL,
FAVOURITE_REQUEST,
FAVOURITE_FAIL,
UNFAVOURITE_SUCCESS,
} from '../actions/interactions';
import {
STATUS_MUTE_SUCCESS,
@@ -37,6 +38,9 @@ export default function statuses(state = initialState, action) {
return importStatuses(state, action.statuses);
case FAVOURITE_REQUEST:
return state.setIn([action.status.get('id'), 'favourited'], true);
case UNFAVOURITE_SUCCESS:
const favouritesCount = action.status.get('favourites_count');
return state.setIn([action.status.get('id'), 'favourites_count'], favouritesCount - 1);
case FAVOURITE_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
case REBLOG_REQUEST:

View File

@@ -1591,6 +1591,20 @@ a.account__display-name {
color: $gold-star;
}
.no-reduce-motion .icon-button.star-icon {
&.activate {
& > .fa-star {
animation: spring-rotate-in 1s linear;
}
}
&.deactivate {
& > .fa-star {
animation: spring-rotate-out 1s linear;
}
}
}
.notification__display-name {
color: inherit;
font-weight: 500;
@@ -3373,6 +3387,50 @@ a.status-card.compact:hover {
animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}
@keyframes spring-rotate-in {
0% {
transform: rotate(0deg);
}
30% {
transform: rotate(-484.8deg);
}
60% {
transform: rotate(-316.7deg);
}
90% {
transform: rotate(-375deg);
}
100% {
transform: rotate(-360deg);
}
}
@keyframes spring-rotate-out {
0% {
transform: rotate(-360deg);
}
30% {
transform: rotate(124.8deg);
}
60% {
transform: rotate(-43.27deg);
}
90% {
transform: rotate(15deg);
}
100% {
transform: rotate(0deg);
}
}
@keyframes loader-figure {
0% {
width: 0;
@@ -5194,6 +5252,7 @@ a.status-card.compact:hover {
max-height: 100% !important;
width: 100% !important;
height: 100% !important;
outline: 0;
}
}
@@ -5271,6 +5330,10 @@ a.status-card.compact:hover {
display: flex;
justify-content: space-between;
padding-bottom: 10px;
.video-player__download__icon {
color: inherit;
}
}
&__buttons {

View File

@@ -149,10 +149,6 @@ a.table-action-link {
margin-top: 0;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
&__actions,
@@ -174,10 +170,6 @@ a.table-action-link {
text-align: right;
padding-right: 16px - 5px;
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
&__form {
@@ -198,7 +190,7 @@ a.table-action-link {
background: darken($ui-base-color, 4%);
@media screen and (max-width: $no-gap-breakpoint) {
&:first-child {
.optional &:first-child {
border-top: 1px solid darken($ui-base-color, 8%);
}
}
@@ -264,6 +256,13 @@ a.table-action-link {
}
}
&.optional .batch-table__toolbar,
&.optional .batch-table__row__select {
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
.status__content {
padding-top: 0;