Merge tag 'v2.7.2' into instance_only_statuses

This commit is contained in:
Renato "Lond" Cerqueira
2019-02-19 21:07:43 +01:00
76 changed files with 1440 additions and 577 deletions

View File

@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent {
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) {
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {

View File

@ -11,26 +11,36 @@ export default class DisplayName extends React.PureComponent {
};
render () {
const { account, others, localDomain } = this.props;
const displayNameHtml = { __html: account.get('display_name_html') };
const { others, localDomain } = this.props;
let suffix;
let displayName, suffix, account;
if (others && others.size > 1) {
suffix = `+${others.size}`;
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`;
}
} else {
if (others) {
account = others.first();
} else {
account = this.props.account;
}
let acct = account.get('acct');
if (acct.indexOf('@') === -1 && localDomain) {
acct = `${acct}@${localDomain}`;
}
suffix = <span className='display-name__account'>@{acct}</span>;
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
suffix = <span className='display-name__account'>@{acct}</span>;
}
return (
<span className='display-name'>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
{displayName} {suffix}
</span>
);
}

View File

@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent {
</span>
<div className='domain__buttons'>
<IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
</div>
</div>
</div>

View File

@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component {
}
updateStateAfterIntersection = (prevState) => {
if (prevState.isIntersecting && !this.entry.isIntersecting) {
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {

View File

@ -194,6 +194,8 @@ class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
};
static defaultProps = {
@ -202,6 +204,7 @@ class MediaGallery extends React.PureComponent {
state = {
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
width: this.props.defaultWidth,
};
componentWillReceiveProps (nextProps) {
@ -221,6 +224,7 @@ class MediaGallery extends React.PureComponent {
handleRef = (node) => {
if (node /*&& this.isStandaloneEligible()*/) {
// offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
this.setState({
width: node.offsetWidth,
});
@ -233,8 +237,10 @@ class MediaGallery extends React.PureComponent {
}
render () {
const { media, intl, sensitive, height } = this.props;
const { width, visible } = this.state;
const { media, intl, sensitive, height, defaultWidth } = this.props;
const { visible } = this.state;
const width = this.state.width || defaultWidth;
let children;

View File

@ -40,6 +40,7 @@ export default class ScrollableList extends PureComponent {
state = {
fullscreen: null,
cachedMediaWidth: 250, // Default media/card width using default Mastodon theme
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
@ -130,6 +131,20 @@ export default class ScrollableList extends PureComponent {
this.handleScroll();
}
getScrollPosition = () => {
if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
return { height: this.node.scrollHeight, top: this.node.scrollTop };
} else {
return null;
}
}
updateScrollBottom = (snapshot) => {
const newScrollTop = this.node.scrollHeight - snapshot;
this.setScrollTop(newScrollTop);
}
getSnapshotBeforeUpdate (prevProps) {
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
@ -150,6 +165,12 @@ export default class ScrollableList extends PureComponent {
}
}
cacheMediaWidth = (width) => {
if (width && this.state.cachedMediaWidth !== width) {
this.setState({ cachedMediaWidth: width });
}
}
componentWillUnmount () {
this.clearMouseIdleTimer();
this.detachScrollListener();
@ -239,7 +260,12 @@ export default class ScrollableList extends PureComponent {
intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
>
{child}
{React.cloneElement(child, {
getScrollPosition: this.getScrollPosition,
updateScrollBottom: this.updateScrollBottom,
cachedMediaWidth: this.state.cachedMediaWidth,
cacheMediaWidth: this.cacheMediaWidth,
})}
</IntersectionObserverArticleContainer>
))}

View File

@ -68,6 +68,10 @@ class Status extends ImmutablePureComponent {
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
showThread: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
};
// Avoid checking props that are functions (and whose equality will always
@ -79,6 +83,43 @@ class Status extends ImmutablePureComponent {
'hidden',
];
// Track height changes we know about to compensate scrolling
componentDidMount () {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
}
getSnapshotBeforeUpdate () {
if (this.props.getScrollPosition) {
return this.props.getScrollPosition();
} else {
return null;
}
}
// Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) {
this.didShowCard = true;
if (snapshot !== null && this.props.updateScrollBottom) {
if (this.node && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
}
}
}
componentWillUnmount() {
if (this.node && this.props.getScrollPosition) {
const position = this.props.getScrollPosition();
if (position !== null && this.node.offsetTop < position.top) {
requestAnimationFrame(() => {
this.props.updateScrollBottom(position.height - position.top);
});
}
}
}
handleClick = () => {
if (this.props.onClick) {
this.props.onClick();
@ -165,6 +206,10 @@ class Status extends ImmutablePureComponent {
}
}
handleRef = c => {
this.node = c;
}
render () {
let media = null;
let statusAvatar, prepend, rebloggedByText;
@ -179,7 +224,7 @@ class Status extends ImmutablePureComponent {
if (hidden) {
return (
<div>
<div ref={this.handleRef}>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
@ -194,7 +239,7 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
</div>
</HotKeys>
@ -242,11 +287,12 @@ class Status extends ImmutablePureComponent {
preview={video.get('preview_url')}
src={video.get('url')}
alt={video.get('description')}
width={239}
width={this.props.cachedMediaWidth}
height={110}
inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
/>
)}
</Bundle>
@ -254,7 +300,16 @@ class Status extends ImmutablePureComponent {
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
{Component => (
<Component
media={status.get('media_attachments')}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.props.onOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
/>
)}
</Bundle>
);
}
@ -264,6 +319,8 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.props.onOpenMedia}
card={status.get('card')}
compact
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
/>
);
}
@ -290,7 +347,7 @@ class Status extends ImmutablePureComponent {
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, !status.get('hidden'))}>
<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, !status.get('hidden'))} ref={this.handleRef}>
{prepend}
<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')}>

View File

@ -78,7 +78,11 @@ class StatusActionBar extends ImmutablePureComponent {
]
handleReplyClick = () => {
this.props.onReply(this.props.status, this.context.router.history);
if (me) {
this.props.onReply(this.props.status, this.context.router.history);
} else {
this._openInteractionDialog('reply');
}
}
handleShareClick = () => {
@ -91,11 +95,23 @@ class StatusActionBar extends ImmutablePureComponent {
}
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
if (me) {
this.props.onFavourite(this.props.status);
} else {
this._openInteractionDialog('favourite');
}
}
handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e);
handleReblogClick = e => {
if (me) {
this.props.onReblog(this.props.status, e);
} else {
this._openInteractionDialog('reblog');
}
}
_openInteractionDialog = type => {
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}
handleDeleteClick = () => {
@ -213,9 +229,9 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<div className='status__action-bar__counter'><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} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton}
<div className='status__action-bar-dropdown'>

View File

@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Compose from '../features/standalone/compose';
import initialState from '../initial_state';
import { fetchCustomEmojis } from '../actions/custom_emojis';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
@ -17,6 +18,8 @@ if (initialState) {
store.dispatch(hydrateStore(initialState));
}
store.dispatch(fetchCustomEmojis());
export default class TimelineContainer extends React.PureComponent {
static propTypes = {

View File

@ -132,7 +132,7 @@ class Header extends ImmutablePureComponent {
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = (
<div className='account--action-button'>
<IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />
<IconButton size={26} icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />
</div>
);
}

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
});
export default @connect(mapStateToProps)
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, accountIds, shouldUpdateScroll } = this.props;
const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props;
if (!accountIds) {
return (
@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent {
<ScrollableList
scrollKey='blocks'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
>

View File

@ -181,7 +181,7 @@ class ComposeForm extends ImmutablePureComponent {
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
</label>
</div>

View File

@ -214,7 +214,7 @@ class PrivacyDropdown extends React.PureComponent {
this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
];

View File

@ -107,9 +107,8 @@ class Upload extends ImmutablePureComponent {
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
<input
<textarea
placeholder={intl.formatMessage(messages.description)}
type='text'
value={description}
maxLength={420}
onFocus={this.handleInputFocus}

View File

@ -19,6 +19,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
domains: state.getIn(['domain_lists', 'blocks', 'items']),
hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
});
export default @connect(mapStateToProps)
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
domains: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired,
};
@ -42,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, domains, shouldUpdateScroll } = this.props;
const { intl, domains, shouldUpdateScroll, hasMore } = this.props;
if (!domains) {
return (
@ -60,6 +62,7 @@ class Blocks extends ImmutablePureComponent {
<ScrollableList
scrollKey='domain_blocks'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
>

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
});
export default @connect(mapStateToProps)
@ -28,6 +29,7 @@ class FollowRequests extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
@ -41,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, accountIds } = this.props;
const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props;
if (!accountIds) {
return (
@ -59,6 +61,7 @@ class FollowRequests extends ImmutablePureComponent {
<ScrollableList
scrollKey='follow_requests'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
>

View File

@ -1,10 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/lib/Async';
const messages = defineMessages({
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
});
export default @injectIntl
class ColumnSettings extends React.PureComponent {
@ -25,6 +30,7 @@ class ColumnSettings extends React.PureComponent {
tags (mode) {
let tags = this.props.settings.getIn(['tags', mode]) || [];
if (tags.toJSON) {
return tags.toJSON();
} else {
@ -32,33 +38,36 @@ class ColumnSettings extends React.PureComponent {
}
};
onSelect = (mode) => {
return (value) => {
this.props.onChange(['tags', mode], value);
};
};
onSelect = mode => value => this.props.onChange(['tags', mode], value);
onToggle = () => {
if (this.state.open && this.hasTags()) {
this.props.onChange('tags', {});
}
this.setState({ open: !this.state.open });
};
noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
modeSelect (mode) {
return (
<div className='column-settings__section'>
{this.modeLabel(mode)}
<div className='column-settings__row'>
<span className='column-settings__section'>
{this.modeLabel(mode)}
</span>
<AsyncSelect
isMulti
autoFocus
value={this.tags(mode)}
settings={this.props.settings}
settingPath={['tags', mode]}
onChange={this.onSelect(mode)}
loadOptions={this.props.onLoad}
classNamePrefix='column-settings__hashtag-select'
className='column-select__container'
classNamePrefix='column-select'
name='tags'
placeholder={this.props.intl.formatMessage(messages.placeholder)}
noOptionsMessage={this.noOptionsMessage}
/>
</div>
);
@ -66,11 +75,15 @@ class ColumnSettings extends React.PureComponent {
modeLabel (mode) {
switch(mode) {
case 'any': return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
case 'all': return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
case 'any':
return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
case 'all':
return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
case 'none':
return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
default:
return '';
}
return '';
};
render () {
@ -78,23 +91,21 @@ class ColumnSettings extends React.PureComponent {
<div>
<div className='column-settings__row'>
<div className='setting-toggle'>
<Toggle
id='hashtag.column_settings.tag_toggle'
onChange={this.onToggle}
checked={this.state.open}
/>
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
<span className='setting-toggle__label'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
</span>
</div>
</div>
{this.state.open &&
{this.state.open && (
<div className='column-settings__hashtags'>
{this.modeSelect('any')}
{this.modeSelect('all')}
{this.modeSelect('none')}
</div>
}
)}
</div>
);
}

View File

@ -41,15 +41,19 @@ class HashtagTimeline extends React.PureComponent {
title = () => {
let title = [this.props.params.id];
if (this.additionalFor('any')) {
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
}
if (this.additionalFor('all')) {
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
}
if (this.additionalFor('none')) {
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
}
return title;
}
@ -77,9 +81,10 @@ class HashtagTimeline extends React.PureComponent {
let all = (tags.all || []).map(tag => tag.value);
let none = (tags.none || []).map(tag => tag.value);
[id, ...any].map((tag) => {
this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => {
[id, ...any].map(tag => {
this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
let tags = status.tags.map(tag => tag.name);
return all.filter(tag => tags.includes(tag)).length === all.length &&
none.filter(tag => tags.includes(tag)).length === 0;
})));
@ -95,12 +100,14 @@ class HashtagTimeline extends React.PureComponent {
const { dispatch } = this.props;
const { id, tags } = this.props.params;
this._subscribe(dispatch, id, tags);
dispatch(expandHashtagTimeline(id, { tags }));
}
componentWillReceiveProps (nextProps) {
const { dispatch, params } = this.props;
const { id, tags } = nextProps.params;
if (id !== params.id || !isEqual(tags, params.tags)) {
this._unsubscribe();
this._subscribe(dispatch, id, tags);

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
});
export default @connect(mapStateToProps)
@ -28,6 +29,7 @@ class Mutes extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
@ -41,7 +43,7 @@ class Mutes extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, accountIds } = this.props;
const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props;
if (!accountIds) {
return (
@ -59,6 +61,7 @@ class Mutes extends ImmutablePureComponent {
<ScrollableList
scrollKey='mutes'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
>

View File

@ -34,6 +34,10 @@ class Notification extends ImmutablePureComponent {
onToggleHidden: PropTypes.func.isRequired,
status: PropTypes.option,
intl: PropTypes.object.isRequired,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
};
handleMoveUp = () => {
@ -128,6 +132,10 @@ class Notification extends ImmutablePureComponent {
onMoveDown={this.handleMoveDown}
onMoveUp={this.handleMoveUp}
contextType='notifications'
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
);
}
@ -148,7 +156,17 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} />
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
muted
withDismiss
hidden={!!this.props.hidden}
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
);
@ -170,7 +188,17 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} />
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
muted
withDismiss
hidden={this.props.hidden}
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
);

View File

@ -60,6 +60,8 @@ export default class Card extends React.PureComponent {
maxDescription: PropTypes.number,
onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.bool,
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
};
static defaultProps = {
@ -68,7 +70,7 @@ export default class Card extends React.PureComponent {
};
state = {
width: 280,
width: this.props.defaultWidth || 280,
embedded: false,
};
@ -111,6 +113,7 @@ export default class Card extends React.PureComponent {
setRef = c => {
if (c) {
if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
this.setState({ width: c.offsetWidth });
}
}

View File

@ -91,7 +91,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
}
render () {
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const intl = this.props.intl;
const outerStyle = { boxSizing: 'border-box' };
const { compact } = this.props;

View File

@ -99,6 +99,7 @@ class Video extends React.PureComponent {
onCloseVideo: PropTypes.func,
detailed: PropTypes.bool,
inline: PropTypes.bool,
cacheWidth: PropTypes.func,
intl: PropTypes.object.isRequired,
};
@ -108,7 +109,7 @@ class Video extends React.PureComponent {
volume: 0.5,
paused: true,
dragging: false,
containerWidth: false,
containerWidth: this.props.width,
fullscreen: false,
hovered: false,
muted: false,
@ -128,6 +129,7 @@ class Video extends React.PureComponent {
this.player = c;
if (c) {
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth);
this.setState({
containerWidth: c.offsetWidth,
});

View File

@ -0,0 +1,13 @@
import ready from '../mastodon/ready';
ready(() => {
const image = document.querySelector('img');
image.addEventListener('mouseenter', () => {
image.src = '/oops.gif';
});
image.addEventListener('mouseleave', () => {
image.src = '/oops.png';
});
});

View File

@ -12,3 +12,58 @@
}
}
}
.rich-formatting a,
.rich-formatting p a,
.rich-formatting li a,
.landing-page__short-description p a,
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);
text-decoration: underline;
&.mention {
text-decoration: none;
}
&.mention span {
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
&:hover,
&:focus,
&:active {
text-decoration: none;
}
&.status__content__spoiler-link {
color: $secondary-text-color;
text-decoration: none;
}
}
.status__content__read-more-button {
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
.getting-started__footer a {
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}

View File

@ -41,3 +41,34 @@
font-size: 16px;
}
}
@mixin search-popout() {
background: $simple-background-color;
border-radius: 4px;
padding: 10px 14px;
padding-bottom: 14px;
margin-top: 10px;
color: $light-text-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
h4 {
text-transform: uppercase;
color: $light-text-color;
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
}
li {
padding: 4px 0;
}
ul {
margin-bottom: 10px;
}
em {
font-weight: 500;
color: $inverted-text-color;
}
}

View File

@ -49,15 +49,9 @@ $small-breakpoint: 960px;
}
}
strong,
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: lighten($darker-text-color, 10%);
}
@ -796,7 +790,7 @@ $small-breakpoint: 960px;
width: 100%;
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
@ -846,14 +840,7 @@ $small-breakpoint: 960px;
}
strong {
display: inline;
margin: 0;
padding: 0;
font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
font-weight: 500;
color: lighten($darker-text-color, 10%);
}

View File

@ -100,12 +100,14 @@ body {
vertical-align: middle;
margin: 20px;
img {
display: block;
max-width: 470px;
width: 100%;
height: auto;
margin-top: -120px;
&__illustration {
img {
display: block;
max-width: 470px;
width: 100%;
height: auto;
margin-top: -120px;
}
}
h1 {

View File

@ -476,7 +476,7 @@
opacity: 0;
transition: opacity .1s ease;
input {
textarea {
background: transparent;
color: $secondary-text-color;
border: 0;
@ -638,7 +638,6 @@
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
padding-top: 2px;
color: $primary-text-color;
@ -662,6 +661,7 @@
p {
margin-bottom: 20px;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
@ -3056,14 +3056,41 @@ a.status-card.compact:hover {
display: block;
font-weight: 500;
margin-bottom: 10px;
}
.column-settings__hashtag-select {
.column-settings__hashtags {
.column-settings__row {
margin-bottom: 15px;
}
.column-select {
&__control {
@include search-input();
}
&__placeholder {
color: $dark-text-color;
padding-left: 2px;
font-size: 12px;
}
&__value-container {
padding-left: 6px;
}
&__multi-value {
background: lighten($ui-base-color, 8%);
&__remove {
cursor: pointer;
&:hover,
&:active,
&:focus {
background: lighten($ui-base-color, 12%);
color: lighten($darker-text-color, 4%);
}
}
}
&__multi-value__label,
@ -3071,9 +3098,42 @@ a.status-card.compact:hover {
color: $darker-text-color;
}
&__indicator-separator,
&__clear-indicator,
&__dropdown-indicator {
display: none;
cursor: pointer;
transition: none;
color: $dark-text-color;
&:hover,
&:active,
&:focus {
color: lighten($dark-text-color, 4%);
}
}
&__indicator-separator {
background-color: lighten($ui-base-color, 8%);
}
&__menu {
@include search-popout();
padding: 0;
background: $ui-secondary-color;
}
&__menu-list {
padding: 6px;
}
&__option {
color: $inverted-text-color;
border-radius: 4px;
font-size: 14px;
&--is-focused,
&--is-selected {
background: darken($ui-secondary-color, 10%);
}
}
}
}
@ -4867,34 +4927,7 @@ a.status-card.compact:hover {
}
.search-popout {
background: $simple-background-color;
border-radius: 4px;
padding: 10px 14px;
padding-bottom: 14px;
margin-top: 10px;
color: $light-text-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
h4 {
text-transform: uppercase;
color: $light-text-color;
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
}
li {
padding: 4px 0;
}
ul {
margin-bottom: 10px;
}
em {
font-weight: 500;
color: $inverted-text-color;
}
@include search-popout();
}
noscript {