Merge commit '0ad66175bf59a34b03d9ab2347181548d07089ea' into glitch-soc/merge-upstream

Conflicts:
- `app/javascript/mastodon/features/compose/components/compose_form.jsx`:
  Upstream changed one import, close to a glitch-soc-only import to handle
  different max character counts.
  Ported upstream's change.
This commit is contained in:
Claire 2023-10-24 18:41:20 +02:00
commit b0c76eaadd
49 changed files with 242 additions and 226 deletions

View file

@ -9,6 +9,10 @@ module FormattingHelper
TextFormatter.new(text, options).to_s
end
def url_for_preview_card(preview_card)
preview_card.url
end
def extract_status_plain_text(status)
PlainTextFormatter.new(status.text, status.local?).to_s
end

View file

@ -1,7 +1,7 @@
import { render, fireEvent, screen } from '@testing-library/react';
import renderer from 'react-test-renderer';
import Button from '../button';
import { Button } from '../button';
describe('<Button />', () => {
it('renders a button element', () => {

View file

@ -15,7 +15,7 @@ import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { me } from '../initial_state';
import { Avatar } from './avatar';
import Button from './button';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { IconButton } from './icon_button';

View file

@ -1,58 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import classNames from 'classnames';
export default class Button extends PureComponent {
static propTypes = {
text: PropTypes.node,
type: PropTypes.string,
onClick: PropTypes.func,
disabled: PropTypes.bool,
block: PropTypes.bool,
secondary: PropTypes.bool,
className: PropTypes.string,
title: PropTypes.string,
children: PropTypes.node,
};
static defaultProps = {
type: 'button',
};
handleClick = (e) => {
if (!this.props.disabled && this.props.onClick) {
this.props.onClick(e);
}
};
setRef = (c) => {
this.node = c;
};
focus() {
this.node.focus();
}
render () {
const className = classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
});
return (
<button
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
title={this.props.title}
type={this.props.type}
>
{this.props.text || this.props.children}
</button>
);
}
}

View file

@ -0,0 +1,58 @@
import { useCallback } from 'react';
import classNames from 'classnames';
interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
block?: boolean;
secondary?: boolean;
text?: JSX.Element;
}
interface PropsWithChildren extends BaseProps {
text?: never;
}
interface PropsWithText extends BaseProps {
text: JSX.Element;
children: never;
}
type Props = PropsWithText | PropsWithChildren;
export const Button: React.FC<Props> = ({
text,
type = 'button',
onClick,
disabled,
block,
secondary,
className,
title,
children,
...props
}) => {
const handleClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(e) => {
if (!disabled && onClick) {
onClick(e);
}
},
[disabled, onClick],
);
return (
<button
className={classNames('button', className, {
'button-secondary': secondary,
'button--block': block,
})}
disabled={disabled}
onClick={handleClick}
title={title}
type={type}
{...props}
>
{text ?? children}
</button>
);
};

View file

@ -11,7 +11,7 @@ import { HotKeys } from 'react-hotkeys';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { withOptionalRouter, WithRouterPropTypes } from 'mastodon/utils/react_router';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router';
import Card from '../features/status/components/card';
// We use the component (and not the container) since we do not want
@ -113,7 +113,7 @@ class Status extends ImmutablePureComponent {
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
...WithRouterPropTypes,
...WithOptionalRouterPropTypes,
};
// Avoid checking props that are functions (and whose equality will always

View file

@ -11,7 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';

View file

@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { revealAccount } from 'mastodon/actions/accounts';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { domain } from 'mastodon/initial_state';
const mapDispatchToProps = (dispatch, { accountId }) => ({

View file

@ -14,7 +14,7 @@ import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/
import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import Button from '../../../components/button';
import { Button } from '../../../components/button';
import { maxChars } from '../../../initial_state';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import LanguageDropdown from '../containers/language_dropdown_container';

View file

@ -17,7 +17,7 @@ import {
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { Avatar } from 'mastodon/components/avatar';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import { ShortNumber } from 'mastodon/components/short_number';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';

View file

@ -45,24 +45,20 @@ class Statuses extends PureComponent {
const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />;
return (
<>
<DismissableBanner id='explore/statuses'>
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' />
</DismissableBanner>
<StatusList
trackScroll
timelineId='explore'
statusIds={statusIds}
scrollKey='explore-statuses'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
withCounters
/>
</>
<StatusList
trackScroll
prepend={<DismissableBanner id='explore/statuses'><FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' /></DismissableBanner>}
alwaysPrepend
timelineId='explore'
statusIds={statusIds}
scrollKey='explore-statuses'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
withCounters
/>
);
}

View file

@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { toServerSideType } from 'mastodon/utils/filters';
const mapStateToProps = (state, { filterId }) => ({

View file

@ -4,7 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { ShortNumber } from 'mastodon/components/short_number';
const messages = defineMessages({

View file

@ -11,7 +11,7 @@ import { throttle, escapeRegExp } from 'lodash';
import { openModal, closeModal } from 'mastodon/actions/modal';
import api from 'mastodon/api';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';

View file

@ -6,7 +6,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { changeListEditorTitle, submitListEditor } from 'mastodon/actions/lists';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
const messages = defineMessages({
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },

View file

@ -7,7 +7,7 @@ import { connect } from 'react-redux';
import { requestBrowserPermission } from 'mastodon/actions/notifications';
import { changeSetting } from 'mastodon/actions/settings';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';

View file

@ -7,7 +7,7 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import Option from './components/option';

View file

@ -11,7 +11,7 @@ import { createSelector } from 'reselect';
import Toggle from 'react-toggle';
import { fetchAccount } from 'mastodon/actions/accounts';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({

View file

@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import Option from './components/option';

View file

@ -7,7 +7,7 @@ import { OrderedSet } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import StatusCheckBox from 'mastodon/features/report/containers/status_check_box_container';

View file

@ -11,7 +11,7 @@ import {
muteAccount,
blockAccount,
} from 'mastodon/actions/accounts';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
const mapStateToProps = () => ({});

View file

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { followAccount } from 'mastodon/actions/accounts';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { IconButton } from 'mastodon/components/icon_button';
import Option from 'mastodon/features/report/components/option';
import { languages as preloadedLanguages } from 'mastodon/initial_state';

View file

@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { blockAccount } from '../../../actions/accounts';
import { closeModal } from '../../../actions/modal';
import { initReport } from '../../../actions/reports';
import Button from '../../../components/button';
import { Button } from '../../../components/button';
import { makeGetAccount } from '../../../selectors';
const makeMapStateToProps = () => {
@ -51,10 +51,6 @@ class BlockModal extends PureComponent {
intl: PropTypes.object.isRequired,
};
componentDidMount() {
this.button.focus();
}
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account);
@ -69,10 +65,6 @@ class BlockModal extends PureComponent {
this.props.onClose();
};
setRef = (c) => {
this.button = c;
};
render () {
const { account } = this.props;
@ -95,7 +87,7 @@ class BlockModal extends PureComponent {
<Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
</Button>
<Button onClick={this.handleClick} ref={this.setRef}>
<Button onClick={this.handleClick} autoFocus>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button>
</div>

View file

@ -16,7 +16,7 @@ import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdo
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar';
import Button from '../../../components/button';
import { Button } from '../../../components/button';
import { DisplayName } from '../../../components/display_name';
import { RelativeTimestamp } from '../../../components/relative_timestamp';
import StatusContent from '../../../components/status_content';
@ -55,10 +55,6 @@ class BoostModal extends ImmutablePureComponent {
...WithRouterPropTypes,
};
componentDidMount() {
this.button.focus();
}
handleReblog = () => {
this.props.onReblog(this.props.status, this.props.privacy);
this.props.onClose();
@ -76,10 +72,6 @@ class BoostModal extends ImmutablePureComponent {
return document.getElementsByClassName('modal-root__container')[0];
};
setRef = (c) => {
this.button = c;
};
render () {
const { status, privacy, intl } = this.props;
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
@ -133,7 +125,7 @@ class BoostModal extends ImmutablePureComponent {
onChange={this.props.onChangeBoostPrivacy}
/>
)}
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} autoFocus />
</div>
</div>
);

View file

@ -7,7 +7,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import Column from 'mastodon/components/column';
import { autoPlayGif } from 'mastodon/initial_state';

View file

@ -3,7 +3,7 @@ import { PureComponent } from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import Button from '../../../components/button';
import { Button } from '../../../components/button';
class ConfirmationModal extends PureComponent {
@ -22,10 +22,6 @@ class ConfirmationModal extends PureComponent {
closeWhenConfirm: true,
};
componentDidMount() {
this.button.focus();
}
handleClick = () => {
if (this.props.closeWhenConfirm) {
this.props.onClose();
@ -42,10 +38,6 @@ class ConfirmationModal extends PureComponent {
this.props.onClose();
};
setRef = (c) => {
this.button = c;
};
render () {
const { message, confirm, secondary } = this.props;
@ -62,7 +54,7 @@ class ConfirmationModal extends PureComponent {
{secondary !== undefined && (
<Button text={secondary} onClick={this.handleSecondary} className='confirmation-modal__secondary-button' />
)}
<Button text={confirm} onClick={this.handleClick} ref={this.setRef} />
<Button text={confirm} onClick={this.handleClick} autoFocus />
</div>
</div>
);

View file

@ -16,7 +16,7 @@ import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
// eslint-disable-next-line import/no-extraneous-dependencies
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
import Button from 'mastodon/components/button';
import { Button } from 'mastodon/components/button';
import { GIFV } from 'mastodon/components/gifv';
import { IconButton } from 'mastodon/components/icon_button';
import Audio from 'mastodon/features/audio';

View file

@ -10,7 +10,7 @@ import Toggle from 'react-toggle';
import { muteAccount } from '../../../actions/accounts';
import { closeModal } from '../../../actions/modal';
import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes';
import Button from '../../../components/button';
import { Button } from '../../../components/button';
const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
@ -63,10 +63,6 @@ class MuteModal extends PureComponent {
onChangeMuteDuration: PropTypes.func.isRequired,
};
componentDidMount() {
this.button.focus();
}
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
@ -76,10 +72,6 @@ class MuteModal extends PureComponent {
this.props.onClose();
};
setRef = (c) => {
this.button = c;
};
toggleNotifications = () => {
this.props.onToggleNotifications();
};
@ -134,7 +126,7 @@ class MuteModal extends PureComponent {
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
<Button onClick={this.handleClick} ref={this.setRef}>
<Button onClick={this.handleClick} autoFocus>
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
</Button>
</div>

View file

@ -163,13 +163,13 @@
"confirmation_modal.cancel": "Annulearje",
"confirmations.block.block_and_report": "Blokkearje en rapportearje",
"confirmations.block.confirm": "Blokkearje",
"confirmations.block.message": "Bisto wis datsto {name} blokkearje wolst?",
"confirmations.block.message": "Binne jo wis dat jo {name} blokkearje wolle?",
"confirmations.cancel_follow_request.confirm": "Fersyk annulearje",
"confirmations.cancel_follow_request.message": "Binne jo wis dat jo jo fersyk om {name} te folgjen annulearje wolle?",
"confirmations.delete.confirm": "Fuortsmite",
"confirmations.delete.message": "Binne jo wis dat jo dit berjocht fuortsmite wolle?",
"confirmations.delete_list.confirm": "Fuortsmite",
"confirmations.delete_list.message": "Bisto wis datsto dizze list foar permanint fuortsmite wolst?",
"confirmations.delete_list.message": "Binne jo wis dat jo dizze list foar permanint fuortsmite wolle?",
"confirmations.discard_edit_media.confirm": "Fuortsmite",
"confirmations.discard_edit_media.message": "Jo hawwe net-bewarre wizigingen yn de mediabeskriuwing of foarfertoaning, wolle jo dizze dochs fuortsmite?",
"confirmations.domain_block.confirm": "Alles fan dit domein blokkearje",
@ -177,16 +177,16 @@
"confirmations.edit.confirm": "Bewurkje",
"confirmations.edit.message": "Troch no te bewurkjen sil it berjocht dat jo no oan it skriuwen binne oerskreaun wurde. Wolle jo trochgean?",
"confirmations.logout.confirm": "Ofmelde",
"confirmations.logout.message": "Bisto wis datsto ôfmelde wolst?",
"confirmations.logout.message": "Binne jo wis dat jo ôfmelde wolle?",
"confirmations.mute.confirm": "Negearje",
"confirmations.mute.explanation": "Dit sil berjochten fan harren en berjochten dêrt se yn fermeld wurde ûnsichtber meitsje, mar se sille berjochten noch hieltyd sjen kinne en jo folgje kinne.",
"confirmations.mute.explanation": "Dit sil berjochten fan harren en berjochten dêrt se yn fermeld wurde ûnsichtber meitsje, mar se sille jo berjochten noch hieltyd sjen kinne en jo folgje kinne.",
"confirmations.mute.message": "Binne jo wis dat jo {name} negearje wolle?",
"confirmations.redraft.confirm": "Fuortsmite en opnij opstelle",
"confirmations.redraft.message": "Binne jo wis dat jo dit berjocht fuortsmite en opnij opstelle wolle? Favoriten en boosts geane dan ferlern en reaksjes op it oarspronklike berjocht reitsje jo kwyt.",
"confirmations.reply.confirm": "Reagearje",
"confirmations.reply.message": "Troch no te reagearjen sil it berjocht dat jo no oan it skriuwen binne oerskreaun wurde. Wolle jo trochgean?",
"confirmations.unfollow.confirm": "Net mear folgje",
"confirmations.unfollow.message": "Bisto wis datsto {name} net mear folgje wolst?",
"confirmations.unfollow.message": "Binne jo wis dat jo {name} net mear folgje wolle?",
"conversation.delete": "Petear fuortsmite",
"conversation.mark_as_read": "As lêzen markearje",
"conversation.open": "Petear toane",
@ -351,7 +351,7 @@
"keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "Skriuwer fermelde",
"keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "Dyn profyl iepenje",
"keyboard_shortcuts.my_profile": "Jo profyl iepenje",
"keyboard_shortcuts.notifications": "Meldingen toane",
"keyboard_shortcuts.open_media": "Media iepenje",
"keyboard_shortcuts.pinned": "Fêstsette berjochten toane",
@ -421,20 +421,20 @@
"navigation_bar.public_timeline": "Globale tiidline",
"navigation_bar.search": "Sykje",
"navigation_bar.security": "Befeiliging",
"not_signed_in_indicator.not_signed_in": "Do moatst oanmelde om tagong ta dizze ynformaasje te krijen.",
"not_signed_in_indicator.not_signed_in": "Jo moatte oanmelde om tagong ta dizze ynformaasje te krijen.",
"notification.admin.report": "{name} hat {target} rapportearre",
"notification.admin.sign_up": "{name} hat harren registrearre",
"notification.favourite": "{name} hat jo berjocht as favoryt markearre",
"notification.follow": "{name} folget dy",
"notification.follow_request": "{name} hat dy in folchfersyk stjoerd",
"notification.mention": "{name} hat dy fermeld",
"notification.own_poll": "Dyn poll is beëinige",
"notification.own_poll": "Jo poll is beëinige",
"notification.poll": "In enkête dêrt jo yn stimd hawwe is beëinige",
"notification.reblog": "{name} hat jo berjocht boost",
"notification.status": "{name} hat in berjocht pleatst",
"notification.update": "{name} hat in berjocht bewurke",
"notifications.clear": "Meldingen wiskje",
"notifications.clear_confirmation": "Bisto wis datsto al dyn meldingen permanint fuortsmite wolst?",
"notifications.clear_confirmation": "Binne jo wis dat jo al jo meldingen permanint fuortsmite wolle?",
"notifications.column_settings.admin.report": "Nije rapportaazjes:",
"notifications.column_settings.admin.sign_up": "Nije registraasjes:",
"notifications.column_settings.alert": "Desktopmeldingen",

View file

@ -204,7 +204,7 @@
"dismissable_banner.explore_links": "이 소식들은 오늘 소셜 웹에서 가장 많이 공유된 내용들입니다. 새 소식을 더 많은 사람들이 공유할수록 높은 순위가 됩니다.",
"dismissable_banner.explore_statuses": "이 게시물들은 오늘 소셜 웹에서 호응을 얻고 있는 게시물들입니다. 부스트와 관심을 받는 새로운 글들이 높은 순위가 됩니다.",
"dismissable_banner.explore_tags": "이 해시태그들은 이 서버와 분산화된 네트워크의 다른 서버에서 사람들의 인기를 끌고 있는 것들입니다.",
"dismissable_banner.public_timeline": "이것들은 {domain}에 있는 사람들이 팔로우한 사람들의 최신 게시물들입니다.",
"dismissable_banner.public_timeline": "{domain} 사람들이 팔로우하는 소셜 웹 사람들의 최신 공개 게시물입니다.",
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.",
"embed.preview": "이렇게 표시됩니다:",
"emoji_button.activity": "활동",

View file

@ -153,7 +153,7 @@
"compose_form.publish": "Legg ut",
"compose_form.publish_form": "Legg ut",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Gøym",
"compose_form.save_changes": "Lagre endringar",
"compose_form.sensitive.hide": "{count, plural, one {Marker mediet som ømtolig} other {Marker media som ømtolige}}",
"compose_form.sensitive.marked": "{count, plural, one {Mediet er markert som ømtolig} other {Media er markerte som ømtolige}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Mediet er ikkje markert som ømtolig} other {Media er ikkje markerte som ømtolige}}",
@ -171,7 +171,7 @@
"confirmations.delete_list.confirm": "Slett",
"confirmations.delete_list.message": "Er du sikker på at du vil sletta denne lista for alltid?",
"confirmations.discard_edit_media.confirm": "Forkast",
"confirmations.discard_edit_media.message": "Du har ulagra endringar i mediaskildringa eller førehandsvisinga. Vil du forkaste dei likevel?",
"confirmations.discard_edit_media.message": "Du har ulagra endringar i mediaskildringa eller førehandsvisinga. Vil du forkasta dei likevel?",
"confirmations.domain_block.confirm": "Skjul alt frå domenet",
"confirmations.domain_block.message": "Er du heilt, heilt sikker på at du vil skjula heile {domain}? I dei fleste tilfelle er det godt nok og føretrekt med nokre få målretta blokkeringar eller målbindingar. Du kjem ikkje til å sjå innhald frå domenet i fødererte tidsliner eller i varsla dine. Fylgjarane dine frå domenet vert fjerna.",
"confirmations.edit.confirm": "Rediger",
@ -285,7 +285,7 @@
"footer.privacy_policy": "Personvernsreglar",
"footer.source_code": "Vis kjeldekode",
"footer.status": "Status",
"generic.saved": "Gøymt",
"generic.saved": "Lagra",
"getting_started.heading": "Kom i gang",
"hashtag.column_header.tag_mode.all": "og {additional}",
"hashtag.column_header.tag_mode.any": "eller {additional}",
@ -314,7 +314,7 @@
"home.pending_critical_update.link": "Sjå oppdateringar",
"home.pending_critical_update.title": "Kritisk sikkerheitsoppdatering er tilgjengeleg!",
"home.show_announcements": "Vis kunngjeringar",
"interaction_modal.description.favourite": "Med ein konto på Mastodon kan du favorittmerkja dette innlegget for å visa forfattaren at du set pris på det, og for å lagra det til seinare.",
"interaction_modal.description.favourite": "Med ein konto på Mastodon kan du favorittmerka dette innlegget for å visa forfattaren at du set pris på det, og for å lagra det til seinare.",
"interaction_modal.description.follow": "Med ein konto på Mastodon kan du fylgja {name} for å sjå innlegga deira i din heimestraum.",
"interaction_modal.description.reblog": "Med ein konto på Mastodon kan du framheva dette innlegget for å dela det med dine eigne fylgjarar.",
"interaction_modal.description.reply": "Med ein konto på Mastodon kan du svara på dette innlegget.",
@ -673,7 +673,7 @@
"status.unmute_conversation": "Opphev målbinding av samtalen",
"status.unpin": "Løys frå profil",
"subscribed_languages.lead": "Kun innlegg på valde språk vil bli dukke opp i heimestraumen din og i listene dine etter denne endringa. For å motta innlegg på alle språk, la vere å velje nokon.",
"subscribed_languages.save": "Gøym",
"subscribed_languages.save": "Lagre endringar",
"subscribed_languages.target": "Endre abonnerte språk for {target}",
"tabs_bar.home": "Heim",
"tabs_bar.notifications": "Varsel",

View file

@ -303,7 +303,7 @@
"hashtag.unfollow": "Отпрати хеш ознаку",
"hashtags.and_other": "…и {count, plural, one {још #} few {још #}other {још #}}",
"home.actions.go_to_explore": "Погледате шта је у тренду",
"home.actions.go_to_suggestions": "Пронађeте људе које бисте пратили",
"home.actions.go_to_suggestions": "Пронађете људе које бисте пратили",
"home.column_settings.basic": "Основна",
"home.column_settings.show_reblogs": "Прикажи подржавања",
"home.column_settings.show_replies": "Прикажи одговоре",

View file

@ -114,7 +114,7 @@
"column.directory": "瀏覽個人檔案",
"column.domain_blocks": "已封鎖網域",
"column.favourites": "最愛",
"column.firehose": "即時河道",
"column.firehose": "即時內容",
"column.follow_requests": "跟隨請求",
"column.home": "首頁",
"column.lists": "列表",
@ -629,7 +629,7 @@
"status.edit": "編輯",
"status.edited": "編輯於 {date}",
"status.edited_x_times": "已編輯 {count, plural, one {{count} 次} other {{count} 次}}",
"status.embed": "內嵌",
"status.embed": "內嵌嘟文",
"status.favourite": "最愛",
"status.filter": "過濾此嘟文",
"status.filtered": "已過濾",

View file

@ -49,6 +49,7 @@ export function withOptionalRouter(Component) {
C.displayName = displayName;
C.WrappedComponent = Component;
C.propTypes = {
...Component.propTypes,
wrappedComponentRef: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,

View file

@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
return if creator.nil?
@ -28,6 +28,8 @@ class ActivityPub::LinkedDataSignature
to_be_verified = options_hash + document_hash
creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
rescue OpenSSL::PKey::RSAError
false
end
def sign!(creator, sign_with: nil)

View file

@ -55,7 +55,7 @@ class PreviewCard < ApplicationRecord
has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false
validates :url, presence: true, uniqueness: true
validates :url, presence: true, uniqueness: true, url: true
validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES
validates_attachment_size :image, less_than: LIMIT
remotable_attachment :image, LIMIT

View file

@ -4,7 +4,7 @@
.batch-table__row__content.pending-account
.pending-account__header
= link_to preview_card.title, preview_card.url
= link_to preview_card.title, url_for_preview_card(preview_card)
%br/

View file

@ -1,39 +0,0 @@
{
"ignored_warnings": [
{
"warning_type": "Cross-Site Scripting",
"warning_code": 4,
"fingerprint": "cd5cfd7f40037fbfa753e494d7129df16e358bfc43ef0da3febafbf4ee1ed3ac",
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in `link_to` href",
"file": "app/views/admin/trends/links/_preview_card.html.haml",
"line": 7,
"link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to((Unresolved Model).new.title, (Unresolved Model).new.url)",
"render_path": [
{
"type": "template",
"name": "admin/trends/links/index",
"line": 49,
"file": "app/views/admin/trends/links/index.html.haml",
"rendered": {
"name": "admin/trends/links/_preview_card",
"file": "app/views/admin/trends/links/_preview_card.html.haml"
}
}
],
"location": {
"type": "template",
"template": "admin/trends/links/_preview_card"
},
"user_input": "(Unresolved Model).new.url",
"confidence": "Weak",
"cwe_id": [
79
],
"note": ""
}
],
"updated": "2023-07-12 11:20:51 -0400",
"brakeman_version": "6.0.0"
}

View file

@ -1,3 +1,5 @@
---
:skip_checks:
- CheckPermitAttributes
:url_safe_methods:
- url_for_preview_card

View file

@ -36,7 +36,7 @@ zh-CN:
status:
attributes:
reblog:
taken: 被转嘟过
taken: 被转嘟过
user:
attributes:
email:

View file

@ -13,7 +13,7 @@ fy:
locked: Jo account is blokkearre.
not_found_in_database: "%{authentication_keys} of wachtwurd ûnjildich."
pending: Jo account moat noch hieltyd beoardiele wurde.
timeout: Dyn sesje is ferrûn, meld dy opnij oan.
timeout: Jo sesje is ferrûn. Meld jo opnij oan om troch te gean.
unauthenticated: Jo moatte oanmelde of registrearje.
unconfirmed: Jo moatte earst jo account befêstigje.
mailer:

View file

@ -9,7 +9,7 @@ zh-CN:
already_authenticated: 你已登录。
inactive: 你还没有激活账户。
invalid: "%{authentication_keys} 无效或密码错误。"
last_attempt: 你只有最后一次尝试机会,若未通过,号将被锁定。
last_attempt: 你只有最后一次尝试机会,若未通过,号将被锁定。
locked: 你的账户已被锁定。
not_found_in_database: "%{authentication_keys}或密码错误。"
pending: 你的账号仍在审核中。
@ -85,7 +85,7 @@ zh-CN:
send_instructions: 如果你的电子邮件地址存在于我们的数据库中,你将在几分钟后收到一个密码恢复链接。如果你没有收到这封邮件,请检查你邮箱的垃圾箱。
send_paranoid_instructions: 如果你的电子邮件地址存在于我们的数据库中,你将在几分钟后收到一个密码恢复链接。如果你没有收到这封邮件,请检查你邮箱的垃圾箱。
updated: 你的密码已成功修改,现在你已登录。
updated_not_active: 你的密码已成功修改。
updated_not_active: 你的密码已修改成功
registrations:
destroyed: 再见!你的账户已成功注销。我们希望很快可以再见到你。
signed_up: 欢迎!你已成功注册。

View file

@ -43,7 +43,7 @@ fy:
new: Nije tapassing
scopes: Tastimmingen
show: Toane
title: Dyn tapassingen
title: Jo tapassingen
new:
title: Nije tapassing
show:
@ -60,7 +60,7 @@ fy:
error:
title: Der is in flater bard
new:
prompt_html: "%{client_name} hat tastimming nedich om tagong te krijen ta dyn account. It giet om in tapassing fan in tredde partij.<strong>Asto dit net fertroust, moatsto gjin tastimming jaan.</strong>"
prompt_html: "%{client_name} hat tastimming nedich om tagong te krijen ta jo account. It giet om in tapassing fan in tredde partij.<strong>As jo dit net fertrouwe, moatte jo gjin tastimming jaan.</strong>"
review_permissions: Tastimmingen beoardiele
title: Autorisaasje fereaske
show:
@ -69,15 +69,15 @@ fy:
buttons:
revoke: Ynlûke
confirmations:
revoke: Bisto wis?
revoke: Binne jo wis?
index:
authorized_at: Autorisearre op %{date}
description_html: Dit binne tapassingen dyt tagong hawwe ta dyn account fia de API. As der tapassingen tusken steane dytsto net werkenst of in tapassing harren misdraacht, kinsto de tagongsrjochten fan de tapassing ynlûke.
description_html: Dit binne tapassingen dyt fia de API tagong hawwe ta jo account. As der tapassingen tusken steane dyt jo net werkenne of in tapassing harren misdraacht, kinne jo de tagongsrjochten fan de tapassing ynlûke.
last_used_at: Lêst brûkt op %{date}
never_used: Nea brûkt
scopes: Tastimmingen
superapp: Yntern
title: Dyn autorisearre tapassingen
title: Jo autorisearre tapassingen
errors:
messages:
access_denied: De boarne-eigener of autorisaasjeserver hat it fersyk wegere.
@ -165,22 +165,22 @@ fy:
admin:write:reports: moderaasjemaatregelen nimme yn rapportaazjes
crypto: ein-ta-ein-fersifering brûke
follow: relaasjes tusken accounts bewurkje
push: dyn pushmeldingen ûntfange
read: alle gegevens fan dyn account lêze
push: jo pushmeldingen ûntfange
read: alle gegevens fan jo account lêze
read:accounts: accountynformaasje besjen
read:blocks: dyn blokkearre brûkers besjen
read:bookmarks: dyn blêdwizers besjen
read:blocks: jo blokkearre brûkers besjen
read:bookmarks: jo blêdwizers besjen
read:favourites: jo favoriten besjen
read:filters: dyn filters besjen
read:filters: jo filters besjen
read:follows: de accounts dytsto folgest besjen
read:lists: dyn listen besjen
read:mutes: dyn negearre brûkers besjen
read:notifications: dyn meldingen besjen
read:reports: dyn rapportearre berjochten besjen
read:search: út dyn namme sykje
read:lists: jo listen besjen
read:mutes: jo negearre brûkers besjen
read:notifications: jo meldingen besjen
read:reports: jo rapportearre berjochten besjen
read:search: út jo namme sykje
read:statuses: alle berjochten besjen
write: alle gegevens fan dyn account bewurkje
write:accounts: dyn profyl bewurkje
write: alle gegevens fan jo account bewurkje
write:accounts: jo profyl bewurkje
write:blocks: accounts en domeinen blokkearje
write:bookmarks: berjochten oan blêdwizers tafoegje
write:conversations: petearen negearre en fuortsmite

View file

@ -570,7 +570,7 @@ nn:
enabled: Skrudd på
inbox_url: Overførings-URL
pending: Avventer overgangens godkjenning
save_and_enable: Lagr og slå på
save_and_enable: Lagre og slå på
setup: Sett opp en overgangsforbindelse
signatures_not_enabled: Overgangar fungerer ikkje så lenge sikker- eller kvitlistingsmodus er aktivert
status: Status
@ -1280,7 +1280,7 @@ nn:
deselect: Vel ingen
none: Ingen
order_by: Sorter etter
save_changes: Lagr endringar
save_changes: Lagre endringar
select_all_matching_items:
one: Vel %{count} element som passar til søket ditt.
other: Vel %{count} element som passar til søket ditt.

View file

@ -86,7 +86,7 @@ fy:
media_cache_retention_period: Mediabestannen dyt fan oare servers download binne wurde nei it opjûne oantal dagen fuortsmiten en wurde op fersyk opnij download.
peers_api_enabled: In list mei domeinnammen, dêrt dizze server yn fediverse kontakt hân mei hat. Hjir wurdt gjin data dield, oft jo mei in bepaalde server federearrest, mar alinnich, dat jo server dat wit. Dit wurdt foar tsjinsten brûkt, dyt statistiken oer federaasje yn algemiene sin sammelet.
profile_directory: De brûkersgids befettet in list fan alle brûkers dy¥t derfoar keazen hawwe om ûntdekt wurde te kinnen.
require_invite_text: Meitsje it ynfoljen fan "Wêrom wolle jo jo hjir registrearje?" ferplicht yn stee fan opsjoneel, wanneart registraasjes hânmjittich goedkard wurde moatte
require_invite_text: Meitsje it ynfoljen fan Wêrom wolle jo jo hjir registrearje? ferplicht yn stee fan opsjoneel, wanneart registraasjes hânmjittich goedkard wurde moatte
site_contact_email: Hoe minsken jo berikke kinne foar juridyske fragen of stipe.
site_contact_username: Hoe minsken jo op Mastodon berikke kinne.
site_extended_description: Alle oanfoljende ynformaasje dyt nuttich wêze kin foar besikers en jo brûkers. Kin opmakke wurde mei Markdown.

View file

@ -40,7 +40,7 @@ zh-CN:
current_email: 当前的电子邮箱
label: 更改电子邮箱
new_email: 新的电子邮箱
submit: 更改电子邮件地址
submit: 更改电子邮
title: 更改 %{username} 的电子邮箱
change_role:
changed_msg: 已成功更改角色!
@ -68,8 +68,8 @@ zh-CN:
enable_sign_in_token_auth: 启用电子邮件令牌认证
enabled: 已启用
enabled_msg: 成功解冻 %{username} 的账号
followers: 关注者
follows: 正在关注
followers: 粉丝
follows: 关注
header: 个人资料页横幅图片
inbox_url: 收件箱InboxURL
invite_request_text: 加入理由

View file

@ -34,6 +34,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end
end
context 'when local account record is missing a public key' do
let(:raw_signature) do
{
'creator' => 'http://example.com/alice',
'created' => '2017-09-23T20:21:34Z',
}
end
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) }
before do
# Ensure signature is computed with the old key
signature
# Unset key
old_key = sender.public_key
sender.update!(private_key: '', public_key: '')
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
sender.update!(public_key: old_key)
sender
end
end
it 'fetches key and returns creator' do
expect(subject.verify_actor!).to eq sender
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
end
end
context 'when signature is missing' do
let(:signature) { nil }

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'rails_helper'
describe PreviewCard do
describe 'validations' do
describe 'urls' do
it 'allows http schemes' do
record = described_class.new(url: 'http://example.host/path')
expect(record).to be_valid
end
it 'allows https schemes' do
record = described_class.new(url: 'https://example.host/path')
expect(record).to be_valid
end
it 'does not allow javascript: schemes' do
record = described_class.new(url: 'javascript:alert()')
expect(record).to_not be_valid
expect(record).to model_have_error_on_field(:url)
end
end
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'admin/trends/links/_preview_card.html.haml' do
it 'correctly escapes user supplied url values' do
form = instance_double(ActionView::Helpers::FormHelper, check_box: nil)
trend = PreviewCardTrend.new(allowed: false)
preview_card = Fabricate.build(
:preview_card,
url: 'https://host.example/path?query=<script>',
trend: trend,
title: 'Fun'
)
render partial: 'admin/trends/links/preview_card', locals: { preview_card: preview_card, f: form }
expect(rendered).to include('<a href="https://host.example/path?query=&lt;script&gt;">Fun</a>')
end
end