mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-06 18:01:05 +00:00
Compare commits
15 Commits
d5e164a0c3
...
8c1702a846
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8c1702a846 | ||
![]() |
14cb5ff881 | ||
![]() |
bc952ebde9 | ||
![]() |
c1542643f5 | ||
![]() |
7217edfd7f | ||
![]() |
43f9ae9ee4 | ||
![]() |
e3a69530a7 | ||
![]() |
25309b92ad | ||
![]() |
1f3e5b9324 | ||
![]() |
941e86e902 | ||
![]() |
7bdbf0c25b | ||
![]() |
f2a151805f | ||
![]() |
46e3c18071 | ||
![]() |
6c38ab5b6b | ||
![]() |
5e50840986 |
|
@ -8,6 +8,7 @@ const meta = {
|
||||||
component: Button,
|
component: Button,
|
||||||
args: {
|
args: {
|
||||||
secondary: false,
|
secondary: false,
|
||||||
|
plain: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
dangerous: false,
|
dangerous: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
@ -57,6 +58,14 @@ export const Secondary: Story = {
|
||||||
play: buttonTest,
|
play: buttonTest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Plain: Story = {
|
||||||
|
args: {
|
||||||
|
plain: true,
|
||||||
|
children: 'Plain button',
|
||||||
|
},
|
||||||
|
play: buttonTest,
|
||||||
|
};
|
||||||
|
|
||||||
export const Compact: Story = {
|
export const Compact: Story = {
|
||||||
args: {
|
args: {
|
||||||
compact: true,
|
compact: true,
|
||||||
|
@ -101,6 +110,14 @@ export const SecondaryDisabled: Story = {
|
||||||
play: disabledButtonTest,
|
play: disabledButtonTest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PlainDisabled: Story = {
|
||||||
|
args: {
|
||||||
|
...Plain.args,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
play: disabledButtonTest,
|
||||||
|
};
|
||||||
|
|
||||||
const loadingButtonTest: Story['play'] = async ({
|
const loadingButtonTest: Story['play'] = async ({
|
||||||
args,
|
args,
|
||||||
canvas,
|
canvas,
|
||||||
|
|
|
@ -9,6 +9,7 @@ interface BaseProps
|
||||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
|
plain?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
dangerous?: boolean;
|
dangerous?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
@ -35,6 +36,7 @@ export const Button: React.FC<Props> = ({
|
||||||
disabled,
|
disabled,
|
||||||
block,
|
block,
|
||||||
secondary,
|
secondary,
|
||||||
|
plain,
|
||||||
compact,
|
compact,
|
||||||
dangerous,
|
dangerous,
|
||||||
loading,
|
loading,
|
||||||
|
@ -62,6 +64,7 @@ export const Button: React.FC<Props> = ({
|
||||||
<button
|
<button
|
||||||
className={classNames('button', className, {
|
className={classNames('button', className, {
|
||||||
'button-secondary': secondary,
|
'button-secondary': secondary,
|
||||||
|
'button--plain': plain,
|
||||||
'button--compact': compact,
|
'button--compact': compact,
|
||||||
'button--block': block,
|
'button--block': block,
|
||||||
'button--dangerous': dangerous,
|
'button--dangerous': dangerous,
|
||||||
|
|
|
@ -10,6 +10,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
|
import { translateStatusSuccess } from 'mastodon/actions/statuses';
|
||||||
|
import { undoStatusTranslation } from 'mastodon/actions/statuses';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { Poll } from 'mastodon/components/poll';
|
import { Poll } from 'mastodon/components/poll';
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||||
|
@ -19,6 +21,8 @@ import { isModernEmojiEnabled } from '../utils/environment';
|
||||||
|
|
||||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
|
const supportsTranslator = 'Translator' in globalThis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {any} status
|
* @param {any} status
|
||||||
|
@ -69,10 +73,18 @@ class TranslateButton extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
languages: supportsTranslator ? new Map() : state.getIn(['server', 'translationLanguages', 'items']),
|
||||||
});
|
});
|
||||||
|
|
||||||
class StatusContent extends PureComponent {
|
class StatusContent extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showTranslateButton: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
identity: identityContextPropShape,
|
identity: identityContextPropShape,
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
|
@ -176,8 +188,36 @@ class StatusContent extends PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
async componentDidMount () {
|
||||||
this._updateStatusLinks();
|
this._updateStatusLinks();
|
||||||
|
|
||||||
|
const { status, intl, languages } = this.props;
|
||||||
|
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||||
|
const targetLanguages = languages?.get(status.get('language') || 'und');
|
||||||
|
|
||||||
|
// The Translator API translates all locally on the client, so private and direct toots are fine to translate.
|
||||||
|
const allowedVisibilities = supportsTranslator ? ['public', 'unlisted', 'private', 'direct'] : ['public', 'unlisted'];
|
||||||
|
const shouldAttemptTranslate =
|
||||||
|
this.props.onTranslate &&
|
||||||
|
this.props.identity.signedIn &&
|
||||||
|
allowedVisibilities.includes(status.get('visibility')) &&
|
||||||
|
status.get('search_index').trim().length > 0;
|
||||||
|
|
||||||
|
if (!shouldAttemptTranslate) return;
|
||||||
|
|
||||||
|
let available = false;
|
||||||
|
if (supportsTranslator) {
|
||||||
|
available = (await Translator.availability({
|
||||||
|
sourceLanguage: status.get('language'),
|
||||||
|
targetLanguage: contentLocale,
|
||||||
|
})) !== 'unavailable';
|
||||||
|
} else {
|
||||||
|
available = targetLanguages?.includes(contentLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (available) {
|
||||||
|
this.setState({ showTranslateButton: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
|
@ -227,8 +267,37 @@ class StatusContent extends PureComponent {
|
||||||
this.startXY = null;
|
this.startXY = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleTranslate = () => {
|
handleTranslate = async () => {
|
||||||
this.props.onTranslate();
|
if (!supportsTranslator) {
|
||||||
|
this.props.onTranslate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { intl, status, statusContent } = this.props;
|
||||||
|
|
||||||
|
if (status.get('translation')) {
|
||||||
|
this.props.dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLanguage = status.get('language');
|
||||||
|
const targetLanguage = intl.locale.replace(/[_-].*/, '');
|
||||||
|
try {
|
||||||
|
const translator = await Translator.create({
|
||||||
|
sourceLanguage,
|
||||||
|
targetLanguage,
|
||||||
|
});
|
||||||
|
const translatedText = await translator.translate(statusContent);
|
||||||
|
const translation = {
|
||||||
|
content: translatedText,
|
||||||
|
provider: 'Translator API',
|
||||||
|
detected_source_language: sourceLanguage,
|
||||||
|
language: targetLanguage,
|
||||||
|
};
|
||||||
|
this.props.dispatch(translateStatusSuccess(status.get('id'), translation));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setRef = (c) => {
|
setRef = (c) => {
|
||||||
|
@ -236,12 +305,9 @@ class StatusContent extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, statusContent } = this.props;
|
const { status, statusContent } = this.props;
|
||||||
|
|
||||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
|
||||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
|
||||||
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
|
||||||
|
|
||||||
const content = statusContent ?? getStatusContent(status);
|
const content = statusContent ?? getStatusContent(status);
|
||||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
@ -256,7 +322,7 @@ class StatusContent extends PureComponent {
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const translateButton = renderTranslate && (
|
const translateButton = this.state.showTranslateButton && (
|
||||||
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
|
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -297,8 +363,7 @@ class StatusContent extends PureComponent {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(withIdentity(connect(mapStateToProps)(injectIntl(StatusContent))));
|
export default withRouter(withIdentity(connect(mapStateToProps)(injectIntl(StatusContent))));
|
||||||
|
|
|
@ -201,6 +201,41 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.button--plain {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
background: transparent;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
// The button has no outline, so we use negative margin to
|
||||||
|
// visually align its label with its surroundings while maintaining
|
||||||
|
// a generous click target
|
||||||
|
margin-inline: -6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent;
|
||||||
|
color: lighten($highlight-text-color, 4%);
|
||||||
|
background-color: transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled,
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: transparent;
|
||||||
|
color: $ui-button-disabled-color;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent;
|
||||||
|
color: $ui-button-disabled-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.button-tertiary {
|
&.button-tertiary {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 17px;
|
padding: 6px 17px;
|
||||||
|
|
|
@ -232,6 +232,15 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
canQuote: {
|
canQuote: {
|
||||||
automaticApproval: approved_uris,
|
automaticApproval: approved_uris,
|
||||||
},
|
},
|
||||||
|
canReply: {
|
||||||
|
always: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
},
|
||||||
|
canLike: {
|
||||||
|
always: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
},
|
||||||
|
canAnnounce: {
|
||||||
|
always: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11957,8 +11957,8 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"sass@npm:^1.62.1":
|
"sass@npm:^1.62.1":
|
||||||
version: 1.91.0
|
version: 1.92.0
|
||||||
resolution: "sass@npm:1.91.0"
|
resolution: "sass@npm:1.92.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@parcel/watcher": "npm:^2.4.1"
|
"@parcel/watcher": "npm:^2.4.1"
|
||||||
chokidar: "npm:^4.0.0"
|
chokidar: "npm:^4.0.0"
|
||||||
|
@ -11969,7 +11969,7 @@ __metadata:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
sass: sass.js
|
sass: sass.js
|
||||||
checksum: 10c0/5be1c98f7a618cb5f90b62f63d2aa0f78f9bf369c93ec7cd9880752a26b0ead19aa63cc341e8a26ce6c74d080baa5705f1685dff52fe6a3f28a7828ae50182b6
|
checksum: 10c0/bdff9fa6988620e2a81962efdd016e3894d19934cfadc105cf41db767f59dd47afd8ff32840e612ef700cb67e19d9e83c108f1724eb8f0bef56c4877dbe6f14d
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user