Merge tag 'v3.0.1' into instance_only_statuses
This commit is contained in:
28
app/javascript/mastodon/components/autosuggest_hashtag.js
Normal file
28
app/javascript/mastodon/components/autosuggest_hashtag.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shortNumberFormat } from 'mastodon/utils/numbers';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class AutosuggestHashtag extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
tag: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
history: PropTypes.array,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { tag } = this.props;
|
||||
const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
|
||||
{tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isRtl } from '../rtl';
|
||||
@ -167,15 +168,15 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
||||
const { selectedSuggestion } = this.state;
|
||||
let inner, key;
|
||||
|
||||
if (typeof suggestion === 'object') {
|
||||
if (suggestion.type === 'emoji') {
|
||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||
key = suggestion.id;
|
||||
} else if (suggestion[0] === '#') {
|
||||
inner = suggestion;
|
||||
key = suggestion;
|
||||
} else {
|
||||
inner = <AutosuggestAccountContainer id={suggestion} />;
|
||||
key = suggestion;
|
||||
} else if (suggestion.type ==='hashtag') {
|
||||
inner = <AutosuggestHashtag tag={suggestion} />;
|
||||
key = suggestion.name;
|
||||
} else if (suggestion.type === 'account') {
|
||||
inner = <AutosuggestAccountContainer id={suggestion.id} />;
|
||||
key = suggestion.id;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isRtl } from '../rtl';
|
||||
@ -173,15 +174,15 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||
const { selectedSuggestion } = this.state;
|
||||
let inner, key;
|
||||
|
||||
if (typeof suggestion === 'object') {
|
||||
if (suggestion.type === 'emoji') {
|
||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||
key = suggestion.id;
|
||||
} else if (suggestion[0] === '#') {
|
||||
inner = suggestion;
|
||||
key = suggestion;
|
||||
} else {
|
||||
inner = <AutosuggestAccountContainer id={suggestion} />;
|
||||
key = suggestion;
|
||||
} else if (suggestion.type === 'hashtag') {
|
||||
inner = <AutosuggestHashtag tag={suggestion} />;
|
||||
key = suggestion.name;
|
||||
} else if (suggestion.type === 'account') {
|
||||
inner = <AutosuggestAccountContainer id={suggestion.id} />;
|
||||
key = suggestion.id;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
if (size === 2) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
} else {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
} else if (size === 3) {
|
||||
if (index === 0) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
} else if (index > 0) {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
|
||||
if (index === 1) {
|
||||
bottom = '2px';
|
||||
bottom = '1px';
|
||||
} else if (index > 1) {
|
||||
top = '2px';
|
||||
top = '1px';
|
||||
}
|
||||
} else if (size === 4) {
|
||||
if (index === 0 || index === 2) {
|
||||
right = '2px';
|
||||
right = '1px';
|
||||
}
|
||||
|
||||
if (index === 1 || index === 3) {
|
||||
left = '2px';
|
||||
left = '1px';
|
||||
}
|
||||
|
||||
if (index < 2) {
|
||||
bottom = '2px';
|
||||
bottom = '1px';
|
||||
} else {
|
||||
top = '2px';
|
||||
top = '1px';
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +88,13 @@ export default class AvatarComposite extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
|
||||
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
|
||||
{accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))}
|
||||
|
||||
{accounts.size > 4 && (
|
||||
<span className='account__avatar-composite__label'>
|
||||
+{accounts.size - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -8,10 +8,11 @@ export default class Column extends React.PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
label: PropTypes.string,
|
||||
bindToDocument: PropTypes.bool,
|
||||
};
|
||||
|
||||
scrollTop () {
|
||||
const scrollable = this.node.querySelector('.scrollable');
|
||||
const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
|
||||
|
||||
if (!scrollable) {
|
||||
return;
|
||||
@ -33,11 +34,19 @@ export default class Column extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
|
||||
if (this.props.bindToDocument) {
|
||||
document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
|
||||
} else {
|
||||
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.node.removeEventListener('wheel', this.handleWheel);
|
||||
if (this.props.bindToDocument) {
|
||||
document.removeEventListener('wheel', this.handleWheel);
|
||||
} else {
|
||||
this.node.removeEventListener('wheel', this.handleWheel);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export default class ColumnBackButton extends React.PureComponent {
|
||||
|
||||
@ -9,6 +10,10 @@ export default class ColumnBackButton extends React.PureComponent {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
this.context.router.history.push('/');
|
||||
@ -18,12 +23,32 @@ export default class ColumnBackButton extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
const { multiColumn } = this.props;
|
||||
|
||||
const component = (
|
||||
<button onClick={this.handleClick} className='column-back-button'>
|
||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>
|
||||
);
|
||||
|
||||
if (multiColumn) {
|
||||
return component;
|
||||
} else {
|
||||
// The portal container and the component may be rendered to the DOM in
|
||||
// the same React render pass, so the container might not be available at
|
||||
// the time `render()` is called.
|
||||
const container = document.getElementById('tabs-bar__portal');
|
||||
if (container === null) {
|
||||
// The container wasn't available, force a re-render so that the
|
||||
// component can eventually be inserted in the container and not scroll
|
||||
// with the rest of the area.
|
||||
this.forceUpdate();
|
||||
return component;
|
||||
} else {
|
||||
return createPortal(component, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
@ -28,6 +29,7 @@ class ColumnHeader extends React.PureComponent {
|
||||
showBackButton: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
pinned: PropTypes.bool,
|
||||
placeholder: PropTypes.bool,
|
||||
onPin: PropTypes.func,
|
||||
onMove: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
@ -79,7 +81,7 @@ class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props;
|
||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
@ -118,7 +120,7 @@ class ColumnHeader extends React.PureComponent {
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn) {
|
||||
} 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>;
|
||||
}
|
||||
|
||||
@ -140,13 +142,13 @@ class ColumnHeader extends React.PureComponent {
|
||||
collapsedContent.push(pinButton);
|
||||
}
|
||||
|
||||
if (children || multiColumn) {
|
||||
if (children || (multiColumn && this.props.onPin)) {
|
||||
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
|
||||
}
|
||||
|
||||
const hasTitle = icon && title;
|
||||
|
||||
return (
|
||||
const component = (
|
||||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
@ -172,6 +174,24 @@ class ColumnHeader extends React.PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (multiColumn || placeholder) {
|
||||
return component;
|
||||
} else {
|
||||
// The portal container and the component may be rendered to the DOM in
|
||||
// the same React render pass, so the container might not be available at
|
||||
// the time `render()` is called.
|
||||
const container = document.getElementById('tabs-bar__portal');
|
||||
if (container === null) {
|
||||
// The container wasn't available, force a re-render so that the
|
||||
// component can eventually be inserted in the container and not scroll
|
||||
// with the rest of the area.
|
||||
this.forceUpdate();
|
||||
return component;
|
||||
} else {
|
||||
return createPortal(component, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import illustration from '../../images/elephant_ui_disappointed.svg';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { version, source_url } from 'mastodon/initial_state';
|
||||
|
||||
export default class ErrorBoundary extends React.PureComponent {
|
||||
|
||||
@ -12,26 +13,53 @@ export default class ErrorBoundary extends React.PureComponent {
|
||||
hasError: false,
|
||||
stackTrace: undefined,
|
||||
componentStack: undefined,
|
||||
}
|
||||
};
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
componentDidCatch (error, info) {
|
||||
this.setState({
|
||||
hasError: true,
|
||||
stackTrace: error.stack,
|
||||
componentStack: info && info.componentStack,
|
||||
copied: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleCopyStackTrace = () => {
|
||||
const { stackTrace } = this.state;
|
||||
const textarea = document.createElement('textarea');
|
||||
|
||||
textarea.textContent = stackTrace;
|
||||
textarea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
try {
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
} catch (e) {
|
||||
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => this.setState({ copied: false }), 700);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasError } = this.state;
|
||||
const { hasError, copied } = this.state;
|
||||
|
||||
if (!hasError) {
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img src={illustration} alt='' />
|
||||
<div className='error-boundary'>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class ExtendedVideoPlayer extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
controls: PropTypes.bool.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
handleLoadedData = () => {
|
||||
if (this.props.time) {
|
||||
this.video.currentTime = this.props.time;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.video = c;
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
e.stopPropagation();
|
||||
const handler = this.props.onClick;
|
||||
if (handler) handler();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { src, muted, controls, alt } = this.props;
|
||||
|
||||
return (
|
||||
<div className='extended-video-player'>
|
||||
<video
|
||||
ref={this.setRef}
|
||||
src={src}
|
||||
autoPlay
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
aria-label={alt}
|
||||
title={alt}
|
||||
muted={muted}
|
||||
controls={controls}
|
||||
loop={!controls}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
75
app/javascript/mastodon/components/gifv.js
Normal file
75
app/javascript/mastodon/components/gifv.js
Normal file
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class GIFV extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
handleLoadedData = () => {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.src !== this.props.src) {
|
||||
this.setState({ loading: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
const { onClick } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { src, width, height, alt } = this.props;
|
||||
const { loading } = this.state;
|
||||
|
||||
return (
|
||||
<div className='gifv' style={{ position: 'relative' }}>
|
||||
{loading && (
|
||||
<canvas
|
||||
width={width}
|
||||
height={height}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
aria-label={alt}
|
||||
title={alt}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
<video
|
||||
src={src}
|
||||
width={width}
|
||||
height={height}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
aria-label={alt}
|
||||
title={alt}
|
||||
muted
|
||||
loop
|
||||
autoPlay
|
||||
playsInline
|
||||
onClick={this.handleClick}
|
||||
onLoadedData={this.handleLoadedData}
|
||||
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -12,11 +12,11 @@ const Hashtag = ({ hashtag }) => (
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
</Permalink>
|
||||
|
||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
|
||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} />
|
||||
</div>
|
||||
|
||||
<div className='trends__item__current'>
|
||||
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
|
||||
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)}
|
||||
</div>
|
||||
|
||||
<div className='trends__item__sparkline'>
|
||||
|
22
app/javascript/mastodon/components/load_pending.js
Normal file
22
app/javascript/mastodon/components/load_pending.js
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class LoadPending extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
count: PropTypes.number,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { count } = this.props;
|
||||
|
||||
return (
|
||||
<button className='load-more load-gap' onClick={this.props.onClick}>
|
||||
<FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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 } from '../initial_state';
|
||||
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -81,6 +81,8 @@ class Item extends React.PureComponent {
|
||||
}
|
||||
|
||||
_decode () {
|
||||
if (!useBlurhash) return;
|
||||
|
||||
const hash = this.props.attachment.get('blurhash');
|
||||
const pixels = decode(hash, 32, 32);
|
||||
|
||||
@ -157,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')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||
</a>
|
||||
</div>
|
||||
@ -313,15 +315,22 @@ class MediaGallery extends React.PureComponent {
|
||||
style.height = height;
|
||||
}
|
||||
|
||||
const size = media.take(4).size;
|
||||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
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} />);
|
||||
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} />);
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
if (uncached) {
|
||||
spoilerButton = (
|
||||
<button type='button' disabled className='spoiler-button__overlay'>
|
||||
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span>
|
||||
</button>
|
||||
);
|
||||
} else if (visible) {
|
||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
||||
} else {
|
||||
spoilerButton = (
|
||||
@ -333,7 +342,7 @@ class MediaGallery extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className='media-gallery' style={style} ref={this.handleRef}>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
|
||||
{spoilerButton}
|
||||
</div>
|
||||
|
||||
|
@ -1,17 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import illustration from 'mastodon/../images/elephant_ui_disappointed.svg';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MissingIndicator = () => (
|
||||
<div className='regeneration-indicator missing-indicator'>
|
||||
<div>
|
||||
<div className='regeneration-indicator__figure' />
|
||||
const MissingIndicator = ({ fullPage }) => (
|
||||
<div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}>
|
||||
<div className='regeneration-indicator__figure'>
|
||||
<img src={illustration} alt='' />
|
||||
</div>
|
||||
|
||||
<div className='regeneration-indicator__label'>
|
||||
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
|
||||
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
|
||||
</div>
|
||||
<div className='regeneration-indicator__label'>
|
||||
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
|
||||
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
MissingIndicator.propTypes = {
|
||||
fullPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MissingIndicator;
|
||||
|
@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import emojify from 'mastodon/features/emoji/emoji';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
|
||||
});
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||
@ -32,8 +34,38 @@ class Poll extends ImmutablePureComponent {
|
||||
|
||||
state = {
|
||||
selected: {},
|
||||
expired: null,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps (props, state) {
|
||||
const { poll, intl } = props;
|
||||
const expired = poll.get('expired') || (new Date(poll.get('expires_at'))).getTime() < intl.now();
|
||||
return (expired === state.expired) ? null : { expired };
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._setupTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this._setupTimer();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this._timer);
|
||||
}
|
||||
|
||||
_setupTimer () {
|
||||
const { poll, intl } = this.props;
|
||||
clearTimeout(this._timer);
|
||||
if (!this.state.expired) {
|
||||
const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now();
|
||||
this._timer = setTimeout(() => {
|
||||
this.setState({ expired: true });
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
handleOptionChange = e => {
|
||||
const { target: { value } } = e;
|
||||
|
||||
@ -68,12 +100,13 @@ class Poll extends ImmutablePureComponent {
|
||||
this.props.dispatch(fetchPoll(this.props.poll.get('id')));
|
||||
};
|
||||
|
||||
renderOption (option, optionIndex) {
|
||||
const { poll, disabled } = this.props;
|
||||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
|
||||
const active = !!this.state.selected[`${optionIndex}`];
|
||||
const showResults = poll.get('voted') || poll.get('expired');
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
const { poll, disabled, intl } = this.props;
|
||||
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
|
||||
const active = !!this.state.selected[`${optionIndex}`];
|
||||
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
||||
|
||||
let titleEmojified = option.get('title_emojified');
|
||||
if (!titleEmojified) {
|
||||
@ -102,7 +135,10 @@ class Poll extends ImmutablePureComponent {
|
||||
/>
|
||||
|
||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
||||
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
|
||||
{showResults && <span className='poll__number'>
|
||||
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
||||
{Math.round(percent)}%
|
||||
</span>}
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
|
||||
</label>
|
||||
@ -112,25 +148,34 @@ class Poll extends ImmutablePureComponent {
|
||||
|
||||
render () {
|
||||
const { poll, intl } = this.props;
|
||||
const { expired } = this.state;
|
||||
|
||||
if (!poll) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
|
||||
const showResults = poll.get('voted') || poll.get('expired');
|
||||
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
|
||||
const showResults = poll.get('voted') || expired;
|
||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
||||
|
||||
let votesCount = null;
|
||||
|
||||
if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
|
||||
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
|
||||
} else {
|
||||
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='poll'>
|
||||
<ul>
|
||||
{poll.get('options').map((option, i) => this.renderOption(option, i))}
|
||||
{poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
|
||||
<FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
|
||||
{votesCount}
|
||||
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
35
app/javascript/mastodon/components/radio_button.js
Normal file
35
app/javascript/mastodon/components/radio_button.js
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class RadioButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { name, value, checked, onChange, label } = this.props;
|
||||
|
||||
return (
|
||||
<label className='radio-button'>
|
||||
<input
|
||||
name={name}
|
||||
type='radio'
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<span className={classNames('radio-button__input', { checked })} />
|
||||
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
18
app/javascript/mastodon/components/regeneration_indicator.js
Normal file
18
app/javascript/mastodon/components/regeneration_indicator.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import illustration from 'mastodon/../images/elephant_ui_working.svg';
|
||||
|
||||
const MissingIndicator = () => (
|
||||
<div className='regeneration-indicator'>
|
||||
<div className='regeneration-indicator__figure'>
|
||||
<img src={illustration} alt='' />
|
||||
</div>
|
||||
|
||||
<div className='regeneration-indicator__label'>
|
||||
<FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' />
|
||||
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MissingIndicator;
|
@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4';
|
||||
import PropTypes from 'prop-types';
|
||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||
import LoadMore from './load_more';
|
||||
import LoadPending from './load_pending';
|
||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
|
||||
import { throttle } from 'lodash';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent {
|
||||
static propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
onLoadMore: PropTypes.func,
|
||||
onLoadPending: PropTypes.func,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
trackScroll: PropTypes.bool,
|
||||
@ -28,10 +30,12 @@ export default class ScrollableList extends PureComponent {
|
||||
isLoading: PropTypes.bool,
|
||||
showLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
numPending: PropTypes.number,
|
||||
prepend: PropTypes.node,
|
||||
alwaysPrepend: PropTypes.bool,
|
||||
emptyMessage: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
bindToDocument: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -47,7 +51,9 @@ export default class ScrollableList extends PureComponent {
|
||||
|
||||
handleScroll = throttle(() => {
|
||||
if (this.node) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.node;
|
||||
const scrollTop = this.getScrollTop();
|
||||
const scrollHeight = this.getScrollHeight();
|
||||
const clientHeight = this.getClientHeight();
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
@ -77,9 +83,14 @@ export default class ScrollableList extends PureComponent {
|
||||
scrollToTopOnMouseIdle = false;
|
||||
|
||||
setScrollTop = newScrollTop => {
|
||||
if (this.node.scrollTop !== newScrollTop) {
|
||||
if (this.getScrollTop() !== newScrollTop) {
|
||||
this.lastScrollWasSynthetic = true;
|
||||
this.node.scrollTop = newScrollTop;
|
||||
|
||||
if (this.props.bindToDocument) {
|
||||
document.scrollingElement.scrollTop = newScrollTop;
|
||||
} else {
|
||||
this.node.scrollTop = newScrollTop;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -97,7 +108,7 @@ export default class ScrollableList extends PureComponent {
|
||||
this.clearMouseIdleTimer();
|
||||
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
||||
|
||||
if (!this.mouseMovedRecently && this.node.scrollTop === 0) {
|
||||
if (!this.mouseMovedRecently && this.getScrollTop() === 0) {
|
||||
// Only set if we just started moving and are scrolled to the top.
|
||||
this.scrollToTopOnMouseIdle = true;
|
||||
}
|
||||
@ -132,15 +143,27 @@ export default class ScrollableList extends PureComponent {
|
||||
}
|
||||
|
||||
getScrollPosition = () => {
|
||||
if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
|
||||
return { height: this.node.scrollHeight, top: this.node.scrollTop };
|
||||
if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
|
||||
return { height: this.getScrollHeight(), top: this.getScrollTop() };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getScrollTop = () => {
|
||||
return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop;
|
||||
}
|
||||
|
||||
getScrollHeight = () => {
|
||||
return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight;
|
||||
}
|
||||
|
||||
getClientHeight = () => {
|
||||
return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight;
|
||||
}
|
||||
|
||||
updateScrollBottom = (snapshot) => {
|
||||
const newScrollTop = this.node.scrollHeight - snapshot;
|
||||
const newScrollTop = this.getScrollHeight() - snapshot;
|
||||
|
||||
this.setScrollTop(newScrollTop);
|
||||
}
|
||||
@ -149,9 +172,10 @@ export default class ScrollableList extends PureComponent {
|
||||
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
||||
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
||||
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
||||
const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0);
|
||||
|
||||
if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
|
||||
return this.node.scrollHeight - this.node.scrollTop;
|
||||
if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
|
||||
return this.getScrollHeight() - this.getScrollTop();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -161,7 +185,7 @@ export default class ScrollableList extends PureComponent {
|
||||
// Reset the scroll position when a new child comes in in order not to
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (snapshot !== null) {
|
||||
this.setScrollTop(this.node.scrollHeight - snapshot);
|
||||
this.setScrollTop(this.getScrollHeight() - snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,6 +199,7 @@ export default class ScrollableList extends PureComponent {
|
||||
this.clearMouseIdleTimer();
|
||||
this.detachScrollListener();
|
||||
this.detachIntersectionObserver();
|
||||
|
||||
detachFullscreenListener(this.onFullScreenChange);
|
||||
}
|
||||
|
||||
@ -194,13 +219,23 @@ export default class ScrollableList extends PureComponent {
|
||||
}
|
||||
|
||||
attachScrollListener () {
|
||||
this.node.addEventListener('scroll', this.handleScroll);
|
||||
this.node.addEventListener('wheel', this.handleWheel);
|
||||
if (this.props.bindToDocument) {
|
||||
document.addEventListener('scroll', this.handleScroll);
|
||||
document.addEventListener('wheel', this.handleWheel);
|
||||
} else {
|
||||
this.node.addEventListener('scroll', this.handleScroll);
|
||||
this.node.addEventListener('wheel', this.handleWheel);
|
||||
}
|
||||
}
|
||||
|
||||
detachScrollListener () {
|
||||
this.node.removeEventListener('scroll', this.handleScroll);
|
||||
this.node.removeEventListener('wheel', this.handleWheel);
|
||||
if (this.props.bindToDocument) {
|
||||
document.removeEventListener('scroll', this.handleScroll);
|
||||
document.removeEventListener('wheel', this.handleWheel);
|
||||
} else {
|
||||
this.node.removeEventListener('scroll', this.handleScroll);
|
||||
this.node.removeEventListener('wheel', this.handleWheel);
|
||||
}
|
||||
}
|
||||
|
||||
getFirstChildKey (props) {
|
||||
@ -225,12 +260,25 @@ export default class ScrollableList extends PureComponent {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
handleLoadPending = e => {
|
||||
e.preventDefault();
|
||||
this.props.onLoadPending();
|
||||
// Prevent the weird scroll-jumping behavior, as we explicitly don't want to
|
||||
// scroll to top, and we know the scroll height is going to change
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
this.lastScrollWasSynthetic = false;
|
||||
this.clearMouseIdleTimer();
|
||||
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
||||
this.mouseMovedRecently = true;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
|
||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
const childrenCount = React.Children.count(children);
|
||||
|
||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||
const loadPending = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
|
||||
let scrollableArea = null;
|
||||
|
||||
if (showLoading) {
|
||||
@ -251,6 +299,8 @@ export default class ScrollableList extends PureComponent {
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
|
||||
{loadPending}
|
||||
|
||||
{React.Children.map(this.props.children, (child, index) => (
|
||||
<IntersectionObserverArticleContainer
|
||||
key={child.key}
|
||||
|
@ -12,7 +12,7 @@ import AttachmentList from './attachment_list';
|
||||
import Card from '../features/status/components/card';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { MediaGallery, Video } from '../features/ui/util/async-components';
|
||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
@ -199,11 +199,15 @@ class Status extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
renderLoadingMediaGallery () {
|
||||
return <div className='media_gallery' style={{ height: '110px' }} />;
|
||||
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
renderLoadingVideoPlayer () {
|
||||
return <div className='media-spoiler-video' style={{ height: '110px' }} />;
|
||||
return <div className='video-player' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
renderLoadingAudioPlayer () {
|
||||
return <div className='audio-player' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
@ -278,12 +282,27 @@ class Status extends ImmutablePureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlers = this.props.muted ? {} : {
|
||||
reply: this.handleHotkeyReply,
|
||||
favourite: this.handleHotkeyFavourite,
|
||||
boost: this.handleHotkeyBoost,
|
||||
mention: this.handleHotkeyMention,
|
||||
open: this.handleHotkeyOpen,
|
||||
openProfile: this.handleHotkeyOpenProfile,
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
toggleHidden: this.handleHotkeyToggleHidden,
|
||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<div ref={this.handleRef}>
|
||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||
{status.get('content')}
|
||||
</div>
|
||||
<HotKeys handlers={handlers}>
|
||||
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'>
|
||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||
{status.get('content')}
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
@ -333,7 +352,23 @@ class Status extends ImmutablePureComponent {
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
);
|
||||
} else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||
peaks={[0]}
|
||||
height={70}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
@ -394,19 +429,6 @@ class Status extends ImmutablePureComponent {
|
||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
|
||||
const handlers = this.props.muted ? {} : {
|
||||
reply: this.handleHotkeyReply,
|
||||
favourite: this.handleHotkeyFavourite,
|
||||
boost: this.handleHotkeyBoost,
|
||||
mention: this.handleHotkeyMention,
|
||||
open: this.handleHotkeyOpen,
|
||||
openProfile: this.handleHotkeyOpenProfile,
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
toggleHidden: this.handleHotkeyToggleHidden,
|
||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
|
@ -55,6 +55,7 @@ export default class StatusContent extends React.PureComponent {
|
||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||
} else {
|
||||
link.setAttribute('title', link.href);
|
||||
link.classList.add('unhandled-link');
|
||||
}
|
||||
|
||||
link.setAttribute('target', '_blank');
|
||||
@ -111,7 +112,7 @@ export default class StatusContent extends React.PureComponent {
|
||||
}
|
||||
|
||||
onHashtagClick = (hashtag, e) => {
|
||||
hashtag = hashtag.replace(/^#/, '').toLowerCase();
|
||||
hashtag = hashtag.replace(/^#/, '');
|
||||
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
@ -215,22 +216,22 @@ export default class StatusContent extends React.PureComponent {
|
||||
return (
|
||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} />
|
||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||
{' '}
|
||||
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
|
||||
</p>
|
||||
|
||||
{mentionsPlaceholder}
|
||||
|
||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
|
||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
|
||||
|
||||
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.onClick) {
|
||||
const output = [
|
||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
|
||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
|
||||
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
|
||||
|
||||
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||
</div>,
|
||||
@ -244,7 +245,7 @@ export default class StatusContent extends React.PureComponent {
|
||||
} else {
|
||||
return (
|
||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
|
||||
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
|
||||
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
|
||||
|
||||
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||
</div>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { debounce } from 'lodash';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusContainer from '../containers/status_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import LoadGap from './load_gap';
|
||||
import ScrollableList from './scrollable_list';
|
||||
import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
|
||||
|
||||
export default class StatusList extends ImmutablePureComponent {
|
||||
|
||||
@ -81,18 +81,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
const { isLoading, isPartial } = other;
|
||||
|
||||
if (isPartial) {
|
||||
return (
|
||||
<div className='regeneration-indicator'>
|
||||
<div>
|
||||
<div className='regeneration-indicator__figure' />
|
||||
|
||||
<div className='regeneration-indicator__label'>
|
||||
<FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' />
|
||||
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <RegenerationIndicator />;
|
||||
}
|
||||
|
||||
let scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||
|
Reference in New Issue
Block a user