Merge commit '121443c0fca383268b8022c048dd137994785aff' into glitch-soc/main
Conflicts: - `.rubocop_todo.yml`: Upstream regenerated this file, glitch-soc had a specific ignore.
This commit is contained in:
@ -22,6 +22,7 @@ export default class Story extends PureComponent {
|
||||
author: PropTypes.string,
|
||||
sharedTimes: PropTypes.number,
|
||||
thumbnail: PropTypes.string,
|
||||
thumbnailDescription: PropTypes.string,
|
||||
blurhash: PropTypes.string,
|
||||
expanded: PropTypes.bool,
|
||||
};
|
||||
@ -33,7 +34,7 @@ export default class Story extends PureComponent {
|
||||
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
|
||||
|
||||
render () {
|
||||
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, blurhash } = this.props;
|
||||
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props;
|
||||
|
||||
const { thumbnailLoaded } = this.state;
|
||||
|
||||
@ -49,7 +50,7 @@ export default class Story extends PureComponent {
|
||||
{thumbnail ? (
|
||||
<>
|
||||
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
|
||||
<img src={thumbnail} onLoad={this.handleImageLoad} alt='' role='presentation' />
|
||||
<img src={thumbnail} onLoad={this.handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} />
|
||||
</>
|
||||
) : <Skeleton />}
|
||||
</div>
|
||||
|
@ -67,6 +67,7 @@ class Links extends PureComponent {
|
||||
author={link.get('author_name')}
|
||||
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
|
||||
thumbnail={link.get('image')}
|
||||
thumbnailDescription={link.get('image_description')}
|
||||
blurhash={link.get('blurhash')}
|
||||
/>
|
||||
))}
|
||||
|
@ -0,0 +1,79 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import Button from 'mastodon/components/button';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
|
||||
const messages = defineMessages({
|
||||
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
|
||||
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
|
||||
});
|
||||
|
||||
const usesRenderer = (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='hashtag.counter_by_uses'
|
||||
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: <strong>{displayNumber}</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const peopleRenderer = (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='hashtag.counter_by_accounts'
|
||||
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: <strong>{displayNumber}</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const usesTodayRenderer = (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='hashtag.counter_by_uses_today'
|
||||
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: <strong>{displayNumber}</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
|
||||
if (!tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
|
||||
const dividingCircle = <span aria-hidden>{' · '}</span>;
|
||||
|
||||
return (
|
||||
<div className='hashtag-header'>
|
||||
<div className='hashtag-header__header'>
|
||||
<h1>#{tag.get('name')}</h1>
|
||||
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ShortNumber value={uses} renderer={usesRenderer} />
|
||||
{dividingCircle}
|
||||
<ShortNumber value={people} renderer={peopleRenderer} />
|
||||
{dividingCircle}
|
||||
<ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
HashtagHeader.propTypes = {
|
||||
tag: ImmutablePropTypes.map,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
intl: PropTypes.object,
|
||||
};
|
@ -1,9 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
@ -17,17 +16,12 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/t
|
||||
import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
|
||||
import { HashtagHeader } from './components/hashtag_header';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
|
||||
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
|
||||
tag: state.getIn(['tags', props.params.id]),
|
||||
@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent {
|
||||
hasUnread: PropTypes.bool,
|
||||
tag: ImmutablePropTypes.map,
|
||||
multiColumn: PropTypes.bool,
|
||||
intl: PropTypes.object,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
@ -188,27 +181,11 @@ class HashtagTimeline extends PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
|
||||
const { hasUnread, columnId, multiColumn, tag } = this.props;
|
||||
const { id, local } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let followButton;
|
||||
|
||||
if (tag) {
|
||||
const following = tag.get('following');
|
||||
|
||||
const classes = classNames('column-header__button', {
|
||||
active: following,
|
||||
});
|
||||
|
||||
followButton = (
|
||||
<button className={classes} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
|
||||
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
|
||||
<ColumnHeader
|
||||
@ -220,13 +197,14 @@ class HashtagTimeline extends PureComponent {
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={followButton}
|
||||
showBackButton
|
||||
>
|
||||
{columnId && <ColumnSettingsContainer columnId={columnId} />}
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
|
||||
alwaysPrepend
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`hashtag_timeline-${columnId}`}
|
||||
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
|
||||
@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent {
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(HashtagTimeline));
|
||||
export default connect(mapStateToProps)(HashtagTimeline);
|
||||
|
@ -13,7 +13,7 @@ import { openModal, closeModal } from 'mastodon/actions/modal';
|
||||
import api from 'mastodon/api';
|
||||
import Button from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { registrationsOpen } from 'mastodon/initial_state';
|
||||
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
|
||||
@ -332,7 +332,13 @@ class InteractionModal extends React.PureComponent {
|
||||
|
||||
let signupButton;
|
||||
|
||||
if (registrationsOpen) {
|
||||
if (sso_redirect) {
|
||||
signupButton = (
|
||||
<a href={sso_redirect} data-method='post' className='link-button'>
|
||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
||||
</a>
|
||||
);
|
||||
} else if (registrationsOpen) {
|
||||
signupButton = (
|
||||
<a href='/auth/sign_up' className='link-button'>
|
||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
||||
|
@ -167,7 +167,8 @@ export default class Card extends PureComponent {
|
||||
/>
|
||||
);
|
||||
|
||||
let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
|
||||
const thumbnailDescription = card.get('image_description');
|
||||
const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
|
||||
|
||||
let spoilerButton = (
|
||||
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
|
||||
|
@ -434,4 +434,4 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {
|
||||
forwardRef: true,
|
||||
})(injectIntl(FocalPointModal, { withRef: true }));
|
||||
})(injectIntl(FocalPointModal, { forwardRef: true }));
|
||||
|
@ -12,7 +12,7 @@ import { fetchServer } from 'mastodon/actions/server';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
||||
import { registrationsOpen, me } from 'mastodon/initial_state';
|
||||
import { registrationsOpen, me, sso_redirect } from 'mastodon/initial_state';
|
||||
|
||||
const Account = connect(state => ({
|
||||
account: state.getIn(['accounts', me]),
|
||||
@ -73,28 +73,35 @@ class Header extends PureComponent {
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let signupButton;
|
||||
|
||||
if (registrationsOpen) {
|
||||
signupButton = (
|
||||
<a href={signupUrl} className='button'>
|
||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
||||
</a>
|
||||
);
|
||||
if (sso_redirect) {
|
||||
content = (
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
)
|
||||
} else {
|
||||
signupButton = (
|
||||
<button className='button' onClick={openClosedRegistrationsModal}>
|
||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
||||
</button>
|
||||
let signupButton;
|
||||
|
||||
if (registrationsOpen) {
|
||||
signupButton = (
|
||||
<a href={signupUrl} className='button'>
|
||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
signupButton = (
|
||||
<button className='button' onClick={openClosedRegistrationsModal}>
|
||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
content = (
|
||||
<>
|
||||
{signupButton}
|
||||
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
content = (
|
||||
<>
|
||||
{signupButton}
|
||||
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -97,14 +97,7 @@ export default class ModalRoot extends PureComponent {
|
||||
|
||||
handleClose = (ignoreFocus = false) => {
|
||||
const { onClose } = this.props;
|
||||
let message = null;
|
||||
try {
|
||||
message = this._modal?.getWrappedInstance?.().getCloseConfirmationMessage?.();
|
||||
} catch (_) {
|
||||
// injectIntl defines `getWrappedInstance` but errors out if `withRef`
|
||||
// isn't set.
|
||||
// This would be much smoother with react-intl 3+ and `forwardRef`.
|
||||
}
|
||||
const message = this._modal?.getCloseConfirmationMessage?.();
|
||||
onClose(message, ignoreFocus);
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { registrationsOpen } from 'mastodon/initial_state';
|
||||
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const SignInBanner = () => {
|
||||
@ -19,6 +19,15 @@ const SignInBanner = () => {
|
||||
|
||||
const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
|
||||
|
||||
if (sso_redirect) {
|
||||
return (
|
||||
<div className='sign-in-banner'>
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
|
||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (registrationsOpen) {
|
||||
signupButton = (
|
||||
<a href={signupUrl} className='button button--block'>
|
||||
|
Reference in New Issue
Block a user