diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb index 6256bed64c..29ab920383 100644 --- a/app/controllers/api/v1/apps/credentials_controller.rb +++ b/app/controllers/api/v1/apps/credentials_controller.rb @@ -4,6 +4,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController def show return doorkeeper_render_error unless valid_doorkeeper_token? - render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key client_id scopes) + render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer end end diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index 97177547a2..50feaf1854 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -5,7 +5,7 @@ class Api::V1::AppsController < Api::BaseController def create @app = Doorkeeper::Application.create!(application_options) - render json: @app, serializer: REST::ApplicationSerializer + render json: @app, serializer: REST::CredentialApplicationSerializer end private @@ -24,6 +24,6 @@ class Api::V1::AppsController < Api::BaseController end def app_params - params.permit(:client_name, :redirect_uris, :scopes, :website) + params.permit(:client_name, :scopes, :website, :redirect_uris, redirect_uris: []) end end diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx index 0177812480..210ec396fa 100644 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -14,8 +14,10 @@ import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; + import { useAppHistory } from './router'; const messages = defineMessages({ @@ -51,12 +53,8 @@ BackButton.propTypes = { }; class ColumnHeader extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, title: PropTypes.node, icon: PropTypes.string, @@ -171,7 +169,7 @@ class ColumnHeader extends PureComponent { ); } - if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) { + if (this.props.identity.signedIn && (children || (multiColumn && this.props.onPin))) { collapseButton = ( } + {!showResults && } {!showResults && <> · } {showResults && !this.props.disabled && <> · } {votesCount} @@ -247,4 +244,4 @@ class Poll extends ImmutablePureComponent { } -export default injectIntl(Poll); +export default injectIntl(withIdentity(Poll)); diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index f27719a6b9..3dfe403987 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -21,6 +21,7 @@ import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -67,12 +68,8 @@ const messages = defineMessages({ }); class StatusActionBar extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, onReply: PropTypes.func, onFavourite: PropTypes.func, @@ -108,7 +105,7 @@ class StatusActionBar extends ImmutablePureComponent { ]; handleReplyClick = () => { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { this.props.onReply(this.props.status, this.props.history); @@ -124,7 +121,7 @@ class StatusActionBar extends ImmutablePureComponent { }; handleFavouriteClick = (e) => { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { this.props.onFavourite(this.props.status, e); @@ -134,7 +131,7 @@ class StatusActionBar extends ImmutablePureComponent { }; handleReblogClick = e => { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { this.props.onReblog(this.props.status, e); @@ -210,7 +207,7 @@ class StatusActionBar extends ImmutablePureComponent { render () { const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props; - const { permissions, signedIn } = this.context.identity; + const { permissions, signedIn } = this.props.identity; const mutingConversation = status.get('muted'); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -359,4 +356,4 @@ class StatusActionBar extends ImmutablePureComponent { } -export default withRouter(injectIntl(StatusActionBar)); +export default withRouter(withIdentity(injectIntl(StatusActionBar))); diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index f5d5ccc70a..24da69cdf2 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -15,6 +15,7 @@ import LinkIcon from '@/material-icons/400-24px/link.svg?react'; import MovieIcon from '@/material-icons/400-24px/movie.svg?react'; import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; @@ -126,12 +127,8 @@ const mapStateToProps = state => ({ }); class StatusContent extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, statusContent: PropTypes.string, expanded: PropTypes.bool, @@ -349,7 +346,7 @@ class StatusContent extends PureComponent { const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const contentLocale = intl.locale.replace(/[_-].*/, ''); const targetLanguages = this.props.languages?.get(status.get('language') || 'und'); - const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); + 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 = { __html: statusContent ?? getStatusContent(status) }; const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') }; @@ -503,4 +500,4 @@ class StatusContent extends PureComponent { } -export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent))); +export default withRouter(withIdentity(connect(mapStateToProps)(injectIntl(StatusContent)))); diff --git a/app/javascript/flavours/glitch/containers/mastodon.jsx b/app/javascript/flavours/glitch/containers/mastodon.jsx index 1704336f2c..c0f63b95b4 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.jsx +++ b/app/javascript/flavours/glitch/containers/mastodon.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { Helmet } from 'react-helmet'; @@ -15,6 +14,7 @@ import { connectUserStream } from 'flavours/glitch/actions/streaming'; import ErrorBoundary from 'flavours/glitch/components/error_boundary'; import { Router } from 'flavours/glitch/components/router'; import UI from 'flavours/glitch/features/ui'; +import { IdentityContext, createIdentityContext } from 'flavours/glitch/identity_context'; import initialState, { title as siteTitle } from 'flavours/glitch/initial_state'; import { IntlProvider } from 'flavours/glitch/locales'; import { store } from 'flavours/glitch/store'; @@ -33,33 +33,9 @@ if (initialState.meta.me) { store.dispatch(fetchCustomEmojis()); } -const createIdentityContext = state => ({ - signedIn: !!state.meta.me, - accountId: state.meta.me, - disabledAccountId: state.meta.disabled_account_id, - accessToken: state.meta.access_token, - permissions: state.role ? state.role.permissions : 0, -}); - export default class Mastodon extends PureComponent { - - static childContextTypes = { - identity: PropTypes.shape({ - signedIn: PropTypes.bool.isRequired, - accountId: PropTypes.string, - disabledAccountId: PropTypes.string, - accessToken: PropTypes.string, - }).isRequired, - }; - identity = createIdentityContext(initialState); - getChildContext() { - return { - identity: this.identity, - }; - } - componentDidMount() { if (this.identity.signedIn) { this.disconnect = store.dispatch(connectUserStream()); @@ -79,19 +55,21 @@ export default class Mastodon extends PureComponent { render () { return ( - - - - - - - - + + + + + + + + + - - - - + + + + + ); } diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx index e311936af9..62c8c84c66 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/header.jsx @@ -23,6 +23,7 @@ import { Icon } from 'flavours/glitch/components/icon'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { autoPlayGif, me, domain as localDomain } from 'flavours/glitch/initial_state'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; @@ -94,6 +95,7 @@ const dateFormatOptions = { class Header extends ImmutablePureComponent { static propTypes = { + identity: identityContextPropShape, account: ImmutablePropTypes.record, identity_props: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, @@ -117,10 +119,6 @@ class Header extends ImmutablePureComponent { ...WithRouterPropTypes, }; - static contextTypes = { - identity: PropTypes.object, - }; - openEditProfile = () => { window.open(profileLink, '_blank'); }; @@ -170,7 +168,7 @@ class Header extends ImmutablePureComponent { render () { const { account, hidden, intl } = this.props; - const { signedIn, permissions } = this.context.identity; + const { signedIn, permissions } = this.props.identity; if (!account) { return null; @@ -414,4 +412,4 @@ class Header extends ImmutablePureComponent { } -export default withRouter(injectIntl(Header)); +export default withRouter(withIdentity(injectIntl(Header))); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.jsx b/app/javascript/flavours/glitch/features/community_timeline/index.jsx index 7be2196511..9f306923e3 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx @@ -9,6 +9,7 @@ import { connect } from 'react-redux'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { domain } from 'flavours/glitch/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; @@ -40,16 +41,12 @@ const mapStateToProps = (state, { columnId }) => { }; class CommunityTimeline extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static defaultProps = { onlyMedia: false, }; static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, columnId: PropTypes.string, intl: PropTypes.object.isRequired, @@ -80,7 +77,7 @@ class CommunityTimeline extends PureComponent { componentDidMount () { const { dispatch, onlyMedia } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; dispatch(expandCommunityTimeline({ onlyMedia })); @@ -90,7 +87,7 @@ class CommunityTimeline extends PureComponent { } componentDidUpdate (prevProps) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (prevProps.onlyMedia !== this.props.onlyMedia) { const { dispatch, onlyMedia } = this.props; @@ -165,4 +162,4 @@ class CommunityTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(CommunityTimeline)); +export default withIdentity(connect(mapStateToProps)(injectIntl(CommunityTimeline))); diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx index 4b64038f57..f5a51334af 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx @@ -12,6 +12,7 @@ import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { domain, searchEnabled } from 'flavours/glitch/initial_state'; import { HASHTAG_REGEX } from 'flavours/glitch/utils/hashtags'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -33,12 +34,8 @@ const labelForRecentSearch = search => { }; class Search extends PureComponent { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, value: PropTypes.string.isRequired, recent: ImmutablePropTypes.orderedSet, submitted: PropTypes.bool, @@ -276,7 +273,7 @@ class Search extends PureComponent { } _calculateOptions (value) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const trimmedValue = value.trim(); const options = []; @@ -318,7 +315,7 @@ class Search extends PureComponent { render () { const { intl, value, submitted, recent } = this.props; const { expanded, options, selectedOption } = this.state; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const hasValue = value.length > 0 || submitted; @@ -402,4 +399,4 @@ class Search extends PureComponent { } -export default withRouter(injectIntl(Search)); +export default withRouter(withIdentity(injectIntl(Search))); diff --git a/app/javascript/flavours/glitch/features/explore/index.jsx b/app/javascript/flavours/glitch/features/explore/index.jsx index cca9cada47..8137e75ea4 100644 --- a/app/javascript/flavours/glitch/features/explore/index.jsx +++ b/app/javascript/flavours/glitch/features/explore/index.jsx @@ -13,6 +13,7 @@ import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import Search from 'flavours/glitch/features/compose/containers/search_container'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { trendsEnabled } from 'flavours/glitch/initial_state'; import Links from './links'; @@ -32,12 +33,8 @@ const mapStateToProps = state => ({ }); class Explore extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, multiColumn: PropTypes.bool, isSearching: PropTypes.bool, @@ -53,7 +50,7 @@ class Explore extends PureComponent { render() { const { intl, multiColumn, isSearching } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; return ( @@ -114,4 +111,4 @@ class Explore extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(Explore)); +export default withIdentity(connect(mapStateToProps)(injectIntl(Explore))); diff --git a/app/javascript/flavours/glitch/features/firehose/index.jsx b/app/javascript/flavours/glitch/features/firehose/index.jsx index dc6a38ae2e..2a6d790d52 100644 --- a/app/javascript/flavours/glitch/features/firehose/index.jsx +++ b/app/javascript/flavours/glitch/features/firehose/index.jsx @@ -6,6 +6,7 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; import { NavLink } from 'react-router-dom'; +import { useIdentity } from '@/flavours/glitch/identity_context'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import { addColumn } from 'flavours/glitch/actions/columns'; import { changeSetting } from 'flavours/glitch/actions/settings'; @@ -13,7 +14,7 @@ import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/act import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; import SettingText from 'flavours/glitch/components/setting_text'; -import initialState, { domain } from 'flavours/glitch/initial_state'; +import { domain } from 'flavours/glitch/initial_state'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; import Column from '../../components/column'; @@ -26,15 +27,6 @@ const messages = defineMessages({ filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, }); -// TODO: use a proper React context later on -const useIdentity = () => ({ - signedIn: !!initialState.meta.me, - accountId: initialState.meta.me, - disabledAccountId: initialState.meta.disabled_account_id, - accessToken: initialState.meta.access_token, - permissions: initialState.role ? initialState.role.permissions : 0, -}); - const ColumnSettings = () => { const intl = useIntl(); const dispatch = useAppDispatch(); diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx index 145cb09a30..2d13d3d584 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx @@ -28,6 +28,7 @@ import { fetchLists } from 'flavours/glitch/actions/lists'; import { openModal } from 'flavours/glitch/actions/modal'; import Column from 'flavours/glitch/features/ui/components/column'; import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { preferencesLink } from 'flavours/glitch/utils/backend_links'; @@ -99,12 +100,8 @@ const badgeDisplay = (number, limit) => { }; class GettingStarted extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, myAccount: ImmutablePropTypes.record, columns: ImmutablePropTypes.list, @@ -123,7 +120,7 @@ class GettingStarted extends ImmutablePureComponent { componentDidMount () { const { fetchFollowRequests } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (!signedIn) { return; @@ -134,7 +131,7 @@ class GettingStarted extends ImmutablePureComponent { render () { const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const navItems = []; let listItems = []; @@ -219,4 +216,4 @@ class GettingStarted extends ImmutablePureComponent { } -export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(GettingStarted)); +export default withIdentity(connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(GettingStarted))); diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx index 7c4979bd63..4a0dbb4a1c 100644 --- a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx @@ -16,7 +16,7 @@ import { openModal } from 'flavours/glitch/actions/modal'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; - +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; const messages = defineMessages({ heading: { id: 'column.heading', defaultMessage: 'Misc' }, @@ -32,11 +32,8 @@ const messages = defineMessages({ class GettingStartedMisc extends ImmutablePureComponent { - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, }; @@ -49,7 +46,7 @@ class GettingStartedMisc extends ImmutablePureComponent { render () { const { intl } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; return ( @@ -69,4 +66,4 @@ class GettingStartedMisc extends ImmutablePureComponent { } -export default connect()(injectIntl(GettingStartedMisc)); +export default connect()(withIdentity(injectIntl(GettingStartedMisc))); diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx b/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx index c2a7574307..c33c458c27 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx @@ -17,6 +17,7 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/ac import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import StatusListContainer from '../ui/containers/status_list_container'; @@ -29,14 +30,10 @@ const mapStateToProps = (state, props) => ({ }); class HashtagTimeline extends PureComponent { - disconnects = []; - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, params: PropTypes.object.isRequired, columnId: PropTypes.string, dispatch: PropTypes.func.isRequired, @@ -94,7 +91,7 @@ class HashtagTimeline extends PureComponent { }; _subscribe (dispatch, id, tags = {}, local) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (!signedIn) { return; @@ -168,7 +165,7 @@ class HashtagTimeline extends PureComponent { handleFollow = () => { const { dispatch, params, tag } = this.props; const { id } = params; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (!signedIn) { return; @@ -185,7 +182,7 @@ class HashtagTimeline extends PureComponent { const { hasUnread, columnId, multiColumn, tag } = this.props; const { id, local } = this.props.params; const pinned = !!columnId; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; return ( @@ -225,4 +222,4 @@ class HashtagTimeline extends PureComponent { } -export default connect(mapStateToProps)(HashtagTimeline); +export default connect(mapStateToProps)(withIdentity(HashtagTimeline)); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx index a2683f587f..3220f777e1 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/home_timeline/index.jsx @@ -14,6 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/act import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge'; import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator'; import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { criticalUpdatesPending } from 'flavours/glitch/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; @@ -41,12 +42,8 @@ const mapStateToProps = state => ({ }); class HomeTimeline extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, hasUnread: PropTypes.bool, @@ -128,7 +125,7 @@ class HomeTimeline extends PureComponent { render () { const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const banners = []; let announcementsButton; @@ -192,4 +189,4 @@ class HomeTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(HomeTimeline)); +export default connect(mapStateToProps)(withIdentity(injectIntl(HomeTimeline))); diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx index 3f02df1881..ee78466b90 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions'; import { CheckboxWithLabel } from './checkbox_with_label'; @@ -13,13 +14,9 @@ import GrantPermissionButton from './grant_permission_button'; import PillBarButton from './pill_bar_button'; import SettingToggle from './setting_toggle'; -export default class ColumnSettings extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - +class ColumnSettings extends PureComponent { static propTypes = { + identity: identityContextPropShape, settings: ImmutablePropTypes.map.isRequired, pushSettings: ImmutablePropTypes.map.isRequired, onChange: PropTypes.func.isRequired, @@ -216,7 +213,7 @@ export default class ColumnSettings extends PureComponent { - {((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && ( + {((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (

@@ -229,7 +226,7 @@ export default class ColumnSettings extends PureComponent {
)} - {((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && ( + {((this.props.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (

@@ -246,3 +243,5 @@ export default class ColumnSettings extends PureComponent { } } + +export default withIdentity(ColumnSettings); diff --git a/app/javascript/flavours/glitch/features/notifications/index.jsx b/app/javascript/flavours/glitch/features/notifications/index.jsx index e84ef70b05..b4f1bf5dfe 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.jsx +++ b/app/javascript/flavours/glitch/features/notifications/index.jsx @@ -19,6 +19,7 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg? import { compareId } from 'flavours/glitch/compare_id'; import { Icon } from 'flavours/glitch/components/icon'; import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { submitMarkers } from '../../actions/markers'; @@ -93,12 +94,8 @@ const mapDispatchToProps = dispatch => ({ }); class Notifications extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, columnId: PropTypes.string, notifications: ImmutablePropTypes.list.isRequired, showFilterBar: PropTypes.bool.isRequired, @@ -225,7 +222,7 @@ class Notifications extends PureComponent { const { animatingNCD } = this.state; const pinned = !!columnId; const emptyMessage = ; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; let scrollableContent = null; @@ -373,4 +370,4 @@ class Notifications extends PureComponent { } -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Notifications)); +export default connect(mapStateToProps, mapDispatchToProps)(withIdentity(injectIntl(Notifications))); diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx index e21ee2d3d3..0b6b91fe91 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx @@ -18,6 +18,7 @@ import { replyCompose } from 'flavours/glitch/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'flavours/glitch/actions/interactions'; import { openModal } from 'flavours/glitch/actions/modal'; import { IconButton } from 'flavours/glitch/components/icon_button'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { me, boostModal } from 'flavours/glitch/initial_state'; import { makeGetStatus } from 'flavours/glitch/selectors'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -48,12 +49,8 @@ const makeMapStateToProps = () => { }; class Footer extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, statusId: PropTypes.string.isRequired, status: ImmutablePropTypes.map.isRequired, intl: PropTypes.object.isRequired, @@ -77,7 +74,7 @@ class Footer extends ImmutablePureComponent { handleReplyClick = () => { const { dispatch, askReplyConfirmation, status, intl } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (askReplyConfirmation) { @@ -106,7 +103,7 @@ class Footer extends ImmutablePureComponent { handleFavouriteClick = () => { const { dispatch, status } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (status.get('favourited')) { @@ -133,7 +130,7 @@ class Footer extends ImmutablePureComponent { handleReblogClick = e => { const { dispatch, status } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (status.get('reblogged')) { @@ -236,4 +233,4 @@ class Footer extends ImmutablePureComponent { } -export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer))); +export default connect(makeMapStateToProps)(withIdentity(withRouter(injectIntl(Footer)))); diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.jsx b/app/javascript/flavours/glitch/features/public_timeline/index.jsx index 8b9503928b..1f255a468d 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/public_timeline/index.jsx @@ -9,6 +9,7 @@ import { connect } from 'react-redux'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { domain } from 'flavours/glitch/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; @@ -44,16 +45,12 @@ const mapStateToProps = (state, { columnId }) => { }; class PublicTimeline extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static defaultProps = { onlyMedia: false, }; static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, columnId: PropTypes.string, @@ -86,7 +83,7 @@ class PublicTimeline extends PureComponent { componentDidMount () { const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly })); if (signedIn) { @@ -95,7 +92,7 @@ class PublicTimeline extends PureComponent { } componentDidUpdate (prevProps) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) { const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; @@ -170,4 +167,4 @@ class PublicTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(PublicTimeline)); +export default connect(mapStateToProps)(withIdentity(injectIntl(PublicTimeline))); diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index ab00091200..808712b021 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -20,6 +20,7 @@ import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -59,12 +60,8 @@ const messages = defineMessages({ }); class ActionBar extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, @@ -157,7 +154,7 @@ class ActionBar extends PureComponent { render () { const { status, intl } = this.props; - const { signedIn, permissions } = this.context.identity; + const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); @@ -265,4 +262,4 @@ class ActionBar extends PureComponent { } -export default withRouter(injectIntl(ActionBar)); +export default withRouter(withIdentity(injectIntl(ActionBar))); diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index 6274bf672c..2656f8afdd 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -21,6 +21,7 @@ import { Icon } from 'flavours/glitch/components/icon'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -185,12 +186,8 @@ const titleFromStatus = (intl, status) => { }; class Status extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, status: ImmutablePropTypes.map, @@ -277,7 +274,7 @@ class Status extends ImmutablePureComponent { handleFavouriteClick = (status, e) => { const { dispatch } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (status.get('favourited')) { @@ -317,7 +314,7 @@ class Status extends ImmutablePureComponent { handleReplyClick = (status) => { const { askReplyConfirmation, dispatch, intl } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (askReplyConfirmation) { @@ -357,7 +354,7 @@ class Status extends ImmutablePureComponent { handleReblogClick = (status, e) => { const { settings, dispatch } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) { @@ -792,4 +789,4 @@ class Status extends ImmutablePureComponent { } -export default withRouter(injectIntl(connect(makeMapStateToProps)(Status))); +export default withRouter(injectIntl(connect(makeMapStateToProps)(withIdentity(Status)))); diff --git a/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx index a99d76c1a4..e530b87d26 100644 --- a/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx @@ -7,16 +7,13 @@ import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose'; import ServerBanner from 'flavours/glitch/components/server_banner'; import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container'; import SearchContainer from 'flavours/glitch/features/compose/containers/search_container'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import LinkFooter from './link_footer'; class ComposePanel extends PureComponent { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, }; @@ -31,7 +28,7 @@ class ComposePanel extends PureComponent { } render() { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; return (
@@ -55,4 +52,4 @@ class ComposePanel extends PureComponent { } -export default connect()(ComposePanel); +export default connect()(withIdentity(ComposePanel)); diff --git a/app/javascript/flavours/glitch/features/ui/components/header.jsx b/app/javascript/flavours/glitch/features/ui/components/header.jsx index 618a6b63d4..f102912faa 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/header.jsx @@ -14,6 +14,7 @@ import { Avatar } from 'flavours/glitch/components/avatar'; import { Icon } from 'flavours/glitch/components/icon'; import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo'; import { Permalink } from 'flavours/glitch/components/permalink'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { registrationsOpen, me, sso_redirect } from 'flavours/glitch/initial_state'; const Account = connect(state => ({ @@ -42,12 +43,8 @@ const mapDispatchToProps = (dispatch) => ({ }); class Header extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, openClosedRegistrationsModal: PropTypes.func, location: PropTypes.object, signupUrl: PropTypes.string.isRequired, @@ -61,7 +58,7 @@ class Header extends PureComponent { } render () { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props; let content; @@ -122,4 +119,4 @@ class Header extends PureComponent { } -export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header))); +export default injectIntl(withRouter(withIdentity(connect(mapStateToProps, mapDispatchToProps)(Header)))); diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx b/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx index 6741552731..7c0ece465f 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx @@ -8,6 +8,7 @@ import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { openModal } from 'flavours/glitch/actions/modal'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'flavours/glitch/initial_state'; import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions'; import { logOut } from 'flavours/glitch/utils/log_out'; @@ -32,12 +33,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }); class LinkFooter extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, multiColumn: PropTypes.bool, onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -53,7 +50,7 @@ class LinkFooter extends PureComponent { }; render () { - const { signedIn, permissions } = this.context.identity; + const { signedIn, permissions } = this.props.identity; const { multiColumn } = this.props; const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS); @@ -108,4 +105,4 @@ class LinkFooter extends PureComponent { } -export default injectIntl(connect(null, mapDispatchToProps)(LinkFooter)); +export default injectIntl(withIdentity(connect(null, mapDispatchToProps)(LinkFooter))); diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx index fa1df612fa..98d82342cf 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx @@ -30,6 +30,7 @@ import StarIcon from '@/material-icons/400-24px/star.svg?react'; import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge'; import { NavigationPortal } from 'flavours/glitch/components/navigation_portal'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state'; import { transientSingleColumn } from 'flavours/glitch/is_mobile'; import { preferencesLink } from 'flavours/glitch/utils/backend_links'; @@ -98,12 +99,8 @@ const FollowRequestsLink = () => { }; class NavigationPanel extends Component { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, onOpenSettings: PropTypes.func, }; @@ -114,7 +111,7 @@ class NavigationPanel extends Component { render () { const { intl, onOpenSettings } = this.props; - const { signedIn, disabledAccountId } = this.context.identity; + const { signedIn, disabledAccountId } = this.props.identity; let banner = undefined; @@ -188,4 +185,4 @@ class NavigationPanel extends Component { } -export default injectIntl(NavigationPanel); +export default injectIntl(withIdentity(NavigationPanel)); diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index c257b8cdf5..4a7b9ebfde 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -17,6 +17,7 @@ import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavour import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding'; import { Permalink } from 'flavours/glitch/components/permalink'; import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture'; +import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { layoutFromWindow } from 'flavours/glitch/is_mobile'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -129,12 +130,8 @@ const keyMap = { }; class SwitchingColumnsArea extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, children: PropTypes.node, location: PropTypes.object, singleColumn: PropTypes.bool, @@ -169,7 +166,7 @@ class SwitchingColumnsArea extends PureComponent { render () { const { children, singleColumn } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const pathName = this.props.location.pathname; let redirect; @@ -262,12 +259,8 @@ class SwitchingColumnsArea extends PureComponent { } class UI extends PureComponent { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, children: PropTypes.node, isWide: PropTypes.bool, @@ -323,7 +316,7 @@ class UI extends PureComponent { this.dragTargets.push(e.target); } - if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore && this.context.identity.signedIn) { + if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore && this.props.identity.signedIn) { this.setState({ draggingOver: true }); } }; @@ -351,7 +344,7 @@ class UI extends PureComponent { this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore && this.context.identity.signedIn) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore && this.props.identity.signedIn) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } }; @@ -403,7 +396,7 @@ class UI extends PureComponent { }; componentDidMount () { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); @@ -649,7 +642,7 @@ class UI extends PureComponent {
- + {children} @@ -665,4 +658,4 @@ class UI extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(withRouter(UI))); +export default connect(mapStateToProps)(injectIntl(withRouter(withIdentity(UI)))); diff --git a/app/javascript/flavours/glitch/identity_context.tsx b/app/javascript/flavours/glitch/identity_context.tsx new file mode 100644 index 0000000000..28dcb1f14b --- /dev/null +++ b/app/javascript/flavours/glitch/identity_context.tsx @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import { createContext, useContext } from 'react'; + +import hoistStatics from 'hoist-non-react-statics'; + +import type { InitialState } from 'flavours/glitch/initial_state'; + +export interface IdentityContextType { + signedIn: boolean; + accountId: string | undefined; + disabledAccountId: string | undefined; + accessToken: string | undefined; + permissions: number; +} + +export const identityContextPropShape = PropTypes.shape({ + signedIn: PropTypes.bool.isRequired, + accountId: PropTypes.string, + disabledAccountId: PropTypes.string, + accessToken: PropTypes.string, +}).isRequired; + +export const createIdentityContext = (state: InitialState) => ({ + signedIn: !!state.meta.me, + accountId: state.meta.me, + disabledAccountId: state.meta.disabled_account_id, + accessToken: state.meta.access_token, + permissions: state.role?.permissions ?? 0, +}); + +export const IdentityContext = createContext({ + signedIn: false, + permissions: 0, + accountId: undefined, + disabledAccountId: undefined, + accessToken: undefined, +}); + +export const useIdentity = () => useContext(IdentityContext); + +export interface IdentityProps { + ref?: unknown; + wrappedComponentRef?: unknown; +} + +/* Injects an `identity` props into the wrapped component to be able to use the new context in class components */ +export function withIdentity< + ComponentType extends React.ComponentType, +>(Component: ComponentType) { + const displayName = `withIdentity(${Component.displayName ?? Component.name})`; + const C = (props: React.ComponentProps) => { + const { wrappedComponentRef, ...remainingProps } = props; + + return ( + + {(context) => { + return ( + // @ts-expect-error - Dynamic covariant generic components are tough to type. + + ); + }} + + ); + }; + + C.displayName = displayName; + C.WrappedComponent = Component; + + return hoistStatics(C, Component); +} diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index 3e84966cb4..1a40810418 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -50,12 +50,22 @@ * @property {string} default_content_type */ +/** + * @typedef Role + * @property {string} id + * @property {string} name + * @property {string} permissions + * @property {string} color + * @property {boolean} highlighted + */ + /** * @typedef InitialState * @property {Record} accounts * @property {InitialStateLanguage[]} languages * @property {boolean=} critical_updates_pending * @property {InitialStateMeta} meta + * @property {Role?} role * @property {object} local_settings * @property {number} max_feed_hashtags * @property {number} poll_limits diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx index a7d07ffdb0..42183f336d 100644 --- a/app/javascript/mastodon/components/column_header.jsx +++ b/app/javascript/mastodon/components/column_header.jsx @@ -14,8 +14,10 @@ import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; import { Icon } from 'mastodon/components/icon'; import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; + import { useAppHistory } from './router'; const messages = defineMessages({ @@ -51,12 +53,8 @@ BackButton.propTypes = { }; class ColumnHeader extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, title: PropTypes.node, icon: PropTypes.string, @@ -171,7 +169,7 @@ class ColumnHeader extends PureComponent { ); } - if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) { + if (this.props.identity.signedIn && (children || (multiColumn && this.props.onPin))) { collapseButton = ( } + {!showResults && } {!showResults && <> · } {showResults && !this.props.disabled && <> · } {votesCount} @@ -247,4 +244,4 @@ class Poll extends ImmutablePureComponent { } -export default injectIntl(Poll); +export default injectIntl(withIdentity(Poll)); diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 6def49fdbb..c79eae8460 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -22,6 +22,7 @@ import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -74,12 +75,8 @@ const mapStateToProps = (state, { status }) => ({ }); class StatusActionBar extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, relationship: ImmutablePropTypes.record, onReply: PropTypes.func, @@ -118,7 +115,7 @@ class StatusActionBar extends ImmutablePureComponent { ]; handleReplyClick = () => { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { this.props.onReply(this.props.status, this.props.history); @@ -136,7 +133,7 @@ class StatusActionBar extends ImmutablePureComponent { }; handleFavouriteClick = () => { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { this.props.onFavourite(this.props.status); @@ -146,7 +143,7 @@ class StatusActionBar extends ImmutablePureComponent { }; handleReblogClick = e => { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { this.props.onReblog(this.props.status, e); @@ -250,7 +247,7 @@ class StatusActionBar extends ImmutablePureComponent { render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; - const { signedIn, permissions } = this.context.identity; + const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); @@ -410,4 +407,4 @@ class StatusActionBar extends ImmutablePureComponent { } -export default withRouter(connect(mapStateToProps)(injectIntl(StatusActionBar))); +export default withRouter(withIdentity(connect(mapStateToProps)(injectIntl(StatusActionBar)))); diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 4a7ba941eb..24483cf512 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -12,8 +12,10 @@ import { connect } from 'react-redux'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import { Icon } from 'mastodon/components/icon'; import PollContainer from 'mastodon/containers/poll_container'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; + const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) /** @@ -67,12 +69,8 @@ const mapStateToProps = state => ({ }); class StatusContent extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, statusContent: PropTypes.string, expanded: PropTypes.bool, @@ -245,7 +243,7 @@ class StatusContent extends PureComponent { 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.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); + 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 = { __html: statusContent ?? getStatusContent(status) }; const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') }; @@ -328,4 +326,4 @@ class StatusContent extends PureComponent { } -export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent))); +export default withRouter(withIdentity(connect(mapStateToProps)(injectIntl(StatusContent)))); diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 87708da191..0b1255c336 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { Helmet } from 'react-helmet'; @@ -14,6 +13,7 @@ import { connectUserStream } from 'mastodon/actions/streaming'; import ErrorBoundary from 'mastodon/components/error_boundary'; import { Router } from 'mastodon/components/router'; import UI from 'mastodon/features/ui'; +import { IdentityContext, createIdentityContext } from 'mastodon/identity_context'; import initialState, { title as siteTitle } from 'mastodon/initial_state'; import { IntlProvider } from 'mastodon/locales'; import { store } from 'mastodon/store'; @@ -28,33 +28,9 @@ if (initialState.meta.me) { store.dispatch(fetchCustomEmojis()); } -const createIdentityContext = state => ({ - signedIn: !!state.meta.me, - accountId: state.meta.me, - disabledAccountId: state.meta.disabled_account_id, - accessToken: state.meta.access_token, - permissions: state.role ? state.role.permissions : 0, -}); - export default class Mastodon extends PureComponent { - - static childContextTypes = { - identity: PropTypes.shape({ - signedIn: PropTypes.bool.isRequired, - accountId: PropTypes.string, - disabledAccountId: PropTypes.string, - accessToken: PropTypes.string, - }).isRequired, - }; - identity = createIdentityContext(initialState); - getChildContext() { - return { - identity: this.identity, - }; - } - componentDidMount() { if (this.identity.signedIn) { this.disconnect = store.dispatch(connectUserStream()); @@ -74,19 +50,21 @@ export default class Mastodon extends PureComponent { render () { return ( - - - - - - - - + + + + + + + + + - - - - + + + + + ); } diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index e9d6071a21..b10ef6ef76 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -25,6 +25,7 @@ import { IconButton } from 'mastodon/components/icon_button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { ShortNumber } from 'mastodon/components/short_number'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -111,6 +112,7 @@ const dateFormatOptions = { class Header extends ImmutablePureComponent { static propTypes = { + identity: identityContextPropShape, account: ImmutablePropTypes.record, identity_props: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, @@ -136,10 +138,6 @@ class Header extends ImmutablePureComponent { ...WithRouterPropTypes, }; - static contextTypes = { - identity: PropTypes.object, - }; - setRef = c => { this.node = c; }; @@ -255,7 +253,7 @@ class Header extends ImmutablePureComponent { render () { const { account, hidden, intl } = this.props; - const { signedIn, permissions } = this.context.identity; + const { signedIn, permissions } = this.props.identity; if (!account) { return null; @@ -516,4 +514,4 @@ class Header extends ImmutablePureComponent { } -export default withRouter(injectIntl(Header)); +export default withRouter(withIdentity(injectIntl(Header))); diff --git a/app/javascript/mastodon/features/community_timeline/index.jsx b/app/javascript/mastodon/features/community_timeline/index.jsx index 0aa1f9aa23..5652ea5327 100644 --- a/app/javascript/mastodon/features/community_timeline/index.jsx +++ b/app/javascript/mastodon/features/community_timeline/index.jsx @@ -9,6 +9,7 @@ import { connect } from 'react-redux'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import { DismissableBanner } from 'mastodon/components/dismissable_banner'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { domain } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; @@ -38,16 +39,12 @@ const mapStateToProps = (state, { columnId }) => { }; class CommunityTimeline extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static defaultProps = { onlyMedia: false, }; static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, columnId: PropTypes.string, intl: PropTypes.object.isRequired, @@ -77,7 +74,7 @@ class CommunityTimeline extends PureComponent { componentDidMount () { const { dispatch, onlyMedia } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; dispatch(expandCommunityTimeline({ onlyMedia })); @@ -87,7 +84,7 @@ class CommunityTimeline extends PureComponent { } componentDidUpdate (prevProps) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (prevProps.onlyMedia !== this.props.onlyMedia) { const { dispatch, onlyMedia } = this.props; @@ -161,4 +158,4 @@ class CommunityTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(CommunityTimeline)); +export default withIdentity(connect(mapStateToProps)(injectIntl(CommunityTimeline))); diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index ca02c23fc4..7fa7ad248b 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -12,6 +12,7 @@ import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import { Icon } from 'mastodon/components/icon'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { domain, searchEnabled } from 'mastodon/initial_state'; import { HASHTAG_REGEX } from 'mastodon/utils/hashtags'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -33,12 +34,8 @@ const labelForRecentSearch = search => { }; class Search extends PureComponent { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, value: PropTypes.string.isRequired, recent: ImmutablePropTypes.orderedSet, submitted: PropTypes.bool, @@ -276,7 +273,7 @@ class Search extends PureComponent { } _calculateOptions (value) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const trimmedValue = value.trim(); const options = []; @@ -318,7 +315,7 @@ class Search extends PureComponent { render () { const { intl, value, submitted, recent } = this.props; const { expanded, options, selectedOption } = this.state; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const hasValue = value.length > 0 || submitted; @@ -402,4 +399,4 @@ class Search extends PureComponent { } -export default withRouter(injectIntl(Search)); +export default withRouter(withIdentity(injectIntl(Search))); diff --git a/app/javascript/mastodon/features/explore/index.jsx b/app/javascript/mastodon/features/explore/index.jsx index d77aec7013..83e5df22f8 100644 --- a/app/javascript/mastodon/features/explore/index.jsx +++ b/app/javascript/mastodon/features/explore/index.jsx @@ -13,6 +13,7 @@ import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; import Search from 'mastodon/features/compose/containers/search_container'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { trendsEnabled } from 'mastodon/initial_state'; import Links from './links'; @@ -32,12 +33,8 @@ const mapStateToProps = state => ({ }); class Explore extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, multiColumn: PropTypes.bool, isSearching: PropTypes.bool, @@ -53,7 +50,7 @@ class Explore extends PureComponent { render() { const { intl, multiColumn, isSearching } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; return ( @@ -114,4 +111,4 @@ class Explore extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(Explore)); +export default withIdentity(connect(mapStateToProps)(injectIntl(Explore))); diff --git a/app/javascript/mastodon/features/firehose/index.jsx b/app/javascript/mastodon/features/firehose/index.jsx index c65fe48eac..f65bee45ec 100644 --- a/app/javascript/mastodon/features/firehose/index.jsx +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -6,13 +6,14 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; import { NavLink } from 'react-router-dom'; +import { useIdentity } from '@/mastodon/identity_context'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import { addColumn } from 'mastodon/actions/columns'; import { changeSetting } from 'mastodon/actions/settings'; import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming'; import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines'; import { DismissableBanner } from 'mastodon/components/dismissable_banner'; -import initialState, { domain } from 'mastodon/initial_state'; +import { domain } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; import Column from '../../components/column'; @@ -24,15 +25,6 @@ const messages = defineMessages({ title: { id: 'column.firehose', defaultMessage: 'Live feeds' }, }); -// TODO: use a proper React context later on -const useIdentity = () => ({ - signedIn: !!initialState.meta.me, - accountId: initialState.meta.me, - disabledAccountId: initialState.meta.disabled_account_id, - accessToken: initialState.meta.access_token, - permissions: initialState.role ? initialState.role.permissions : 0, -}); - const ColumnSettings = () => { const dispatch = useAppDispatch(); const settings = useAppSelector((state) => state.getIn(['settings', 'firehose'])); diff --git a/app/javascript/mastodon/features/getting_started/index.jsx b/app/javascript/mastodon/features/getting_started/index.jsx index db6e0f6ecd..628bbe62bb 100644 --- a/app/javascript/mastodon/features/getting_started/index.jsx +++ b/app/javascript/mastodon/features/getting_started/index.jsx @@ -24,6 +24,7 @@ import { fetchFollowRequests } from 'mastodon/actions/accounts'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; import LinkFooter from 'mastodon/features/ui/components/link_footer'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { me, showTrends } from '../../initial_state'; import { NavigationBar } from '../compose/components/navigation_bar'; @@ -75,12 +76,8 @@ const badgeDisplay = (number, limit) => { }; class GettingStarted extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, myAccount: ImmutablePropTypes.record, multiColumn: PropTypes.bool, @@ -91,7 +88,7 @@ class GettingStarted extends ImmutablePureComponent { componentDidMount () { const { fetchFollowRequests } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (!signedIn) { return; @@ -102,7 +99,7 @@ class GettingStarted extends ImmutablePureComponent { render () { const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const navItems = []; @@ -167,4 +164,4 @@ class GettingStarted extends ImmutablePureComponent { } -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(GettingStarted)); +export default withIdentity(connect(mapStateToProps, mapDispatchToProps)(injectIntl(GettingStarted))); diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.jsx b/app/javascript/mastodon/features/hashtag_timeline/index.jsx index f431a7e9b7..42a668859e 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.jsx +++ b/app/javascript/mastodon/features/hashtag_timeline/index.jsx @@ -17,6 +17,7 @@ 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 { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import StatusListContainer from '../ui/containers/status_list_container'; @@ -29,14 +30,10 @@ const mapStateToProps = (state, props) => ({ }); class HashtagTimeline extends PureComponent { - disconnects = []; - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, params: PropTypes.object.isRequired, columnId: PropTypes.string, dispatch: PropTypes.func.isRequired, @@ -94,7 +91,7 @@ class HashtagTimeline extends PureComponent { }; _subscribe (dispatch, id, tags = {}, local) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (!signedIn) { return; @@ -168,7 +165,7 @@ class HashtagTimeline extends PureComponent { handleFollow = () => { const { dispatch, params, tag } = this.props; const { id } = params; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (!signedIn) { return; @@ -185,7 +182,7 @@ class HashtagTimeline extends PureComponent { const { hasUnread, columnId, multiColumn, tag } = this.props; const { id, local } = this.props.params; const pinned = !!columnId; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; return ( @@ -225,4 +222,4 @@ class HashtagTimeline extends PureComponent { } -export default connect(mapStateToProps)(HashtagTimeline); +export default connect(mapStateToProps)(withIdentity(HashtagTimeline)); diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index 6e7dc2b6c8..00b5835a16 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -14,6 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an import { IconWithBadge } from 'mastodon/components/icon_with_badge'; import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { criticalUpdatesPending } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; @@ -40,12 +41,8 @@ const mapStateToProps = state => ({ }); class HomeTimeline extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, hasUnread: PropTypes.bool, @@ -126,7 +123,7 @@ class HomeTimeline extends PureComponent { render () { const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const banners = []; let announcementsButton; @@ -190,4 +187,4 @@ class HomeTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(HomeTimeline)); +export default connect(mapStateToProps)(withIdentity(injectIntl(HomeTimeline))); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index fc737c0fe2..e375b856c9 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'mastodon/permissions'; import { CheckboxWithLabel } from './checkbox_with_label'; @@ -12,13 +13,9 @@ import ClearColumnButton from './clear_column_button'; import GrantPermissionButton from './grant_permission_button'; import SettingToggle from './setting_toggle'; -export default class ColumnSettings extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - +class ColumnSettings extends PureComponent { static propTypes = { + identity: identityContextPropShape, settings: ImmutablePropTypes.map.isRequired, pushSettings: ImmutablePropTypes.map.isRequired, onChange: PropTypes.func.isRequired, @@ -215,7 +212,7 @@ export default class ColumnSettings extends PureComponent {
- {((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && ( + {((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (

@@ -228,7 +225,7 @@ export default class ColumnSettings extends PureComponent {
)} - {((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && ( + {((this.props.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (

@@ -245,3 +242,5 @@ export default class ColumnSettings extends PureComponent { } } + +export default withIdentity(ColumnSettings); diff --git a/app/javascript/mastodon/features/notifications/index.jsx b/app/javascript/mastodon/features/notifications/index.jsx index e062957ff8..d45f517152 100644 --- a/app/javascript/mastodon/features/notifications/index.jsx +++ b/app/javascript/mastodon/features/notifications/index.jsx @@ -17,6 +17,7 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg? import { compareId } from 'mastodon/compare_id'; import { Icon } from 'mastodon/components/icon'; import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { submitMarkers } from '../../actions/markers'; @@ -77,12 +78,8 @@ const mapStateToProps = state => ({ }); class Notifications extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, columnId: PropTypes.string, notifications: ImmutablePropTypes.list.isRequired, dispatch: PropTypes.func.isRequired, @@ -190,7 +187,7 @@ class Notifications extends PureComponent { const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props; const pinned = !!columnId; const emptyMessage = ; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; let scrollableContent = null; @@ -299,4 +296,4 @@ class Notifications extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(Notifications)); +export default connect(mapStateToProps)(withIdentity(injectIntl(Notifications))); diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx index 7a163a8825..d6b1b5fa81 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -18,6 +18,7 @@ import { replyCompose } from 'mastodon/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; import { openModal } from 'mastodon/actions/modal'; import { IconButton } from 'mastodon/components/icon_button'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { me, boostModal } from 'mastodon/initial_state'; import { makeGetStatus } from 'mastodon/selectors'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -47,12 +48,8 @@ const makeMapStateToProps = () => { }; class Footer extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, statusId: PropTypes.string.isRequired, status: ImmutablePropTypes.map.isRequired, intl: PropTypes.object.isRequired, @@ -75,7 +72,7 @@ class Footer extends ImmutablePureComponent { handleReplyClick = () => { const { dispatch, askReplyConfirmation, status, intl } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (askReplyConfirmation) { @@ -104,7 +101,7 @@ class Footer extends ImmutablePureComponent { handleFavouriteClick = () => { const { dispatch, status } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (status.get('favourited')) { @@ -131,7 +128,7 @@ class Footer extends ImmutablePureComponent { handleReblogClick = e => { const { dispatch, status } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (status.get('reblogged')) { @@ -209,4 +206,4 @@ class Footer extends ImmutablePureComponent { } -export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer))); +export default connect(makeMapStateToProps)(withIdentity(withRouter(injectIntl(Footer)))); diff --git a/app/javascript/mastodon/features/public_timeline/index.jsx b/app/javascript/mastodon/features/public_timeline/index.jsx index 3601dfeae8..91351901f5 100644 --- a/app/javascript/mastodon/features/public_timeline/index.jsx +++ b/app/javascript/mastodon/features/public_timeline/index.jsx @@ -9,6 +9,7 @@ import { connect } from 'react-redux'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import { DismissableBanner } from 'mastodon/components/dismissable_banner'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { domain } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; @@ -40,16 +41,12 @@ const mapStateToProps = (state, { columnId }) => { }; class PublicTimeline extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static defaultProps = { onlyMedia: false, }; static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, columnId: PropTypes.string, @@ -80,7 +77,7 @@ class PublicTimeline extends PureComponent { componentDidMount () { const { dispatch, onlyMedia, onlyRemote } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; dispatch(expandPublicTimeline({ onlyMedia, onlyRemote })); @@ -90,7 +87,7 @@ class PublicTimeline extends PureComponent { } componentDidUpdate (prevProps) { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) { const { dispatch, onlyMedia, onlyRemote } = this.props; @@ -164,4 +161,4 @@ class PublicTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(PublicTimeline)); +export default connect(mapStateToProps)(withIdentity(injectIntl(PublicTimeline))); diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 69209e8bd0..d610539987 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -21,6 +21,7 @@ import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -67,12 +68,8 @@ const mapStateToProps = (state, { status }) => ({ }); class ActionBar extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, relationship: ImmutablePropTypes.record, onReply: PropTypes.func.isRequired, @@ -198,7 +195,7 @@ class ActionBar extends PureComponent { render () { const { status, relationship, intl } = this.props; - const { signedIn, permissions } = this.context.identity; + const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); @@ -326,4 +323,4 @@ class ActionBar extends PureComponent { } -export default withRouter(connect(mapStateToProps)(injectIntl(ActionBar))); +export default withRouter(connect(mapStateToProps)(withIdentity(injectIntl(ActionBar)))); diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 3914759725..3a9bf524f8 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -20,6 +20,7 @@ import { Icon } from 'mastodon/components/icon'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import ScrollContainer from 'mastodon/containers/scroll_container'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { @@ -189,12 +190,8 @@ const titleFromStatus = (intl, status) => { }; class Status extends ImmutablePureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, status: ImmutablePropTypes.map, @@ -244,7 +241,7 @@ class Status extends ImmutablePureComponent { handleFavouriteClick = (status) => { const { dispatch } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (status.get('favourited')) { @@ -274,7 +271,7 @@ class Status extends ImmutablePureComponent { handleReplyClick = (status) => { const { askReplyConfirmation, dispatch, intl } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (askReplyConfirmation) { @@ -307,7 +304,7 @@ class Status extends ImmutablePureComponent { handleReblogClick = (status, e) => { const { dispatch } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; if (signedIn) { if (status.get('reblogged')) { @@ -745,4 +742,4 @@ class Status extends ImmutablePureComponent { } -export default withRouter(injectIntl(connect(makeMapStateToProps)(Status))); +export default withRouter(injectIntl(connect(makeMapStateToProps)(withIdentity(Status)))); diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.jsx b/app/javascript/mastodon/features/ui/components/compose_panel.jsx index e6ac79bdd9..18321cbe63 100644 --- a/app/javascript/mastodon/features/ui/components/compose_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/compose_panel.jsx @@ -7,16 +7,13 @@ import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/ import ServerBanner from 'mastodon/components/server_banner'; import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; import SearchContainer from 'mastodon/features/compose/containers/search_container'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import LinkFooter from './link_footer'; class ComposePanel extends PureComponent { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, }; @@ -41,7 +38,7 @@ class ComposePanel extends PureComponent { } render() { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; return (
@@ -65,4 +62,4 @@ class ComposePanel extends PureComponent { } -export default connect()(ComposePanel); +export default connect()(withIdentity(ComposePanel)); diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index 2f8636b12a..19c76c722b 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -13,6 +13,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 { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { registrationsOpen, me, sso_redirect } from 'mastodon/initial_state'; const Account = connect(state => ({ @@ -41,12 +42,8 @@ const mapDispatchToProps = (dispatch) => ({ }); class Header extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, openClosedRegistrationsModal: PropTypes.func, location: PropTypes.object, signupUrl: PropTypes.string.isRequired, @@ -60,7 +57,7 @@ class Header extends PureComponent { } render () { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props; let content; @@ -121,4 +118,4 @@ class Header extends PureComponent { } -export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header))); +export default injectIntl(withRouter(withIdentity(connect(mapStateToProps, mapDispatchToProps)(Header)))); diff --git a/app/javascript/mastodon/features/ui/components/link_footer.jsx b/app/javascript/mastodon/features/ui/components/link_footer.jsx index 6b1555243b..08af6fa444 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.jsx +++ b/app/javascript/mastodon/features/ui/components/link_footer.jsx @@ -8,6 +8,7 @@ import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { openModal } from 'mastodon/actions/modal'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state'; import { PERMISSION_INVITE_USERS } from 'mastodon/permissions'; import { logOut } from 'mastodon/utils/log_out'; @@ -32,12 +33,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }); class LinkFooter extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, multiColumn: PropTypes.bool, onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -53,7 +50,7 @@ class LinkFooter extends PureComponent { }; render () { - const { signedIn, permissions } = this.context.identity; + const { signedIn, permissions } = this.props.identity; const { multiColumn } = this.props; const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS); @@ -108,4 +105,4 @@ class LinkFooter extends PureComponent { } -export default injectIntl(connect(null, mapDispatchToProps)(LinkFooter)); +export default injectIntl(withIdentity(connect(null, mapDispatchToProps)(LinkFooter))); diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 14a1933436..ff90eef359 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -31,6 +31,7 @@ import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { IconWithBadge } from 'mastodon/components/icon_with_badge'; import { WordmarkLogo } from 'mastodon/components/logo'; import { NavigationPortal } from 'mastodon/components/navigation_portal'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { timelinePreview, trendsEnabled } from 'mastodon/initial_state'; import { transientSingleColumn } from 'mastodon/is_mobile'; @@ -97,12 +98,8 @@ const FollowRequestsLink = () => { }; class NavigationPanel extends Component { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, intl: PropTypes.object.isRequired, }; @@ -112,7 +109,7 @@ class NavigationPanel extends Component { render () { const { intl } = this.props; - const { signedIn, disabledAccountId } = this.context.identity; + const { signedIn, disabledAccountId } = this.props.identity; let banner = undefined; @@ -189,4 +186,4 @@ class NavigationPanel extends Component { } -export default injectIntl(NavigationPanel); +export default injectIntl(withIdentity(NavigationPanel)); diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index c84a2c51a4..7742f64860 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -15,6 +15,7 @@ import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; import { PictureInPicture } from 'mastodon/features/picture_in_picture'; +import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -120,12 +121,8 @@ const keyMap = { }; class SwitchingColumnsArea extends PureComponent { - - static contextTypes = { - identity: PropTypes.object, - }; - static propTypes = { + identity: identityContextPropShape, children: PropTypes.node, location: PropTypes.object, singleColumn: PropTypes.bool, @@ -160,7 +157,7 @@ class SwitchingColumnsArea extends PureComponent { render () { const { children, singleColumn } = this.props; - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; const pathName = this.props.location.pathname; let redirect; @@ -252,12 +249,8 @@ class SwitchingColumnsArea extends PureComponent { } class UI extends PureComponent { - - static contextTypes = { - identity: PropTypes.object.isRequired, - }; - static propTypes = { + identity: identityContextPropShape, dispatch: PropTypes.func.isRequired, children: PropTypes.node, isComposing: PropTypes.bool, @@ -309,7 +302,7 @@ class UI extends PureComponent { this.dragTargets.push(e.target); } - if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore && this.context.identity.signedIn) { + if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore && this.props.identity.signedIn) { this.setState({ draggingOver: true }); } }; @@ -337,7 +330,7 @@ class UI extends PureComponent { this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore && this.context.identity.signedIn) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore && this.props.identity.signedIn) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } }; @@ -389,7 +382,7 @@ class UI extends PureComponent { }; componentDidMount () { - const { signedIn } = this.context.identity; + const { signedIn } = this.props.identity; window.addEventListener('focus', this.handleWindowFocus, false); window.addEventListener('blur', this.handleWindowBlur, false); @@ -586,7 +579,7 @@ class UI extends PureComponent {
- + {children} @@ -602,4 +595,4 @@ class UI extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(withRouter(UI))); +export default connect(mapStateToProps)(injectIntl(withRouter(withIdentity(UI)))); diff --git a/app/javascript/mastodon/identity_context.tsx b/app/javascript/mastodon/identity_context.tsx new file mode 100644 index 0000000000..564b7895c9 --- /dev/null +++ b/app/javascript/mastodon/identity_context.tsx @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import { createContext, useContext } from 'react'; + +import hoistStatics from 'hoist-non-react-statics'; + +import type { InitialState } from 'mastodon/initial_state'; + +export interface IdentityContextType { + signedIn: boolean; + accountId: string | undefined; + disabledAccountId: string | undefined; + accessToken: string | undefined; + permissions: number; +} + +export const identityContextPropShape = PropTypes.shape({ + signedIn: PropTypes.bool.isRequired, + accountId: PropTypes.string, + disabledAccountId: PropTypes.string, + accessToken: PropTypes.string, +}).isRequired; + +export const createIdentityContext = (state: InitialState) => ({ + signedIn: !!state.meta.me, + accountId: state.meta.me, + disabledAccountId: state.meta.disabled_account_id, + accessToken: state.meta.access_token, + permissions: state.role?.permissions ?? 0, +}); + +export const IdentityContext = createContext({ + signedIn: false, + permissions: 0, + accountId: undefined, + disabledAccountId: undefined, + accessToken: undefined, +}); + +export const useIdentity = () => useContext(IdentityContext); + +export interface IdentityProps { + ref?: unknown; + wrappedComponentRef?: unknown; +} + +/* Injects an `identity` props into the wrapped component to be able to use the new context in class components */ +export function withIdentity< + ComponentType extends React.ComponentType, +>(Component: ComponentType) { + const displayName = `withIdentity(${Component.displayName ?? Component.name})`; + const C = (props: React.ComponentProps) => { + const { wrappedComponentRef, ...remainingProps } = props; + + return ( + + {(context) => { + return ( + // @ts-expect-error - Dynamic covariant generic components are tough to type. + + ); + }} + + ); + }; + + C.displayName = displayName; + C.WrappedComponent = Component; + + return hoistStatics(C, Component); +} diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index d8c57a2a0c..5d60565e17 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -44,12 +44,22 @@ * @property {string} sso_redirect */ +/** + * @typedef Role + * @property {string} id + * @property {string} name + * @property {string} permissions + * @property {string} color + * @property {boolean} highlighted + */ + /** * @typedef InitialState * @property {Record} accounts * @property {InitialStateLanguage[]} languages * @property {boolean=} critical_updates_pending * @property {InitialStateMeta} meta + * @property {Role?} role */ const element = document.getElementById('initial-state'); diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index 313563bdfe..47c64e3f0d 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -19,7 +19,7 @@ "account.block_domain": "Blocar dominio {domain}", "account.block_short": "Blocar", "account.blocked": "Blocate", - "account.browse_more_on_origin_server": "Navigar plus sur le profilo original", + "account.browse_more_on_origin_server": "Percurrer plus sur le profilo original", "account.cancel_follow_request": "Cancellar sequimento", "account.copy": "Copiar ligamine a profilo", "account.direct": "Mentionar privatemente @{name}", @@ -122,7 +122,7 @@ "column.direct": "Mentiones private", "column.directory": "Navigar profilos", "column.domain_blocks": "Dominios blocate", - "column.favourites": "Favoritos", + "column.favourites": "Favorites", "column.firehose": "Fluxos in directo", "column.follow_requests": "Requestas de sequimento", "column.home": "Initio", @@ -204,7 +204,7 @@ "disabled_account_banner.account_settings": "Parametros de conto", "disabled_account_banner.text": "Tu conto {disabledAccount} es actualmente disactivate.", "dismissable_banner.community_timeline": "Ecce le messages public le plus recente del personas con contos sur {domain}.", - "dismissable_banner.dismiss": "Dimitter", + "dismissable_banner.dismiss": "Clauder", "dismissable_banner.explore_links": "Istes es le articulos de novas que se condivide le plus sur le rete social hodie. Le articulos de novas le plus recente, publicate per plus personas differente, se classifica plus in alto.", "dismissable_banner.explore_statuses": "Ecce le messages de tote le rete social que gania popularitate hodie. Le messages plus nove con plus impulsos e favorites se classifica plus in alto.", "dismissable_banner.explore_tags": "Ecce le hashtags que gania popularitate sur le rete social hodie. Le hashtags usate per plus personas differente se classifica plus in alto.", @@ -212,8 +212,8 @@ "domain_block_modal.block": "Blocar le servitor", "domain_block_modal.block_account_instead": "Blocar @{name} in su loco", "domain_block_modal.they_can_interact_with_old_posts": "Le personas de iste servitor pote interager con tu messages ancian.", - "domain_block_modal.they_cant_follow": "Nulle persona ab iste servitor pote sequer te.", - "domain_block_modal.they_wont_know": "Illes non sapera que illes ha essite blocate.", + "domain_block_modal.they_cant_follow": "Necuno de iste servitor pote sequer te.", + "domain_block_modal.they_wont_know": "Ille non sapera que ille ha essite blocate.", "domain_block_modal.title": "Blocar dominio?", "domain_block_modal.you_will_lose_followers": "Omne sequitores ab iste servitor essera removite.", "domain_block_modal.you_wont_see_posts": "Tu non videra messages e notificationes ab usatores sur iste servitor.", @@ -307,7 +307,7 @@ "follow_request.reject": "Rejectar", "follow_requests.unlocked_explanation": "Benque tu conto non es serrate, le personal de {domain} pensa que es un bon idea que tu revide manualmente le sequente requestas de iste contos.", "follow_suggestions.curated_suggestion": "Selection del equipa", - "follow_suggestions.dismiss": "Non monstrar novemente", + "follow_suggestions.dismiss": "Non monstrar de novo", "follow_suggestions.featured_longer": "Seligite con cura per le equipa de {domain}", "follow_suggestions.friends_of_friends_longer": "Popular inter le gente que tu seque", "follow_suggestions.hints.featured": "Iste profilo ha essite seligite manualmente per le equipa de {domain}.", @@ -412,7 +412,7 @@ "lightbox.next": "Sequente", "lightbox.previous": "Precedente", "limited_account_hint.action": "Monstrar profilo in omne caso", - "limited_account_hint.title": "Iste profilo esseva celate per le moderatores de {domain}.", + "limited_account_hint.title": "Iste profilo ha essite celate per le moderatores de {domain}.", "link_preview.author": "Per {name}", "lists.account.add": "Adder al lista", "lists.account.remove": "Remover del lista", @@ -432,12 +432,12 @@ "loading_indicator.label": "Cargante…", "media_gallery.toggle_visible": "{number, plural, one {Celar imagine} other {Celar imagines}}", "moved_to_account_banner.text": "Tu conto {disabledAccount} es actualmente disactivate perque tu ha cambiate de conto a {movedToAccount}.", - "mute_modal.hide_from_notifications": "Celar ab notificationes", + "mute_modal.hide_from_notifications": "Celar in notificationes", "mute_modal.hide_options": "Celar optiones", "mute_modal.indefinite": "Usque io dissilentia iste persona", "mute_modal.show_options": "Monstrar optiones", - "mute_modal.they_can_mention_and_follow": "Illes pote mentionar te e sequer te, ma tu non potera vider los.", - "mute_modal.they_wont_know": "Illes non sapera que illes ha essite silentiate.", + "mute_modal.they_can_mention_and_follow": "Ille pote mentionar te e sequer te, ma tu non potera vider le.", + "mute_modal.they_wont_know": "Ille non sapera que ille ha essite silentiate.", "mute_modal.title": "Silentiar le usator?", "mute_modal.you_wont_see_mentions": "Tu non videra le messages que mentiona iste persona.", "mute_modal.you_wont_see_posts": "Iste persona pote totevia vider tu messages, ma tu non videra le sues.", @@ -451,13 +451,13 @@ "navigation_bar.discover": "Discoperir", "navigation_bar.domain_blocks": "Dominios blocate", "navigation_bar.explore": "Explorar", - "navigation_bar.favourites": "Favoritos", + "navigation_bar.favourites": "Favorites", "navigation_bar.filters": "Parolas silentiate", "navigation_bar.follow_requests": "Requestas de sequimento", "navigation_bar.followed_tags": "Hashtags sequite", "navigation_bar.follows_and_followers": "Sequites e sequitores", "navigation_bar.lists": "Listas", - "navigation_bar.logout": "Clauder le session", + "navigation_bar.logout": "Clauder session", "navigation_bar.mutes": "Usatores silentiate", "navigation_bar.opened_in_classic_interface": "Messages, contos e altere paginas specific es aperite per predefinition in le interfacie web classic.", "navigation_bar.personal": "Personal", @@ -501,7 +501,7 @@ "notifications.column_settings.admin.report": "Nove signalationes:", "notifications.column_settings.admin.sign_up": "Nove inscriptiones:", "notifications.column_settings.alert": "Notificationes de scriptorio", - "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.favourite": "Favorites:", "notifications.column_settings.filter_bar.advanced": "Monstrar tote le categorias", "notifications.column_settings.filter_bar.category": "Barra de filtro rapide", "notifications.column_settings.follow": "Nove sequitores:", @@ -518,7 +518,7 @@ "notifications.column_settings.update": "Modificationes:", "notifications.filter.all": "Toto", "notifications.filter.boosts": "Impulsos", - "notifications.filter.favourites": "Favoritos", + "notifications.filter.favourites": "Favorites", "notifications.filter.follows": "Sequites", "notifications.filter.mentions": "Mentiones", "notifications.filter.polls": "Resultatos del sondage", @@ -717,7 +717,7 @@ "status.edited": "Ultime modification le {date}", "status.edited_x_times": "Modificate {count, plural, one {{count} vice} other {{count} vices}}", "status.embed": "Incastrar", - "status.favourite": "Adder al favoritos", + "status.favourite": "Adder al favorites", "status.favourites": "{count, plural, one {favorite} other {favorites}}", "status.filter": "Filtrar iste message", "status.filtered": "Filtrate", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index cd09a505b3..07a41385a2 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -468,6 +468,7 @@ "notification.follow": "{name} подписался (-лась) на вас", "notification.follow_request": "{name} отправил запрос на подписку", "notification.mention": "{name} упомянул(а) вас", + "notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.", "notification.own_poll": "Ваш опрос закончился", "notification.poll": "Опрос, в котором вы приняли участие, завершился", "notification.reblog": "{name} продвинул(а) ваш пост", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 459d05ce3e..7806abc6b5 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -474,6 +474,7 @@ "notification.follow_request": "{name} vam želi slediti", "notification.mention": "{name} vas je omenil/a", "notification.moderation-warning.learn_more": "Več o tem", + "notification.moderation_warning": "Prejeli ste opozorilo moderatorjev", "notification.moderation_warning.action_delete_statuses": "Nekatere vaše objave so odstranjene.", "notification.moderation_warning.action_disable": "Vaš račun je bil onemogočen.", "notification.moderation_warning.action_mark_statuses_as_sensitive": "Nekatere vaše objave so bile označene kot občutljive.", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 7c6b2ade4e..b1b9407ba1 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -158,7 +158,7 @@ "compose_form.poll.option_placeholder": "ตัวเลือก {number}", "compose_form.poll.single": "เลือกอย่างใดอย่างหนึ่ง", "compose_form.poll.switch_to_multiple": "เปลี่ยนการสำรวจความคิดเห็นเป็นอนุญาตหลายตัวเลือก", - "compose_form.poll.switch_to_single": "เปลี่ยนการสำรวจความคิดเห็นเป็นอนุญาตตัวเลือกเดี่ยว", + "compose_form.poll.switch_to_single": "เปลี่ยนการสำรวจความคิดเห็นเป็นอนุญาตตัวเลือกเดียว", "compose_form.poll.type": "ลักษณะ", "compose_form.publish": "โพสต์", "compose_form.publish_form": "โพสต์ใหม่", diff --git a/app/javascript/mastodon/test_helpers.tsx b/app/javascript/mastodon/test_helpers.tsx index 69d57b95a0..bfea3f6bf4 100644 --- a/app/javascript/mastodon/test_helpers.tsx +++ b/app/javascript/mastodon/test_helpers.tsx @@ -1,7 +1,3 @@ -import PropTypes from 'prop-types'; -import type { PropsWithChildren } from 'react'; -import { Component } from 'react'; - import { IntlProvider } from 'react-intl'; import { MemoryRouter } from 'react-router'; @@ -9,44 +5,27 @@ import { MemoryRouter } from 'react-router'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as rtlRender } from '@testing-library/react'; -class FakeIdentityWrapper extends Component< - PropsWithChildren<{ signedIn: boolean }> -> { - static childContextTypes = { - identity: PropTypes.shape({ - signedIn: PropTypes.bool.isRequired, - accountId: PropTypes.string, - disabledAccountId: PropTypes.string, - accessToken: PropTypes.string, - }).isRequired, - }; - - getChildContext() { - return { - identity: { - signedIn: this.props.signedIn, - accountId: '123', - accessToken: 'test-access-token', - }, - }; - } - - render() { - return this.props.children; - } -} +import { IdentityContext } from './identity_context'; function render( ui: React.ReactElement, { locale = 'en', signedIn = true, ...renderOptions } = {}, ) { + const fakeIdentity = { + signedIn: signedIn, + accountId: '123', + accessToken: 'test-access-token', + disabledAccountId: undefined, + permissions: 0, + }; + const Wrapper = (props: { children: React.ReactNode }) => { return ( - + {props.children} - + ); diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index 400c51a023..2fea1057cb 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -23,6 +23,12 @@ module ApplicationExtension redirect_uri.lines.first.strip end + def redirect_uris + # Doorkeeper stores the redirect_uri value as a newline delimeted list in + # the database: + redirect_uri.split + end + def push_to_streaming_api # TODO: #28793 Combine into a single topic payload = Oj.dump(event: :kill) diff --git a/app/lib/vacuum/imports_vacuum.rb b/app/lib/vacuum/imports_vacuum.rb index 8c8bb783ac..700bd81847 100644 --- a/app/lib/vacuum/imports_vacuum.rb +++ b/app/lib/vacuum/imports_vacuum.rb @@ -9,10 +9,10 @@ class Vacuum::ImportsVacuum private def clean_unconfirmed_imports! - BulkImport.state_unconfirmed.where('created_at <= ?', 10.minutes.ago).reorder(nil).in_batches.delete_all + BulkImport.state_unconfirmed.where(created_at: ..10.minutes.ago).reorder(nil).in_batches.delete_all end def clean_old_imports! - BulkImport.where('created_at <= ?', 1.week.ago).reorder(nil).in_batches.delete_all + BulkImport.where(created_at: ..1.week.ago).reorder(nil).in_batches.delete_all end end diff --git a/app/lib/vacuum/statuses_vacuum.rb b/app/lib/vacuum/statuses_vacuum.rb index ad1de07380..92d3ccf4f4 100644 --- a/app/lib/vacuum/statuses_vacuum.rb +++ b/app/lib/vacuum/statuses_vacuum.rb @@ -34,7 +34,7 @@ class Vacuum::StatusesVacuum def statuses_scope Status.unscoped.kept .joins(:account).merge(Account.remote) - .where('statuses.id < ?', retention_period_as_id) + .where(statuses: { id: ...retention_period_as_id }) end def retention_period_as_id diff --git a/app/models/concerns/expireable.rb b/app/models/concerns/expireable.rb index c64fc7d807..26740e8213 100644 --- a/app/models/concerns/expireable.rb +++ b/app/models/concerns/expireable.rb @@ -4,7 +4,7 @@ module Expireable extend ActiveSupport::Concern included do - scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) } + scope :expired, -> { where.not(expires_at: nil).where(expires_at: ...Time.now.utc) } def expires_in return @expires_in if defined?(@expires_in) diff --git a/app/models/invite.rb b/app/models/invite.rb index 2fe9f22fbe..ea095a3ac1 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -24,7 +24,7 @@ class Invite < ApplicationRecord belongs_to :user, inverse_of: :invites has_many :users, inverse_of: :invite, dependent: nil - scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) } + scope :available, -> { where(expires_at: nil).or(where(expires_at: Time.now.utc..)) } validates :comment, length: { maximum: COMMENT_SIZE_LIMIT } diff --git a/app/policies/backup_policy.rb b/app/policies/backup_policy.rb index 86b8efbe96..7a4c5b4347 100644 --- a/app/policies/backup_policy.rb +++ b/app/policies/backup_policy.rb @@ -4,6 +4,6 @@ class BackupPolicy < ApplicationPolicy MIN_AGE = 6.days def create? - user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero? + user_signed_in? && current_user.backups.where(created_at: MIN_AGE.ago..).count.zero? end end diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb index 635508a17c..1a7b9265f1 100644 --- a/app/serializers/rest/application_serializer.rb +++ b/app/serializers/rest/application_serializer.rb @@ -1,24 +1,18 @@ # frozen_string_literal: true class REST::ApplicationSerializer < ActiveModel::Serializer - attributes :id, :name, :website, :scopes, :redirect_uri, - :client_id, :client_secret + attributes :id, :name, :website, :scopes, :redirect_uris # NOTE: Deprecated in 4.3.0, needs to be removed in 5.0.0 attribute :vapid_key + # We should consider this property deprecated for 4.3.0 + attribute :redirect_uri + def id object.id.to_s end - def client_id - object.uid - end - - def client_secret - object.secret - end - def website object.website.presence end diff --git a/app/serializers/rest/credential_application_serializer.rb b/app/serializers/rest/credential_application_serializer.rb new file mode 100644 index 0000000000..bfec7d03e8 --- /dev/null +++ b/app/serializers/rest/credential_application_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class REST::CredentialApplicationSerializer < REST::ApplicationSerializer + attributes :client_id, :client_secret + + def client_id + object.uid + end + + def client_secret + object.secret + end +end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index c6b600dd7c..36e866b6ce 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -56,7 +56,7 @@ class FetchLinkCardService < BaseService @html_charset = res.charset - res.body_with_limit + res.truncated_body end end diff --git a/app/workers/scheduler/ip_cleanup_scheduler.rb b/app/workers/scheduler/ip_cleanup_scheduler.rb index f78c0584d7..04fb0aaa33 100644 --- a/app/workers/scheduler/ip_cleanup_scheduler.rb +++ b/app/workers/scheduler/ip_cleanup_scheduler.rb @@ -16,11 +16,11 @@ class Scheduler::IpCleanupScheduler private def clean_ip_columns! - SessionActivation.where('updated_at < ?', SESSION_RETENTION_PERIOD.ago).in_batches.destroy_all - SessionActivation.where('updated_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(ip: nil) - User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(sign_up_ip: nil) - LoginActivity.where('created_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all - Doorkeeper::AccessToken.where('last_used_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(last_used_ip: nil) + SessionActivation.where(updated_at: ...SESSION_RETENTION_PERIOD.ago).in_batches.destroy_all + SessionActivation.where(updated_at: ...IP_RETENTION_PERIOD.ago).in_batches.update_all(ip: nil) + User.where(current_sign_in_at: ...IP_RETENTION_PERIOD.ago).in_batches.update_all(sign_up_ip: nil) + LoginActivity.where(created_at: ...IP_RETENTION_PERIOD.ago).in_batches.destroy_all + Doorkeeper::AccessToken.where(last_used_at: ...IP_RETENTION_PERIOD.ago).in_batches.update_all(last_used_ip: nil) end def clean_expired_ip_blocks! diff --git a/app/workers/scheduler/scheduled_statuses_scheduler.rb b/app/workers/scheduler/scheduled_statuses_scheduler.rb index fe60d5524e..4e251780de 100644 --- a/app/workers/scheduler/scheduled_statuses_scheduler.rb +++ b/app/workers/scheduler/scheduled_statuses_scheduler.rb @@ -20,7 +20,7 @@ class Scheduler::ScheduledStatusesScheduler end def due_statuses - ScheduledStatus.where('scheduled_at <= ?', Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET) + ScheduledStatus.where(scheduled_at: ..Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET) end def publish_scheduled_announcements! diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb index 63ea876e50..74abc23701 100644 --- a/app/workers/scheduler/user_cleanup_scheduler.rb +++ b/app/workers/scheduler/user_cleanup_scheduler.rb @@ -25,7 +25,7 @@ class Scheduler::UserCleanupScheduler end def clean_discarded_statuses! - Status.unscoped.discarded.where('deleted_at <= ?', DISCARDED_STATUSES_MAX_AGE_DAYS.days.ago).find_in_batches do |statuses| + Status.unscoped.discarded.where(deleted_at: ..DISCARDED_STATUSES_MAX_AGE_DAYS.days.ago).find_in_batches do |statuses| RemovalWorker.push_bulk(statuses) do |status| [status.id, { 'immediate' => true, 'skip_streaming' => true }] end diff --git a/config/initializers/opentelemetry.rb b/config/initializers/opentelemetry.rb index 9af0ab89c8..cf9f0b96f3 100644 --- a/config/initializers/opentelemetry.rb +++ b/config/initializers/opentelemetry.rb @@ -51,6 +51,9 @@ if ENV.keys.any? { |name| name.match?(/OTEL_.*_ENDPOINT/) } use_rack_events: false, # instead of events, use middleware; allows for untraced_endpoints to ignore child spans untraced_endpoints: ['/health'], }, + 'OpenTelemetry::Instrumentation::Sidekiq' => { + span_naming: :job_class, # Use the job class as the span name, otherwise this is the queue name and not very helpful + }, }) prefix = ENV.fetch('OTEL_SERVICE_NAME_PREFIX', 'mastodon') diff --git a/config/locales/bg.yml b/config/locales/bg.yml index f242039ed8..51180bc66f 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -285,6 +285,7 @@ bg: update_custom_emoji_html: "%{name} обнови емоджито %{target}" update_domain_block_html: "%{name} обнови блокирането на домейна за %{target}" update_ip_block_html: "%{name} промени правило за IP на %{target}" + update_report_html: "%{name} осъвремени доклад %{target}" update_status_html: "%{name} обнови публикация от %{target}" update_user_role_html: "%{name} промени ролята %{target}" deleted_account: изтрит акаунт diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 08fef73642..34fd900851 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -285,6 +285,7 @@ ca: update_custom_emoji_html: "%{name} ha actualitzat l'emoji %{target}" update_domain_block_html: "%{name} ha actualitzat el bloqueig de domini per a %{target}" update_ip_block_html: "%{name} ha canviat la norma per la IP %{target}" + update_report_html: "%{name} ha actualitzat l'informe %{target}" update_status_html: "%{name} ha actualitzat l'estat de %{target}" update_user_role_html: "%{name} ha canviat el rol %{target}" deleted_account: compte eliminat diff --git a/config/locales/da.yml b/config/locales/da.yml index 252d0e2b58..17d3037a75 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -285,6 +285,7 @@ da: update_custom_emoji_html: "%{name} opdaterede emoji %{target}" update_domain_block_html: "%{name} opdaterede domæneblokeringen for %{target}" update_ip_block_html: "%{name} ændrede reglen for IP'en %{target}" + update_report_html: "%{name} opdaterede rapporten %{target}" update_status_html: "%{name} opdaterede indlægget fra %{target}" update_user_role_html: "%{name} ændrede %{target}-rolle" deleted_account: slettet konto diff --git a/config/locales/de.yml b/config/locales/de.yml index b19315e394..dd2129584f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -285,6 +285,7 @@ de: update_custom_emoji_html: "%{name} bearbeitete das Emoji %{target}" update_domain_block_html: "%{name} aktualisierte die Domain-Sperre für %{target}" update_ip_block_html: "%{name} änderte die Regel für die IP-Adresse %{target}" + update_report_html: "%{name} überarbeitete die Meldung %{target}" update_status_html: "%{name} überarbeitete einen Beitrag von %{target}" update_user_role_html: "%{name} änderte die Rolle von %{target}" deleted_account: gelöschtes Konto diff --git a/config/locales/devise.ia.yml b/config/locales/devise.ia.yml index c68efddd07..e6ae6d4afb 100644 --- a/config/locales/devise.ia.yml +++ b/config/locales/devise.ia.yml @@ -6,11 +6,11 @@ ia: send_instructions: Tu recipera un e-mail con instructiones pro confirmar tu adresse de e-mail in poc minutas. Per favor verifica tu dossier de spam si tu non lo recipe. send_paranoid_instructions: Si tu adresse de e-mail existe in nostre base de datos, tu recipera un e-mail con instructiones pro confirmar tu adresse de e-mail in poc minutas. Per favor verifica tu dossier de spam si tu non lo recipe. failure: - already_authenticated: Tu jam initiava le session. - inactive: Tu conto ancora non es activate. + already_authenticated: Tu ha jam aperite session. + inactive: Tu conto non es ancora activate. invalid: "%{authentication_keys} o contrasigno non valide." last_attempt: Tu ha solmente un altere tentativa ante que tu conto es serrate. - locked: Tu conto es blocate. + locked: Tu conto es serrate. not_found_in_database: "%{authentication_keys} o contrasigno non valide." omniauth_user_creation_failure: Error creante un conto pro iste identitate. pending: Tu conto es ancora sub revision. @@ -51,12 +51,12 @@ ia: explanation: Ora es possibile aperir session con solmente le adresse de e-mail e contrasigno. subject: 'Mastodon: Authentication bifactorial disactivate' subtitle: Le authentication bifactorial ha essite disactivate pro tu conto. - title: 2FA disactivate + title: A2F disactivate two_factor_enabled: explanation: Pro le apertura de session essera necessari un token generate per le application TOTP accopulate. subject: 'Mastodon: Authentication bifactorial activate' subtitle: Le authentication bifactorial ha essite activate pro tu conto. - title: 2FA activate + title: A2F activate two_factor_recovery_codes_changed: explanation: Le ancian codices de recuperation ha essite invalidate e nove codices ha essite generate. subject: 'Mastodon: Codices de recuperation regenerate' @@ -66,11 +66,11 @@ ia: subject: 'Mastodon: Instructiones pro disblocar' webauthn_credential: added: - explanation: Le sequente clave de securitate esseva addite a tu conto + explanation: Le sequente clave de securitate ha essite addite a tu conto subject: 'Mastodon: Nove clave de securitate' - title: Un nove clave de securitate esseva addite + title: Un nove clave de securitate ha essite addite deleted: - explanation: Le sequente clave de securitate esseva delite de tu conto + explanation: Le sequente clave de securitate ha essite delite de tu conto subject: 'Mastodon: Clave de securitate delite' title: Un de tu claves de securitate ha essite delite webauthn_disabled: @@ -81,18 +81,41 @@ ia: webauthn_enabled: explanation: Le authentication con claves de securitate ha essite activate pro tu conto. extra: Tu clave de securitate pote ora esser usate pro aperir session. + subject: 'Mastodon: authentication de clave de securitate activate' title: Claves de securitate activate + omniauth_callbacks: + failure: Impossibile authenticar te ab %{kind} perque “%{reason}”. + success: Authenticate con successo ab conto %{kind}. + passwords: + no_token: Tu non pote acceder iste pagina sin venir ab un email de redefinition de contrasigno. Si tu veni ab un email de redefinition de contrasigno, verifica que tu usava le integre URL fornite. + send_instructions: Si tu adresse de e-mail existe in nostre base de datos, tu recipera un ligamine de recuperation de contrasigno in tu adresse de e-mail in poc minutas. Per favor verifica tu dossier de spam si tu non lo recipe. + send_paranoid_instructions: Si tu adresse de e-mail existe in nostre base de datos, tu recipera un ligamine de recuperation de contrasigno in tu adresse de e-mail in poc minutas. Per favor verifica tu dossier de spam si tu non lo recipe. + updated: Tu contrasigno ha essite cambiate. Tu ha ora aperite session. + updated_not_active: Tu contrasigno ha essite cambiate. registrations: - destroyed: A revider! Tu conto esseva cancellate con successo. Nos spera vider te novemente tosto. - signed_up_but_pending: Un message con un ligamine de confirmation esseva inviate a tu conto de email. Post que tu clicca le ligamine, nos revidera tu application. Tu essera notificate si illo es approbate. + destroyed: A revider! Tu conto ha essite cancellate. Nos spera vider te de novo tosto. + signed_up: Benvenite! Tu te ha inscribite con successo. + signed_up_but_inactive: Tu te ha inscribite con successo. Nonobstante, nos non poteva aperir tu session perque tu conto non es ancora activate. + signed_up_but_locked: Tu te ha inscribite con successo. Nonobstante, nos non poteva aperir tu session perque tu conto es serrate. + signed_up_but_pending: Un message con un ligamine de confirmation ha essite inviate a tu adresse de email. Post que tu clicca sur le ligamine, nos revidera tu demanda. Tu essera notificate si illo es approbate. + signed_up_but_unconfirmed: Un message con un ligamine de confirmation ha essite inviate a tu adresse de e-mail. Per favor seque le ligamine pro activar tu conto. Verifica tu dossier de spam si tu non recipe iste e-mail. + update_needs_confirmation: Tu ha actualisate tu conto con successo, ma nos debe verificar tu nove adresse de e-mail. Accede a tu e-mail e seque le ligamine de confirmation pro confirmar tu nove adresse de e-mail. Verifica tu dossier de spam si tu non recipe iste e-mail. updated: Tu conto ha essite actualisate con successo. sessions: - signed_in: Connexe con successo. - signed_out: Disconnexe con successo. + already_signed_out: Session claudite con successo. + signed_in: Session aperite con successo. + signed_out: Session claudite con successo. unlocks: - unlocked: Tu conto ha essite disblocate con successo. Initia session a continuar. + send_instructions: Tu recipera un e-mail con instructiones explicante como disserrar tu conto in alcun minutas. Verifica tu dossier de spam si tu non recipe iste e-mail. + send_paranoid_instructions: Si tu conto existe, tu recipera un email con instructiones explicante como disserrar lo in alcun minutas. Verifica tu dossier de spam si tu non recipe iste e-mail. + unlocked: Tu conto ha essite disserrate con successo. Aperi session pro continuar. errors: messages: - already_confirmed: jam esseva confirmate, tenta initiar session + already_confirmed: jam esseva confirmate, tenta aperir session + confirmation_period_expired: debe esser confirmate in %{period}, per favor requesta un nove + expired: ha expirate, per favor requesta un nove not_found: non trovate - not_locked: non era blocate + not_locked: non esseva serrate + not_saved: + one: '1 error ha impedite a iste %{resource} de esser salvate:' + other: "%{count} errores ha impedite a iste %{resource} de esser salvate:" diff --git a/config/locales/doorkeeper.ia.yml b/config/locales/doorkeeper.ia.yml index dc96599681..9c493e3d7f 100644 --- a/config/locales/doorkeeper.ia.yml +++ b/config/locales/doorkeeper.ia.yml @@ -3,28 +3,40 @@ ia: activerecord: attributes: doorkeeper/application: - name: Nomine de application + name: Nomine del application + redirect_uri: URI de redirection scopes: Ambitos - website: Sito web de application + website: Sito web del application errors: models: doorkeeper/application: attributes: redirect_uri: + fragment_present: non pote continer un fragmento. invalid_uri: debe esser un URI valide. + relative_uri: debe esser un URI absolute. + secured_uri: debe esser un URI HTTPS/SSL. doorkeeper: applications: buttons: authorize: Autorisar cancel: Cancellar + destroy: Destruer edit: Modificar submit: Submitter confirmations: destroy: Es tu secur? edit: title: Modificar application + form: + error: Oops! Verifica tu formulario pro possibile errores + help: + native_redirect_uri: Usar %{native_redirect_uri} pro tests local + redirect_uri: Usar un linea per URI + scopes: Separa ambitos con spatios. Lassa vacue pro usar le ambitos predefinite. index: application: Application + callback_url: URL de retorno delete: Deler empty: Tu non ha applicationes. name: Nomine @@ -37,17 +49,22 @@ ia: show: actions: Actiones application_id: Clave del cliente + callback_urls: URLs de retorno scopes: Ambitos + secret: Secreto del application title: 'Application: %{name}' authorizations: buttons: authorize: Autorisar deny: Negar error: - title: Ocurreva un error + title: Un error ha occurrite new: + prompt_html: "%{client_name} vole haber le permission de acceder a tu conto. Illo es un application tertie. Si tu non confide in illo, alora tu non deberea autorisar lo." review_permissions: Revisionar le permissos title: Autorisation necessari + show: + title: Copia iste codice de autorisation e colla lo in le application. authorized_applications: buttons: revoke: Revocar @@ -55,11 +72,35 @@ ia: revoke: Es tu secur? index: authorized_at: Autorisate le %{date} + description_html: Ecce applicationes que pote acceder tu conto per le API. Si il ha applicationes que tu non recognosce ci, o un application que se comporta mal, tu pote revocar su accesso. last_used_at: Ultime uso in %{date} never_used: Nunquam usate scopes: Permissiones superapp: Interne title: Tu applicationes autorisate + errors: + messages: + access_denied: Le proprietario del ressource o servitor de autorisation ha refusate le requesta. + credential_flow_not_configured: Le processo de credentiales de contrasigno del proprietario del ressource ha fallite perque Doorkeeper.configure.resource_owner_from_credentials non es configurate. + invalid_client: Le authentication del cliente ha fallite perque le cliente es incognite, necun authentication de cliente es includite, o le methodo de authentication non es supportate. + invalid_grant: Le concession de autorisation fornite es invalide, expirate, revocate, non corresponde al URI de redirection usate in le requesta de autorisation, o ha essite emittite a un altere cliente. + invalid_redirect_uri: Le URI de redirection includite non es valide. + invalid_request: + missing_param: 'Parametro requirite mancante: %{value}.' + request_not_authorized: Le requesta debe esser autorisate. Un parametro requirite pro autorisar le requesta manca o non es valide. + unknown: Le requesta non include un parametro requirite, include un valor de parametro non supportate, o es alteremente mal formate. + invalid_resource_owner: Le credentiales del proprietario del ressource fornite non es valide, o le proprietario del ressource non pote esser trovate + invalid_scope: Le ambito requirite es invalide, incognite, o mal formate. + invalid_token: + expired: Le token de accesso ha expirate + revoked: Le token de accesso ha essite revocate + unknown: Le token de accesso non es valide + resource_owner_authenticator_not_configured: Impossibile trovar le proprietario del ressource perque Doorkeeper.configure.resource_owner_authenticator non es configurate. + server_error: Le servitor de autorisation ha incontrate un condition impreviste que lo ha impedite de complir le requesta. + temporarily_unavailable: Le servitor de autorisation actualmente non pote gerer le requesta a causa de un supercarga temporari o de mantenentia del servitor. + unauthorized_client: Le application non es autorisate a exequer iste requesta usante iste methodo. + unsupported_grant_type: Le typo de concession de autorisation non es supportate per le servitor de autorisation. + unsupported_response_type: Le servitor de autorisation non supporta iste typo de responsa. flash: applications: create: @@ -73,20 +114,22 @@ ia: notice: Application revocate. grouped_scopes: access: - read: Accesso de sol lectura + read: Accesso de lectura sol read/write: Accesso de lectura e scriptura - write: Accesso de sol scriptura + write: Accesso de scriptura sol title: accounts: Contos admin/accounts: Gestion de contos admin/all: Tote le functiones administrative admin/reports: Gestion de reportos - all: Accesso plen a tu conto de Mastodon + all: Accesso complete a tu conto de Mastodon blocks: Blocadas bookmarks: Marcapaginas conversations: Conversationes - favourites: Favoritos + crypto: Cryptation de puncta a puncta + favourites: Favorites filters: Filtros + follow: Sequites, silentiates e blocates follows: Sequites lists: Listas media: Annexos multimedial @@ -101,21 +144,41 @@ ia: nav: applications: Applicationes oauth2_provider: Fornitor OAuth2 + application: + title: Autorisation OAuth necessari scopes: admin:read: leger tote le datos in le servitor admin:read:accounts: leger information sensibile de tote le contos + admin:read:canonical_email_blocks: leger datos sensibile de tote le blocadas de email canonic + admin:read:domain_allows: leger informationes sensibile de tote le dominios permittite + admin:read:domain_blocks: leger informationes sensibile de tote le blocadas de dominio + admin:read:email_domain_blocks: leger informationes sensibile de tote le blocadas de dominio email + admin:read:ip_blocks: leger informationes sensibile de tote le blocadas de IP + admin:read:reports: leger information sensibile de tote le reportos e contos signalate admin:write: modificar tote le datos in le servitor + admin:write:accounts: exequer action de moderation sur contos + admin:write:canonical_email_blocks: exequer actiones de moderation sur blocadas de email canonic + admin:write:domain_allows: exequer actiones de moderation sur dominios permittite + admin:write:domain_blocks: exequer actiones de moderation sur blocadas de dominio + admin:write:email_domain_blocks: exequer actiones de moderation sur blocadas de dominio email + admin:write:ip_blocks: exequer actiones de moderation sur blocadas de IP + admin:write:reports: exequer action de moderation sur reportos + crypto: usar cryptation de extremo-a-extremo follow: modificar relationes del contos + push: reciper tu notificationes push read: leger tote le datos de tu conto read:accounts: vider informationes de conto + read:blocks: vider tu blocadas read:bookmarks: vider tu marcapaginas read:favourites: vider tu favoritos read:filters: vider tu filtros read:follows: vider tu sequites read:lists: vider tu listas read:me: leger solmente le information basic de tu conto + read:mutes: vider tu silentiates read:notifications: vider tu notificationes read:reports: vider tu reportos + read:search: cercar in tu nomine read:statuses: vider tote le messages write: modificar tote le datos de tu conto write:accounts: modificar tu profilo diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index aa3668d92a..302be44112 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -285,6 +285,7 @@ es-AR: update_custom_emoji_html: "%{name} actualizó el emoji %{target}" update_domain_block_html: "%{name} actualizó el bloqueo de dominio para %{target}" update_ip_block_html: "%{name} cambió la regla para la dirección IP %{target}" + update_report_html: "%{name} actualizó la denuncia %{target}" update_status_html: "%{name} actualizó el mensaje de %{target}" update_user_role_html: "%{name} cambió el rol %{target}" deleted_account: cuenta eliminada diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 6a306b07b0..10806c6b6c 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -285,6 +285,7 @@ es-MX: update_custom_emoji_html: "%{name} actualizó el emoji %{target}" update_domain_block_html: "%{name} actualizó el bloqueo de dominio para %{target}" update_ip_block_html: "%{name} cambió la regla para la IP %{target}" + update_report_html: "%{name} actualizó el informe %{target}" update_status_html: "%{name} actualizó el estado de %{target}" update_user_role_html: "%{name} cambió el rol %{target}" deleted_account: cuenta eliminada diff --git a/config/locales/es.yml b/config/locales/es.yml index e7db7c8b04..840bc2ce9b 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -285,6 +285,7 @@ es: update_custom_emoji_html: "%{name} actualizó el emoji %{target}" update_domain_block_html: "%{name} actualizó el bloqueo de dominio para %{target}" update_ip_block_html: "%{name} cambió la regla para la IP %{target}" + update_report_html: "%{name} actualizó el informe %{target}" update_status_html: "%{name} actualizó la publicación de %{target}" update_user_role_html: "%{name} cambió el rol %{target}" deleted_account: cuenta eliminada diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 5f96f611b4..53db0232aa 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -285,6 +285,7 @@ fi: update_custom_emoji_html: "%{name} päivitti emojin %{target}" update_domain_block_html: "%{name} päivitti verkkotunnuksen %{target} eston" update_ip_block_html: "%{name} muutti sääntöä IP-osoitteelle %{target}" + update_report_html: "%{name} päivitti raportin %{target}" update_status_html: "%{name} päivitti käyttäjän %{target} julkaisun" update_user_role_html: "%{name} muutti roolia %{target}" deleted_account: poisti tilin diff --git a/config/locales/fo.yml b/config/locales/fo.yml index f7303c512a..57caff4d71 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -285,6 +285,7 @@ fo: update_custom_emoji_html: "%{name} dagførdi kensluteknið %{target}" update_domain_block_html: "%{name} dagførdi navnaøkisblokeringina hjá %{target}" update_ip_block_html: "%{name} broytti IP-reglurnar %{target}" + update_report_html: "%{name} dagførdi meldingina %{target}" update_status_html: "%{name} dagførdi postin hjá %{target}" update_user_role_html: "%{name} broyttir %{target} leiklutir" deleted_account: strikað konta diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 57af7c82c0..2c85dc89ae 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -285,6 +285,7 @@ gl: update_custom_emoji_html: "%{name} actualizou o emoji %{target}" update_domain_block_html: "%{name} actualizou o bloqueo do dominio para %{target}" update_ip_block_html: "%{name} cambiou a regra para IP %{target}" + update_report_html: "%{name} actualizou a denuncia %{target}" update_status_html: "%{name} actualizou a publicación de %{target}" update_user_role_html: "%{name} cambiou o rol %{target}" deleted_account: conta eliminada diff --git a/config/locales/he.yml b/config/locales/he.yml index 14da27ec75..3613a9f0b3 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -291,6 +291,7 @@ he: update_custom_emoji_html: "%{name} עדכן/ה אמוג'י %{target}" update_domain_block_html: "%{name} עדכן/ה חסימת דומיין עבור %{target}" update_ip_block_html: "%{name} שינה כלל עבור IP %{target}" + update_report_html: '%{name} עדכן/ה דו"ח %{target}' update_status_html: "%{name} עדכן/ה הודעה של %{target}" update_user_role_html: "%{name} שינה את התפקיד של %{target}" deleted_account: חשבון מחוק diff --git a/config/locales/hu.yml b/config/locales/hu.yml index c48d527caf..dd57830515 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -285,6 +285,7 @@ hu: update_custom_emoji_html: "%{name} frissítette az emodzsit: %{target}" update_domain_block_html: "%{name} frissítette a %{target} domain tiltását" update_ip_block_html: "%{name} módosította a(z) %{target} IP-címre vonatkozó szabályt" + update_report_html: "%{name} frissítette a %{target} bejelentést" update_status_html: "%{name} frissítette %{target} felhasználó bejegyzését" update_user_role_html: "%{name} módosította a(z) %{target} szerepkört" deleted_account: törölt fiók diff --git a/config/locales/ia.yml b/config/locales/ia.yml index 68676a09f7..8af676454f 100644 --- a/config/locales/ia.yml +++ b/config/locales/ia.yml @@ -285,6 +285,7 @@ ia: update_custom_emoji_html: "%{name} actualisava le emoticone %{target}" update_domain_block_html: "%{name} actualisava le blocada de dominio pro %{target}" update_ip_block_html: "%{name} cambiava le regula pro IP %{target}" + update_report_html: "%{name} actualisava le reporto %{target}" update_status_html: "%{name} actualisava le message per %{target}" update_user_role_html: "%{name} cambiava le rolo de %{target}" deleted_account: conto delite @@ -973,6 +974,7 @@ ia: webhook: Crocs web admin_mailer: auto_close_registrations: + body: Per un carentia recente de activate de moderator, le registrationes sur %{instance} ha essite automaticamente mutate a besoniante revision manual, pro impedir %{instance} de esser usate como un platteforma pro potential mal actores. Tu pote mutar lo retro pro sempre aperir le registrationes. subject: Le registrationes pro %{instance} ha essite automaticamente mutate a besoniante de approbation new_appeal: actions: @@ -1054,19 +1056,27 @@ ia: clicking_this_link: cliccante iste ligamine login_link: acceder proceed_to_login_html: Ora tu pote continuar a %{login_link}. + redirect_to_app_html: Tu deberea haber essite re-dirigite al app %{app_name}. Si isto non eveni, tenta %{clicking_this_link} o manualmente retorna al app. + registration_complete: Tu registration sur %{domain} es ora complete! welcome_title: Benvenite, %{name}! + wrong_email_hint: Si ille adresse email non es correcte, tu pote cambiar lo in parametros de conto. delete_account: Deler le conto + delete_account_html: Si tu vole a dele tu conto, tu pote continuar ci. Te sera demandate confirmation. description: + prefix_invited_by_user: "@%{name} te invita a junger te a iste servitor de Mastodon!" prefix_sign_up: Inscribe te sur Mastodon hodie! + suffix: Con un conto, tu potera sequer personas, messages de actualisation e excambios de messages con usatores de ulle servitor de Mastodon e plus! didnt_get_confirmation: Non recipeva tu un ligamine de confirmation? dont_have_your_security_key: Non ha tu le clave de securitate? forgot_password: Contrasigno oblidate? invalid_reset_password_token: Pete un nove. + link_to_otp: Insere un codice a duo factores o un codice de recuperation ab tu telephono link_to_webauth: Usa tu apparato clave de securitate log_in_with: Accede con login: Accede logout: Clauder le session migrate_account: Move a un conto differente + migrate_account_html: Si tu vole re-adressar iste conto a un altere, tu pote configurar lo ci. or_log_in_with: O accede con privacy_policy_agreement_html: Io ha legite e acceptar le politica de confidentialitate progress: @@ -1192,6 +1202,7 @@ ia: invalid_domain: non es un nomine de dominio valide edit_profile: basic_information: Information basic + hint_html: "Personalisa lo que le personas vide sur tu profilo public e presso tu messages. Il es plus probabile que altere personas te seque e interage con te quando tu ha un profilo compilate e un photo de profilo." other: Alteres errors: '400': Le requesta que tu inviava era non valide o mal formate. @@ -1230,6 +1241,7 @@ ia: add_new: Adder nove errors: limit: Tu ha jam consiliate le maxime numero de hashtags + hint_html: "Consilia tu plus importante hashtags sur tu profilo. Un grande instrumento pro tener tracia de tu labores creative e projectos de longe-tempore, le hashtags consiliate es monstrate prominentemente sur tu profilo e permitte accesso rapide a tu proprie messages." filters: contexts: account: Profilos @@ -1241,8 +1253,10 @@ ia: add_keyword: Adder parola clave keywords: Parolas clave statuses: Messages individual + statuses_hint_html: Iste filtro se applica a seliger messages singule sin reguardo si illes concorda le parolas clave infra. Revide o remove le messages ab le filtro. title: Modificar filtro errors: + deprecated_api_multiple_keywords: Iste parametros non pote esser cambiate ab iste application perque illos se applica a plus que un sol parola clave del filtro. Usa un application plus recente o le interfacie web. invalid_context: Nulle o non valide contexto supplite index: contexts: Filtros in %{contexts} @@ -1272,6 +1286,12 @@ ia: title: Messages filtrate generic: all: Toto + all_items_on_page_selected_html: + one: "%{count} elemento sur iste pagina es seligite." + other: Tote le %{count} elementos sur iste pagina es seligite. + all_matching_items_selected_html: + one: "%{count} elemento concordante que tu cerca es seligite." + other: Tote le %{count} elementos concordante que tu cerca es seligite. cancel: Cancellar changes_saved_msg: Cambios salveguardate con successo! confirm: Confirmar @@ -1299,13 +1319,24 @@ ia: imported: Importate mismatched_types_warning: Il appare que tu pote haber seligite le typo errate pro iste importation, controla duo vices. modes: + merge: Funder + merge_long: Mantene le registrationes existente e adde illos nove + overwrite: Superscriber overwrite_long: Reimplaciar registros actual con le noves overwrite_preambles: blocking_html: Tu es sur le puncto de reimplaciar tu lista de blocadas per usque a %{total_items} contos proveniente de %{filename}. + bookmarks_html: Tu va reimplaciar tu lista de blocadas per usque a %{total_items} contos proveniente de %{filename}. domain_blocking_html: Tu es sur le puncto de reimplaciar tu lista de blocadas de dominio per usque a %{total_items} dominios proveniente de %{filename}. + following_html: Tu va sequer usque %{total_items} contos de %{filename} e cessar de sequer ulle altere. + lists_html: Tu va reimplaciar tu lista con contentos de %{filename}. Usque %{total_items} contos sera addite a nove listas. + muting_html: Tu va reimplaciar tu lista de contos silentiate con usque %{total_items} contos ab %{filename}. preambles: blocking_html: Tu es sur le puncto de blocar usque a %{total_items} contos a partir de %{filename}. + bookmarks_html: Tu va adder usque %{total_items} messages de %{filename} a tu marcapaginas. domain_blocking_html: Tu es sur le puncto de blocar usque a %{total_items} dominios a partir de %{filename}. + following_html: Tu va blocar usque a %{total_items} dominios ab %{filename}. + lists_html: Tu va adder usque %{total_items} contos ab %{filename} a tu lista. Nove listas sera create si il non ha lista a adder. + muting_html: Tu va silentiar usque %{total_items} contos ab %{filename}. preface: Tu pote importar datos que tu ha exportate de un altere servitor, como un lista de personas que tu seque o bloca. recent_imports: Importationes recente states: @@ -1348,16 +1379,30 @@ ia: expires_in_prompt: Nunquam generate: Generar ligamine de invitation invalid: Iste invitation non es valide + invited_by: 'Tu ha essite invitate per:' max_uses: one: un uso other: "%{count} usos" + max_uses_prompt: Nulle limite + prompt: Genera e comparti ligamines con alteres pro conceder accesso a iste servitor table: expires_at: Expira + uses: Usos title: Invitar personas + lists: + errors: + limit: Tu ha attingite le maxime numero de listas login_activities: authentication_methods: + otp: app pro authentication a duo factores password: contrasigno + sign_in_token: codice de securitate de e-mail webauthn: claves de securitate + description_html: Si tu vide activitate que tu non recognosce, considera de cambiar tu contrasigno e activar le authentication a duo factores. + empty: Nulle chronologia de authentication disponibile + failed_sign_in_html: Tentativa de authentication fallite con %{method} ab %{ip} (%{browser}) + successful_sign_in_html: Apertura de session con successo con %{method} ab %{ip} (%{browser}) + title: Chronologia de authentication mail_subscriptions: unsubscribe: action: Si, desubscriber @@ -1373,32 +1418,110 @@ ia: resubscribe_html: Si tu ha cancellate le subscription in error, tu pote resubscriber te a partir del parametros de notification in e-mail. success_html: Tu non recipera plus %{type} pro Mastodon sur %{domain} a tu adresse de e-mail %{email}. title: Desubcriber + media_attachments: + validations: + images_and_video: Impossibile annexar un video a un message que jam contine imagines + not_ready: Impossibile annexar un video a un message que jam contine imagines. Retenta post un momento! + too_many: Impossibile annexar plus que 4 files migrations: + acct: Movite a + cancel: Cancellar redirection + cancel_explanation: Cancellar le redirection reactivara tu conto actual, ma non reportara sequaces que ha essite movite in ille conto. + cancelled_msg: Redirection cancellate con successo. errors: + already_moved: is the same account you have already moved to + missing_also_known_as: non es un alias de iste conto move_to_self: non pote esser le conto actual not_found: non poterea esser trovate + on_cooldown: Tu es in pausa + followers_count: Sequaces a tempore de mover + incoming_migrations: Movente ab un conto differente + incoming_migrations_html: Pro mover ab un altere conto a isto, primo tu debe crear un alias de conto. + moved_msg: Tu conto ora es redirigite a %{acct} e tu sequaces es movite super. + not_redirecting: Tu conto actualmente non es redirigite a ulle altere conto. + on_cooldown: Tu recentemente ha migrate tu conto. Iste function de novo sera disponibile in %{count} dies. + past_migrations: Migrationes passate + proceed_with_move: Mover sequaces + redirected_msg: Tu conto es ora redirigite a %{acct}. + redirecting_to: Tu conto es redirigite a %{acct}. + set_redirect: Predefinir redirection + warning: + backreference_required: Le nove conto debe primo esser configurate pro referer se a isto + before: 'Ante de continuar, lege iste notas accuratemente:' + cooldown: Post le movimento il ha un periodo de pausa durante le qual tu non potera mover te ancora + disabled_account: Tu conto actual non sera plenmente usabile postea. Comocunque, tu habera accesso a exportation de datos e re-activation. + followers: Iste action movera tote le sequaces ab le conto actual al nove conto + only_redirect_html: In alternativa, tu pote solo superponer un redirection sur tu profilo. + other_data: Nulle altere datos sera movite automaticamente + redirect: Le profilo de tu conto actual sera actualisate con un aviso de redirection e sera excludite de recercas moderation: title: Moderation move_handler: carry_blocks_over_text: Iste usator ha cambiate de conto desde %{acct}, que tu habeva blocate. + carry_mutes_over_text: Iste usator moveva ab %{acct}, que tu habeva silentiate. + copy_account_note_text: 'Iste usator moveva ab %{acct}, ci era tu previe notas re ille:' + navigation: + toggle_menu: Mutar menu notification_mailer: admin: + report: + subject: "%{name} inviava un reporto" sign_up: subject: "%{name} se ha inscribite" + favourite: + body: 'Tu message era favorite per %{name}:' + subject: "%{name} favoriva tu message" + title: Nove preferito follow: + body: "%{name} ora te seque!" + subject: "%{name} ora te seque" title: Nove sequitor follow_request: + action: Gere requestas de sequer + body: "%{name} ha demandate de sequer te" + subject: 'Sequace pendente: %{name}' title: Nove requesta de sequimento mention: action: Responder + body: 'Tu era mentionate per %{name} in:' + subject: Tu ha essite mentionate per %{name} title: Nove mention poll: subject: Un inquesta de %{name} ha finite + reblog: + body: 'Tu message ha essite impulsate per %{name}:' + subject: "%{name} ha impulsate tu message" + title: Nove impulso + status: + subject: "%{name} justo ha publicate" + update: + subject: "%{name} ha modificate un message" + notifications: + administration_emails: Avisos de email per administrator + email_events: Eventos pro avisos de email + email_events_hint: 'Selige eventos pro que tu vole reciper avisos:' + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T otp_authentication: + code_hint: Insere le codice generate per tu app de authentication pro confirmar + description_html: Si tu activa le authentication a duo factores per un app de authentication, le authentication requirera que tu es in possession de tu telephono, que generara testimonios pro facer te entrar. enable: Activar + instructions_html: "Scande iste codice QR in Google Authenticator o un simile app TOTP sur tu telephono. Desde ora in avante, ille app generara testimonios que tu debera inserer quando tu te authenticara." + manual_instructions: 'Si tu non pote scander le codice QR e besonia de inserer lo manualmente, ecce le texto-simple secrete:' setup: Configurar + wrong_code: Le codice inserite non era valide! Es tempore de servitor e tempore de apparato correcte? pagination: + newer: Plus recente next: Sequente + older: Plus vetere prev: Previe truncate: "…" polls: @@ -1418,9 +1541,13 @@ ia: posting_defaults: Publicationes predefinite public_timelines: Chronologias public privacy: + hint_html: "Personalisa como tu vole que tu profilo e tu messages a es trovate. Un varietate de functiones in Mastodon pote adjutar te attinger un plus large auditorio si activate. Prende un momento pro revider iste parametros pro assecurar te que illos se adapta a tu caso de uso." privacy: Confidentialitate + privacy_hint_html: Controla quanto tu vole divulgar pro le beneficio de alteres. Le gente discoperi profilos e applicationes interessante percurrente le profilos sequite per altere personas e vidente a partir de qual applicationes illos publica lor messages, ma tu pote preferer de mantener tal information private. reach: Portata + reach_hint_html: Controla si tu vole esser discoperite e sequite per nove personas. Vole tu que tu messages appare sur le schermo Explorar? Vole tu que altere personas te vide in lor recommendationes de sequimento? Vole tu acceptar automaticamente tote le nove sequitores o prefere tu haber le controlo granular super cata un? search: Cercar + search_hint_html: Controla como tu vole esser trovate. Vole tu que le gente te trova per medio del contento de tu messages public? Vole tu que personas foras de Mastodon trova tu profilo quando illes cerca in le web? Nota ben que non es possibile garantir le exclusion total de tu information public del motores de recerca. title: Confidentialitate e portata privacy_policy: title: Politica de confidentialitate @@ -1521,35 +1648,96 @@ ia: aliases: Aliases de conto appearance: Apparentia authorized_apps: Apps autorisate + back: Tornar a Mastodon delete: Deletion de conto development: Disveloppamento edit_profile: Modificar profilo + export: Exportation de datos featured_tags: Hashtags eminente import: Importar + import_and_export: Importar e exportar migrate: Migration de conto notifications: Notificationes de e-mail preferences: Preferentias profile: Profilo public relationships: Sequites e sequitores + severed_relationships: Relationes rupte + statuses_cleanup: Deletion de message automatic strikes: Admonitiones de moderation + two_factor_authentication: Authentication a duo factores + webauthn_authentication: Claves de securitate severed_relationships: download: Discargar (%{count}) event_type: account_suspension: Suspension del conto (%{target_name}) domain_block: Suspension del servitor (%{target_name}) user_domain_block: Tu ha blocate %{target_name} + lost_followers: Sequitores perdite + lost_follows: Sequites perdite preamble: Tu pote perder sequites e sequitores quando tu bloca un dominio o quando tu moderatores decide suspender un servitor remote. Quando isto occurre, tu potera discargar listas de relationes rumpite, a inspectar e eventualmente importar in un altere servitor. + purged: Le information re iste servitor ha essite purgate per le administratores de tu servitor. type: Evento statuses: + attached: + audio: + one: "%{count} audio" + other: "%{count} audio" + description: 'Attachate: %{attached}' + image: + one: "%{count} imagine" + other: "%{count} imagines" + video: + one: "%{count} video" + other: "%{count} videos" + boosted_from_html: Impulsate desde %{acct_link} + content_warning: 'Advertimento de contento: %{warning}' + default_language: Mesme como lingua de interfacie + disallowed_hashtags: + one: 'contineva un hashtag non autorisate: %{tags}' + other: 'contineva le hashtags non autorisate: %{tags}' + edited_at_html: Modificate le %{date} + errors: + in_reply_not_found: Le message a que tu tenta responder non pare exister. open_in_web: Aperir in le web + over_character_limit: limite de characteres de %{max} excedite + pin_errors: + direct: Messages que es solo visibile a usatores mentionate non pote esser appunctate + limit: Tu ha jam appunctate le maxime numero de messages + ownership: Le message de alcuno altere non pote esser appunctate + reblog: Un impulso non pote esser affixate poll: + total_people: + one: "%{count} persona" + other: "%{count} personas" + total_votes: + one: "%{count} voto" + other: "%{count} votos" vote: Votar show_more: Monstrar plus + show_thread: Monstrar argumento + title: '%{name}: "%{quote}"' visibilities: direct: Directe + private: Solo-sequaces private_long: Solmente monstrar a sequitores public: Public + public_long: Omnes pote vider + unlisted: Non listate + unlisted_long: Omnes pote vider, ma non es listate in le chronologias public statuses_cleanup: + enabled: Deler automaticamente le messages ancian + enabled_hint: Dele automaticamente tu messages un vice que illos attinge un limine de etate specificate, salvo que illes concorda un del exceptiones infra + exceptions: Exceptiones + explanation: Pois que deler messages es un operation costose, isto es facite lentemente in le tempore quando le servitor non es alteremente occupate. Pro iste ration, tu messages pote esser delite un poco post que illos attinge le limine de etate. + ignore_favs: Ignorar favoritos + ignore_reblogs: Ignorar impulsos + interaction_exceptions: Exceptiones basate super interactiones + interaction_exceptions_explanation: Nota que il non ha garantia que le messages essera delite si illos va sub le limine de favorites o impulsos post haber lo superate un vice. + keep_direct: Mantener le messages directe + keep_direct_hint: Non dele alcuno de tu messages directe + keep_media: Mantener messages con annexos de medios + keep_media_hint: Non dele alcuno de tu messages que ha annexos de medios + keep_pinned: Mantener messages appunctate keep_pinned_hint: Non dele alcuno de tu messages appunctate keep_polls: Mantener sondages keep_polls_hint: Non dele ulle de tu sondages @@ -1567,32 +1755,94 @@ ia: '63113904': 2 annos '7889238': 3 menses min_age_label: Limine de etate + min_favs: Mantener messages favorite al minus + min_favs_hint: Non deler alcuno de tu messages que ha recipite al minus iste numero de favoritos. Lassar blanc pro deler messages sin reguardo de lor numero de favoritos + min_reblogs: Mantener messages impulsate al minus + min_reblogs_hint: Non dele alcun de tu messages que ha essite impulsate al minus iste numero de vices. Lassar vacue pro deler messages independentemente de lor numero de impulsos stream_entries: sensitive_content: Contento sensibile strikes: errors: too_late: Es troppo tarde pro facer appello contra iste admonition + tags: + does_not_match_previous_name: non concorda le nomine previe themes: contrast: Mastodon (Alte contrasto) default: Mastodon (Obscur) mastodon-light: Mastodon (Clar) system: Automatic (usar thema del systema) + time: + formats: + default: "%d %b %Y, %H:%M" + month: "%b %Y" + time: "%H:%M" + with_time_zone: "%b %d, %Y, %H:%M %Z" + translation: + errors: + quota_exceeded: Le quota de utilisation del servitor pro le servicio de traduction ha essite excedite. + too_many_requests: Il ha habite troppe requestas al servicio de traduction recentemente. two_factor_authentication: add: Adder disable: Disactivar 2FA + disabled_success: Authentication a duo factores disactivate con successo edit: Modificar + enabled: Le authentication a duo factores es activate + enabled_success: Authentication a duo factores activate con successo generate_recovery_codes: Generar codices de recuperation + lost_recovery_codes: Le codices de recuperation te permitte de reganiar accesso a tu conto si tu perde tu telephono. Si tu ha perdite tu codices de recuperation, tu pote regenerar los ci. Tu vetere codices de recuperation sera invalidate. + methods: Methodos a duo factores + otp: App de authenticator + recovery_codes: Salveguardar codices de recuperation + recovery_codes_regenerated: Codices de recuperation regenerate con successo + recovery_instructions_html: Si tu perde le accesso a tu telephono, tu pote usar un del codices de recuperation hic infra pro reganiar le accesso a tu conto. Mantene le codices de recuperation secur. Per exemplo, tu pote imprimer los e guardar los con altere documentos importante. + webauthn: Claves de securitate user_mailer: appeal_approved: action: Parametros de conto explanation: Le appello contra le admonition contra tu conto del %{strike_date}, que tu ha submittite le %{appeal_date}, ha essite approbate. Tu conto ha de novo un bon reputation. + subject: Tu appello ab %{date} ha essite approbate + subtitle: Tu conto es ancora un vice in regula. + title: Appello approbate appeal_rejected: explanation: Le appello contra le admonition contra tu conto del %{strike_date}, que tu ha submittite le %{appeal_date}, ha essite rejectate. + subject: Tu appello ab %{date} ha essite rejectate + subtitle: Tu appello ha essite rejectate. + title: Appello rejectate + backup_ready: + explanation: Tu ha requestate un copia de securitate complete de tu conto de Mastodon. + extra: Isto es preste pro discargar! + subject: Tu archivo es preste pro discargar + title: Discargar archivo + failed_2fa: + details: 'Hic es le detalios del tentativa de initio de session:' + explanation: Alcuno ha tentate aperir session a tu conto ma ha fornite un secunde factor de authentication non valide. + further_actions_html: Si non se tractava de te, nos recommenda %{action} immediatemente perque illo pote esser compromittite. + subject: Fallimento del authentication de duo factores + title: Falleva le authentication de duo factores + suspicious_sign_in: + change_password: cambiar tu contrasigno + details: 'Hic es le detalios del initio de session:' + explanation: Nos ha detegite un initio de session a tu conto ab un nove adresse IP. + further_actions_html: Si non se tractava de te, nos recommenda %{action} immediatemente e activar le authentication bifactorial pro mantener tu conto secur. + subject: Alcuno ha accedite a tu conto desde un nove adresse IP + title: Un nove initio de session warning: appeal: Submitter un appello + appeal_description: Si tu crede que se tracta de un error, tu pote presentar un appello al personal de %{instance}. categories: spam: Spam + violation: Le contento viola le sequente regulas del communitate + explanation: + delete_statuses: Alcunes de tu messages ha essite judicate contrari a un o plus directivas communitari e ha dunque essite removite per le moderatores de %{instance}. + disable: Tu non pote plus usar tu conto, ma tu profilo e altere datos remane intacte. Tu pote requestar un copia de reserva de tu datos, cambiar le parametros del conto o deler le conto. + mark_statuses_as_sensitive: Alcunes de tu messages ha essite marcate como sensibile per le moderatores de %{instance}. Isto vole dicer que le gente debe toccar le objectos multimedial in le messages ante que un previsualisation appare. Tu pote marcar objectos multimedial como sensibile tu mesme quando tu publica messages in futuro. + sensitive: A partir de iste momento, tote le files multimedial que tu incarga essera marcate como sensibile e le gente debera cliccar sur un advertimento ante de poter vider los. + silence: Tu pote ancora usar tu conto ma solmente le personas qui ja te seque videra tu messages sur iste servitor, e tu pote esser excludite de varie functiones de discoperta. Nonobstante, altere personas pote ancora sequer te manualmente. + suspend: Tu non pote plus usar tu conto, e tu profilo e altere datos non es plus accessibile. Tu pote ancora aperir session pro requestar un copia de reserva de tu datos usque lor elimination in circa 30 dies. Nos retenera certe datos de base pro impedir que tu evade le suspension. + reason: 'Ration:' + statuses: 'Message citate:' subject: + delete_statuses: Tu messages sur %{acct} esseva removite disable: Tu conto %{acct} ha essite gelate mark_statuses_as_sensitive: Tu messages sur %{acct} ha essite marcate como sensibile none: Advertimento pro %{acct} @@ -1612,20 +1862,71 @@ ia: apps_ios_action: Discargar sur le App Store apps_step: Discarga nostre applicationes official. apps_title: Applicationes de Mastodon + checklist_subtitle: 'Comencia tu aventura sur le web social:' + checklist_title: Prime passos edit_profile_action: Personalisar edit_profile_step: Impulsa tu interactiones con un profilo comprehensive. edit_profile_title: Personalisar tu profilo explanation: Ecce alcun consilios pro initiar feature_action: Apprender plus + feature_audience: Mastodon te presenta le possibilitate unic de gerer tu audientia sin intermediarios. Mastodon, installate sur tu proprie infrastructura, te permitte sequer, e esser sequite per, personas sur qualcunque altere servitor Mastodon in linea, e necuno lo controla salvo tu. feature_audience_title: Crea tu auditorio in fiducia + feature_control: Tu sape melio lo que tu vole vider sur tu fluxo de initio. Nulle algorithmos o annuncios dissipa tu tempore. Seque quicinque sur qualcunque servitor Mastodon desde un sol conto, recipe lor messages in ordine chronologic, e face te un angulo del internet ubi tu te senti a casa. + feature_control_title: Mantene le controlo de tu proprie chronologia + feature_creativity: Mastodon supporta messages con audio, video e imagines, descriptiones de accessibilitate, sondages, advertimentos de contento, avatares con animation, emojis personalisate, controlo de retalio de miniaturas, e plus, pro adjutar te a exprimer te in linea. Que tu publica tu arte, tu musica o tu podcast, Mastodon existe pro te. + feature_creativity_title: Creativitate sin parallel + feature_moderation: Mastodon remitte le controlo in tu manos. Cata servitor crea su proprie regulas e directivas, applicate localmente e non de maniera vertical como le medios social corporative, rendente lo flexibile in responder al necessitates de differente gruppos de personas. Adhere a un servitor con regulas que te place, o alberga le tue. feature_moderation_title: Moderation como deberea esser follow_action: Sequer + follow_step: Sequer personas interessante es le ration de esser de Mastodon. + follow_title: Personalisa tu fluxo de initio + follows_subtitle: Seque contos popular + follows_title: Qui sequer + follows_view_more: Vider plus de personas a sequer + hashtags_recent_count: + one: "%{people} persona in le passate duo dies" + other: "%{people} personas in le passate duo diea" + hashtags_subtitle: Explora le tendentias del passate 2 dies + hashtags_title: Hashtags in tendentia + hashtags_view_more: Vider plus de hashtags in tendentia + post_action: Scriber + post_step: Saluta le mundo con texto, photos, videos o sondages. post_title: Face tu prime message share_action: Compartir + share_step: Face saper a tu amicos como trovar te sur Mastodon. share_title: Compartir tu profilo de Mastodon + sign_in_action: Initiar session subject: Benvenite in Mastodon + title: Benvenite a bordo, %{name}! + users: + follow_limit_reached: Tu non pote sequer plus de %{limit} personas + go_to_sso_account_settings: Vader al parametros de conto de tu fornitor de identitate + invalid_otp_token: Codice de duo factores non valide + otp_lost_help_html: Si tu ha perdite le accesso a ambes, tu pote contactar %{email} + rate_limited: Troppo de tentativas de authentication. Per favor reessaya plus tarde. + seamless_external_login: Tu ha aperite session per medio de un servicio externe. Le parametros de contrasigno e de e-mail es dunque indisponibile. + signed_in_as: 'Session aperite como:' verification: + extra_instructions_html: Consilio: Le ligamine sur tu sito web pote esser invisibile. Le parte importante es rel="me" que impedi le usurpation de identitate sur sitos web con contento generate per usatores. Tu pote mesmo usar un etiquetta link in le capite del pagina in vice de a, ma le codice HTML debe esser accessibile sin executar JavaScript. + here_is_how: Ecce como + hint_html: "Omnes pote verificar lor identitate sur Mastodon. Isto es basate sur standards web aperte e es gratuite, ora e pro sempre. Tote lo que es necessari es un sito web personal que le gente recognosce como le tue. Quando tu liga a iste sito web desde tu profilo, le systema verificara que le sito web liga retro a tu profilo e monstrara un indicator visual de iste facto." + instructions_html: Copia e colla le codice hic infra in le HTML de tu sito web. Alora adde le adresse de tu sito web in un del campos supplementari sur tu profilo desde le scheda “Modificar profilo” e salva le cambiamentos. + verification: Verification verified_links: Tu ligamines verificate webauthn_credentials: add: Adder un nove clave de securitate + create: + error: Il habeva un problema in adder tu clave de securitate. Tenta novemente. + success: Tu clave de securitate ha essite addite con successo. delete: Deler + delete_confirmation: Es tu secur que tu vole deler iste clave de securitate? + description_html: Si tu activa le authentication per clave de securitate, le apertura de session requirera que tu usa un de tu claves de securitate. + destroy: + error: Il habeva un problema in deler tu clave de securitate. Tenta novemente. + success: Tu clave de securitate ha essite delite con successo. + invalid_credential: Clave de securitate non valide + nickname_hint: Insere le pseudonymo de tu nove clave de securitate + not_enabled: Tu ancora non ha activate WebAuthn + not_supported: Iste navigator non supporta claves de securitate + otp_required: Pro usar le claves de securitate activa prime le authentication de duo factores. + registered_on: Registrate le %{date} diff --git a/config/locales/it.yml b/config/locales/it.yml index bda681ac08..5b75e7af7d 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -285,6 +285,7 @@ it: update_custom_emoji_html: "%{name} ha aggiornato emoji %{target}" update_domain_block_html: "%{name} ha aggiornato il blocco dominio per %{target}" update_ip_block_html: "%{name} ha cambiato la regola per l'IP %{target}" + update_report_html: "%{name} ha aggiornato la segnalazione %{target}" update_status_html: "%{name} ha aggiornato lo status di %{target}" update_user_role_html: "%{name} ha modificato il ruolo %{target}" deleted_account: account eliminato diff --git a/config/locales/ko.yml b/config/locales/ko.yml index f3f3e3a8fb..b104e31fc0 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -282,6 +282,7 @@ ko: update_custom_emoji_html: "%{name} 님이 에모지 %{target}를 업데이트 했습니다" update_domain_block_html: "%{name} 님이 %{target}에 대한 도메인 차단을 갱신했습니다" update_ip_block_html: "%{name} 님이 IP 규칙 %{target}을 수정했습니다" + update_report_html: "%{name} 님이 신고 %{target}를 업데이트 했습니다" update_status_html: "%{name} 님이 %{target}의 게시물을 업데이트했습니다" update_user_role_html: "%{name} 님이 %{target} 역할을 수정했습니다" deleted_account: 계정을 삭제했습니다 diff --git a/config/locales/lad.yml b/config/locales/lad.yml index e9f18d4bed..9c165472cd 100644 --- a/config/locales/lad.yml +++ b/config/locales/lad.yml @@ -285,6 +285,7 @@ lad: update_custom_emoji_html: "%{name} aktualizo el emoji %{target}" update_domain_block_html: "%{name} aktualizo el bloko de domeno para %{target}" update_ip_block_html: "\"%{name} troko la regla de IP %{target}" + update_report_html: "%{name} aktualizo el raporto %{target}" update_status_html: "%{name} aktualizo la publikasyon de %{target}" update_user_role_html: "%{name} troko el rolo %{target}" deleted_account: kuento supremido diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 74dea29b0e..3452f80994 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -285,6 +285,7 @@ nl: update_custom_emoji_html: Emoji %{target} is door %{name} bijgewerkt update_domain_block_html: "%{name} heeft de domeinblokkade bijgewerkt voor %{target}" update_ip_block_html: "%{name} wijzigde de IP-regel voor %{target}" + update_report_html: Rapportage %{target} is door %{name} bijgewerkt update_status_html: "%{name} heeft de berichten van %{target} bijgewerkt" update_user_role_html: "%{name} wijzigde de rol %{target}" deleted_account: verwijderd account diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 7c037d7d00..1c3fda8d03 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -291,6 +291,7 @@ pl: update_custom_emoji_html: Zaktualizowane emoji %{target} przez %{name} update_domain_block_html: Zaktualizowano blokadę domeny dla %{target} przez %{name} update_ip_block_html: "%{name} stworzył(a) regułę dla IP %{target}" + update_report_html: "%{target} zaktualizowany przez %{name}" update_status_html: "%{name} zaktualizował(a) wpis użytkownika %{target}" update_user_role_html: "%{name} zmienił rolę %{target}" deleted_account: usunięte konto diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index 0c2e6cfd6d..b4669e24d8 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -285,6 +285,7 @@ pt-PT: update_custom_emoji_html: "%{name} atualizou o emoji %{target}" update_domain_block_html: "%{name} atualizou o bloqueio de domínio para %{target}" update_ip_block_html: "%{name} alterou regra para IP %{target}" + update_report_html: "%{name} atualizou a denúncia %{target}" update_status_html: "%{name} atualizou o estado de %{target}" update_user_role_html: "%{name} alterou a função %{target}" deleted_account: conta apagada diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index 2f7010bacf..9ac36447de 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -78,14 +78,14 @@ fi: form_admin_settings: activity_api_enabled: Paikallisesti julkaistujen julkaisujen, aktiivisten käyttäjien ja rekisteröitymisten viikoittainen määrä app_icon: WEBP, PNG, GIF tai JPG. Korvaa oletusarvoisen mobiililaitteiden sovelluskuvakkeen omalla kuvakkeella. - backups_retention_period: Käyttäjillä on mahdollisuus arkistoida julkaisujaan myöhemmin ladattaviksi. Kun tämä on asetettu positiiviseksi arvoksi, nämä arkistot poistetaan automaattisesti asetetun päivien määrän jälkeen. + backups_retention_period: Käyttäjillä on mahdollisuus arkistoida julkaisujaan myöhemmin ladattaviksi. Kun arvo on positiivinen, nämä arkistot poistuvat automaattisesti, kun määritetty määrä päiviä on kulunut. bootstrap_timeline_accounts: Nämä tilit kiinnitetään uusien käyttäjien seuraamissuosituslistojen alkuun. closed_registrations_message: Näkyy, kun rekisteröityminen on suljettu - content_cache_retention_period: Kaikki muiden palvelimien viestit (mukaan lukien tehostukset ja vastaukset) poistetaan määritetyn päivien lukumäärän jälkeen, ottamatta huomioon paikallisen käyttäjän vuorovaikutusta kyseisten viestien kanssa. Sisältää viestit, jossa paikallinen käyttäjä on merkinnyt kirjanmerkiksi tai suosikeiksi. Myös yksityiset maininnat eri käyttäjien välillä menetetään, eikä niitä voi palauttaa. Tämän asetuksen käyttö on tarkoitettu erityisiin tapauksiin ja se rikkoo monia käyttäjien odotuksia, kun se toteutetaan yleistarkoituksiin. + content_cache_retention_period: Kaikki muiden palvelinten julkaisut (mukaan lukien tehostukset ja vastaukset) poistuvat, kun määritetty määrä päiviä on kulunut, ottamatta huomioon paikallisen käyttäjän vuorovaikutusta näiden julkaisujen kanssa. Sisältää julkaisut, jotka paikallinen käyttäjä on merkinnyt kirjanmerkiksi tai suosikiksi. Myös yksityiset maininnat eri palvelinten käyttäjien välillä menetetään, eikä niitä voi palauttaa. Tämä asetus on tarkoitettu käytettäväksi erityistapauksissa ja rikkoo monia käyttäjien odotuksia, kun sitä käytetään yleistarkoituksiin. custom_css: Voit käyttää mukautettuja tyylejä Mastodonin verkkoversiossa. favicon: WEBP, PNG, GIF tai JPG. Korvaa oletusarvoisen Mastodonin suosikkikuvakkeen omalla kuvakkeella. mascot: Ohittaa kuvituksen edistyneessä selainkäyttöliittymässä. - media_cache_retention_period: Mediatiedostot käyttäjien tekemistä viesteistä ovat välimuistissa palvelimellasi. Kun arvo on positiivinen, media poistetaan määritetyn ajan jälkeen. Jos mediaa pyydetään sen poistamisen jälkeen, ne ladataan uudelleen, jos lähdesisältö on vielä saatavilla. Koska linkkien katselun kyselyitä kolmansien osapuolien sivustoille on rajoitettu, on suositeltavaa asettaa tämä arvo vähintään 14 päivään tai linkkien kortteja ei päivitetä pyynnöstä ennen tätä aikaa. + media_cache_retention_period: Käyttäjien tekemien julkaisujen mediatiedostot ovat välimuistissa palvelimellasi. Kun arvo on positiivinen, media poistuu, kun määritetty määrä päiviä on kulunut. Jos mediaa pyydetään sen poistamisen jälkeen, se ladataan uudelleen, jos lähdesisältö on vielä saatavilla. Koska linkkien esikatselun kyselyitä kolmansien osapuolien sivustoille on rajoitettu, on suositeltavaa asettaa tämä arvo vähintään 14 päivään, tai linkkien kortteja ei päivitetä pyynnöstä ennen tätä ajankohtaa. peers_api_enabled: Luettelo verkkotunnuksista, jotka tämä palvelin on kohdannut fediversumissa. Se ei kerro, oletko liitossa tietyn palvelimen kanssa, vaan että palvelimesi on ylipäätään tietoinen siitä. Tätä tietoa käytetään palveluissa, jotka keräävät tilastoja federoinnista yleisellä tasolla. profile_directory: Profiilihakemisto lueteloi kaikki käyttäjät, jotka ovat ilmoittaneet olevansa löydettävissä. require_invite_text: Kun rekisteröityminen vaatii manuaalisen hyväksynnän, tee ”Miksi haluat liittyä?” -tekstikentästä pakollinen vapaaehtoisen sijaan diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml index a4abb737c0..96b36307a5 100644 --- a/config/locales/simple_form.sl.yml +++ b/config/locales/simple_form.sl.yml @@ -77,10 +77,15 @@ sl: warn: Skrij filtrirano vsebino za opozorilom, ki pomenja naslov filtra form_admin_settings: activity_api_enabled: Številke krajevno objavljenih objav, dejavnih uporabnikov in novih registracij na tedenskih seznamih + app_icon: WEBP, PNG, GIF ali JPG. Zamenja privzeto ikono programa na mobilnih napravah z ikono po meri. + backups_retention_period: Uporabniki lahko ustvarijo arhive svojih objav za kasnejši prenos k sebi. Ko je nastavljeno na pozitivno vrednost, bodo ti arhivi po nastavljenem številu dni samodejno izbrisani. bootstrap_timeline_accounts: Ti računi bodo pripeti na vrh priporočenih sledenj za nove uporabnike. closed_registrations_message: Prikazano, ko so registracije zaprte + content_cache_retention_period: Vse objave z drugih strežnikov (vključno z izpostavitvami in odgovori) bodo izbrisani po nastavljenem številu dni, ne glede na krajevne interakcije s temi objavami. To vključuje objave, ki jih je krajevni uporabnik dodal med zaznamke ali priljubljene. Zasebne omembe med uporabniki na različnih strežnikih bodo prav tako izgubljene in jih ne bo moč obnoviti. Uporaba te nastavitve je namenjena strežnikom s posebnim namenom in nasprotuje mnogim pričakovanjem uporabnikov na strežnikih za splošni namen. custom_css: Spletni različici Mastodona lahko uveljavite sloge po meri. + favicon: WEBP, PNG, GIF ali JPG. Zamenja privzeto ikono spletne strani Mastodon z ikono po meri. mascot: Preglasi ilustracijo v naprednem spletnem vmesniku. + media_cache_retention_period: Predstavnostne datoteke iz objav uporabnikov na ostalih strežnikih se začasno hranijo na tem strežniku. Ko je nastavljeno na pozitivno vrednost, bodo predstavnostne datoteke izbrisane po nastavljenem številu dni. Če bo predstavnostna datoteka zahtevana po izbrisu, bo ponovno prenešena, če bo vir še vedno na voljo. Zaradi omejitev pogostosti prejemanja predogledov povezav z drugih strani je priporočljivo to vrednost nastaviti na vsaj 14 dni. V nasprotnem predogledi povezav pred tem časom ne bodo osveženi na zahtevo. peers_api_enabled: Seznam imen domen, na katere je ta strežnik naletel v fediverzumu. Sem niso vključeni podatki o tem, ali ste v federaciji z danim strežnikom, zgolj to, ali vaš strežnik ve zanj. To uporabljajo storitve, ki zbirajo statistične podatke o federaciji v splošnem smislu. profile_directory: Imenik profilov izpiše vse uporabnike, ki so dovolili, da so v njem navedeni. require_invite_text: Če registracije zahtevajo ročno potrditev, nastavite vnos besedila pod »Zakaj se želite pridružiti?« za obveznega. @@ -240,6 +245,7 @@ sl: backups_retention_period: Obdobje hrambe arhivov uporabnikov bootstrap_timeline_accounts: Vedno priporočaj te račune novim uporabnikom closed_registrations_message: Sporočilo po meri, ko registracije niso na voljo + content_cache_retention_period: Obdobje hranjenja vsebine z ostalih strežnikov custom_css: CSS po meri mascot: Maskota po meri (opuščeno) media_cache_retention_period: Obdobje hrambe predpomnilnika predstavnosti diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 6c26511ad6..329ce5a29b 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -291,6 +291,7 @@ sl: update_custom_emoji_html: "%{name} je posodobil/a emotikone %{target}" update_domain_block_html: "%{name} je posodobil/a domenski blok za %{target}" update_ip_block_html: "%{name} je spremenil/a pravilo za IP %{target}" + update_report_html: "%{name} je posodobil poročilo %{target}" update_status_html: "%{name} je posodobil/a objavo uporabnika %{target}" update_user_role_html: "%{name} je spremenil/a vlogo %{target}" deleted_account: izbrisan račun diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index b4976f8985..808a10e729 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -288,6 +288,7 @@ sr-Latn: update_custom_emoji_html: "%{name} je ažurirao/-la emodži %{target}" update_domain_block_html: "%{name} je ažurirao/-la blok domena %{target}" update_ip_block_html: "%{name} je promenio/-la IP uslov za %{target}" + update_report_html: "%{name} je ažurirao izveštaj %{target}" update_status_html: "%{name} je ažurirao/-la objavu korisnika %{target}" update_user_role_html: "%{name} je promenio/-la poziciju %{target}" deleted_account: obrisan nalog diff --git a/config/locales/sr.yml b/config/locales/sr.yml index aec6d399d5..f03c6e878a 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -288,6 +288,7 @@ sr: update_custom_emoji_html: "%{name} је ажурирао/-ла емоџи %{target}" update_domain_block_html: "%{name} је ажурирао/-ла блок домена %{target}" update_ip_block_html: "%{name} је променио/-ла IP услов за %{target}" + update_report_html: "%{name} је ажурирао извештај %{target}" update_status_html: "%{name} је ажурирао/-ла објаву корисника %{target}" update_user_role_html: "%{name} је променио/-ла позицију %{target}" deleted_account: обрисан налог diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 11e1fce3fe..9f0de4a723 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -285,6 +285,7 @@ sv: update_custom_emoji_html: "%{name} uppdaterade emoji %{target}" update_domain_block_html: "%{name} uppdaterade domän-block för %{target}" update_ip_block_html: "%{name} ändrade regel för IP %{target}" + update_report_html: "%{name} uppdaterade rapporten %{target}" update_status_html: "%{name} uppdaterade inlägget av %{target}" update_user_role_html: "%{name} ändrade rollen %{target}" deleted_account: raderat konto diff --git a/config/locales/th.yml b/config/locales/th.yml index 56b7bea69a..5711f68ff8 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -282,6 +282,7 @@ th: update_custom_emoji_html: "%{name} ได้อัปเดตอีโมจิ %{target}" update_domain_block_html: "%{name} ได้อัปเดตการปิดกั้นโดเมนสำหรับ %{target}" update_ip_block_html: "%{name} ได้เปลี่ยนกฎสำหรับ IP %{target}" + update_report_html: "%{name} ได้อัปเดตรายงาน %{target}" update_status_html: "%{name} ได้อัปเดตโพสต์โดย %{target}" update_user_role_html: "%{name} ได้เปลี่ยนบทบาท %{target}" deleted_account: บัญชีที่ลบแล้ว @@ -1838,6 +1839,7 @@ th: feature_action: เรียนรู้เพิ่มเติม feature_audience: Mastodon มีความพิเศษที่ให้คุณจัดการผู้รับสารของคุณได้โดยไม่มีตัวกลาง นอกจากนี้ การติดตั้ง Mastodon บนโครงสร้างพื้นฐานของคุณจะทำให้คุณสามารถติดตาม (และติดตามโดย) เซิร์ฟเวอร์ Mastodon แห่งไหนก็ได้ที่ทำงานอยู่ โดยไม่มีใครสามารถควบคุมได้นอกจากคุณ feature_audience_title: สร้างผู้ชมของคุณด้วยความมั่นใจ + feature_control: คุณทราบดีที่สุดถึงสิ่งที่คุณต้องการเห็นในฟีดหน้าแรกของคุณ ไม่มีอัลกอริทึมหรือโฆษณาให้เสียเวลาของคุณ ติดตามใครก็ตามทั่วทั้งเซิร์ฟเวอร์ Mastodon ใด ๆ จากบัญชีเดียวและรับโพสต์ของเขาตามลำดับเวลา และทำให้มุมอินเทอร์เน็ตของคุณเป็นเหมือนคุณมากขึ้นอีกนิด feature_control_title: การควบคุมเส้นเวลาของคุณเอง feature_creativity_title: ความคิดสร้างสรรค์ที่ไม่มีใครเทียบได้ feature_moderation_title: การกลั่นกรองในแบบที่ควรจะเป็น diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 7b9cf50aaf..469f2c5ad8 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -285,6 +285,7 @@ tr: update_custom_emoji_html: "%{name}, %{target} emojisini güncelledi" update_domain_block_html: "%{name}, %{target} alan adının engelini güncelledi" update_ip_block_html: "%{name}, %{target} IP adresi için kuralı güncelledi" + update_report_html: "%{name}, %{target} raporunu güncelledi" update_status_html: "%{name}, %{target} kullanıcısının gönderisini güncelledi" update_user_role_html: "%{name}, %{target} rolünü değiştirdi" deleted_account: hesap silindi diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 5d9e881ea4..05f3157ec9 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -282,6 +282,7 @@ vi: update_custom_emoji_html: "%{name} đã cập nhật emoji %{target}" update_domain_block_html: "%{name} cập nhật chặn máy chủ %{target}" update_ip_block_html: "%{name} cập nhật chặn IP %{target}" + update_report_html: "%{name} cập nhật báo cáo %{target}" update_status_html: "%{name} cập nhật tút của %{target}" update_user_role_html: "%{name} đã thay đổi vai trò %{target}" deleted_account: tài khoản đã xóa diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 3140ebdd30..12b6197938 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -282,6 +282,7 @@ zh-CN: update_custom_emoji_html: "%{name} 更新了自定义表情 %{target}" update_domain_block_html: "%{name} 更新了对 %{target} 的域名屏蔽" update_ip_block_html: "%{name} 修改了对 IP %{target} 的规则" + update_report_html: "%{name} 更新了举报 %{target}" update_status_html: "%{name} 刷新了 %{target} 的嘟文" update_user_role_html: "%{name} 更改了 %{target} 角色" deleted_account: 账号已注销 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index cdedd759ea..ac633a201d 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -282,6 +282,7 @@ zh-TW: update_custom_emoji_html: "%{name} 已更新自訂 emoji 表情符號 %{target}" update_domain_block_html: "%{name} 已更新 %{target} 之網域封鎖" update_ip_block_html: "%{name} 已變更 IP %{target} 之規則" + update_report_html: "%{name} 已更新 %{target} 的檢舉" update_status_html: "%{name} 已更新 %{target} 的嘟文" update_user_role_html: "%{name} 已變更 %{target} 角色" deleted_account: 已刪除帳號 diff --git a/lib/mastodon/cli/preview_cards.rb b/lib/mastodon/cli/preview_cards.rb index 9b20a0cbb8..c0e207ad59 100644 --- a/lib/mastodon/cli/preview_cards.rb +++ b/lib/mastodon/cli/preview_cards.rb @@ -29,7 +29,7 @@ module Mastodon::CLI link = options[:link] ? 'link-type ' : '' scope = PreviewCard.cached scope = scope.where(type: :link) if options[:link] - scope = scope.where('updated_at < ?', time_ago) + scope = scope.where(updated_at: ...time_ago) processed, aggregate = parallelize_with_progress(scope) do |preview_card| next if preview_card.image.blank? diff --git a/package.json b/package.json index 99f895f8c3..71020e897d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", - "@rails/ujs": "7.1.3-3", + "@rails/ujs": "7.1.3", "@reduxjs/toolkit": "^2.0.1", "@svgr/webpack": "^5.5.0", "arrow-key-navigation": "^1.2.0", diff --git a/spec/helpers/self_destruct_helper_spec.rb b/spec/helpers/self_destruct_helper_spec.rb new file mode 100644 index 0000000000..09d7347eee --- /dev/null +++ b/spec/helpers/self_destruct_helper_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SelfDestructHelper do + describe 'self_destruct?' do + context 'when SELF_DESTRUCT is unset' do + it 'returns false' do + expect(helper.self_destruct?).to be false + end + end + + context 'when SELF_DESTRUCT is set to an invalid value' do + around do |example| + ClimateControl.modify SELF_DESTRUCT: 'true' do + example.run + end + end + + it 'returns false' do + expect(helper.self_destruct?).to be false + end + end + + context 'when SELF_DESTRUCT is set to value signed for the wrong purpose' do + around do |example| + ClimateControl.modify( + SELF_DESTRUCT: Rails.application.message_verifier('foo').generate('example.com'), + LOCAL_DOMAIN: 'example.com' + ) do + example.run + end + end + + it 'returns false' do + expect(helper.self_destruct?).to be false + end + end + + context 'when SELF_DESTRUCT is set to value signed for the wrong domain' do + around do |example| + ClimateControl.modify( + SELF_DESTRUCT: Rails.application.message_verifier('self-destruct').generate('foo.com'), + LOCAL_DOMAIN: 'example.com' + ) do + example.run + end + end + + it 'returns false' do + expect(helper.self_destruct?).to be false + end + end + + context 'when SELF_DESTRUCT is set to a correctly-signed value' do + around do |example| + ClimateControl.modify( + SELF_DESTRUCT: Rails.application.message_verifier('self-destruct').generate('example.com'), + LOCAL_DOMAIN: 'example.com' + ) do + example.run + end + end + + it 'returns true' do + expect(helper.self_destruct?).to be true + end + end + end +end diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index 88ad7aa02b..cd1ab3311c 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -125,4 +125,22 @@ RSpec.describe AdminMailer do .and(have_header('X-Priority', '1')) end end + + describe '.auto_close_registrations' do + let(:recipient) { Fabricate(:account, username: 'Bob') } + let(:mail) { described_class.with(recipient: recipient).auto_close_registrations } + + before do + recipient.user.update(locale: :en) + end + + it 'renders the email' do + expect(mail) + .to be_present + .and(deliver_to(recipient.user_email)) + .and(deliver_from('notifications@localhost')) + .and(have_subject('Registrations for cb6e6126.ngrok.io have been automatically switched to requiring approval')) + .and(have_body_text('have been automatically switched')) + end + end end diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb index 942d40d568..b8fb387acd 100644 --- a/spec/mailers/previews/admin_mailer_preview.rb +++ b/spec/mailers/previews/admin_mailer_preview.rb @@ -32,4 +32,9 @@ class AdminMailerPreview < ActionMailer::Preview def new_critical_software_updates AdminMailer.with(recipient: Account.first).new_critical_software_updates end + + # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/auto_close_registrations + def auto_close_registrations + AdminMailer.with(recipient: Account.first).auto_close_registrations + end end diff --git a/spec/requests/api/v1/apps/credentials_spec.rb b/spec/requests/api/v1/apps/credentials_spec.rb index e1455fe799..6e6970ce53 100644 --- a/spec/requests/api/v1/apps/credentials_spec.rb +++ b/spec/requests/api/v1/apps/credentials_spec.rb @@ -20,14 +20,26 @@ describe 'Credentials' do expect(body_as_json).to match( a_hash_including( + id: token.application.id.to_s, name: token.application.name, website: token.application.website, - vapid_key: Rails.configuration.x.vapid_public_key, scopes: token.application.scopes.map(&:to_s), - client_id: token.application.uid + redirect_uris: token.application.redirect_uris, + # Deprecated properties as of 4.3: + redirect_uri: token.application.redirect_uri.split.first, + vapid_key: Rails.configuration.x.vapid_public_key ) ) end + + it 'does not expose the client_id or client_secret' do + subject + + expect(response).to have_http_status(200) + + expect(body_as_json[:client_id]).to_not be_present + expect(body_as_json[:client_secret]).to_not be_present + end end context 'with a non-read scoped oauth token' do @@ -46,11 +58,14 @@ describe 'Credentials' do expect(body_as_json).to match( a_hash_including( + id: token.application.id.to_s, name: token.application.name, website: token.application.website, - vapid_key: Rails.configuration.x.vapid_public_key, scopes: token.application.scopes.map(&:to_s), - client_id: token.application.uid + redirect_uris: token.application.redirect_uris, + # Deprecated properties as of 4.3: + redirect_uri: token.application.redirect_uri.split.first, + vapid_key: Rails.configuration.x.vapid_public_key ) ) end diff --git a/spec/requests/api/v1/apps_spec.rb b/spec/requests/api/v1/apps_spec.rb index acabbc93f0..1f01bddf3c 100644 --- a/spec/requests/api/v1/apps_spec.rb +++ b/spec/requests/api/v1/apps_spec.rb @@ -9,8 +9,9 @@ RSpec.describe 'Apps' do end let(:client_name) { 'Test app' } - let(:scopes) { nil } - let(:redirect_uris) { 'urn:ietf:wg:oauth:2.0:oob' } + let(:scopes) { 'read write' } + let(:redirect_uri) { 'urn:ietf:wg:oauth:2.0:oob' } + let(:redirect_uris) { [redirect_uri] } let(:website) { nil } let(:params) do @@ -26,13 +27,63 @@ RSpec.describe 'Apps' do it 'creates an OAuth app', :aggregate_failures do subject + expect(response).to have_http_status(200) + + app = Doorkeeper::Application.find_by(name: client_name) + + expect(app).to be_present + expect(app.scopes.to_s).to eq scopes + expect(app.redirect_uris).to eq redirect_uris + + expect(body_as_json).to match( + a_hash_including( + id: app.id.to_s, + client_id: app.uid, + client_secret: app.secret, + name: client_name, + website: website, + scopes: ['read', 'write'], + redirect_uris: redirect_uris, + # Deprecated properties as of 4.3: + redirect_uri: redirect_uri, + vapid_key: Rails.configuration.x.vapid_public_key + ) + ) + end + end + + context 'without scopes being supplied' do + let(:scopes) { nil } + + it 'creates an OAuth App with the default scope' do + subject + expect(response).to have_http_status(200) expect(Doorkeeper::Application.find_by(name: client_name)).to be_present body = body_as_json - expect(body[:client_id]).to be_present - expect(body[:client_secret]).to be_present + expect(body[:scopes]).to eq Doorkeeper.config.default_scopes.to_a + end + end + + # FIXME: This is a bug: https://github.com/mastodon/mastodon/issues/30152 + context 'with scopes as an array' do + let(:scopes) { %w(read write follow) } + + it 'creates an OAuth App with the default scope' do + subject + + expect(response).to have_http_status(200) + + app = Doorkeeper::Application.find_by(name: client_name) + + expect(app).to be_present + expect(app.scopes.to_s).to eq 'read' + + body = body_as_json + + expect(body[:scopes]).to eq ['read'] end end @@ -77,8 +128,8 @@ RSpec.describe 'Apps' do end end - context 'with a too-long redirect_uris' do - let(:redirect_uris) { "https://foo.bar/#{'hoge' * 2_000}" } + context 'with a too-long redirect_uri' do + let(:redirect_uris) { "https://app.example/#{'hoge' * 2_000}" } it 'returns http unprocessable entity' do subject @@ -87,8 +138,80 @@ RSpec.describe 'Apps' do end end - context 'without required params' do - let(:client_name) { '' } + # NOTE: This spec currently tests the same as the "with a too-long redirect_uri test case" + context 'with too many redirect_uris' do + let(:redirect_uris) { (0...500).map { |i| "https://app.example/#{i}/callback" } } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'with multiple redirect_uris as a string' do + let(:redirect_uris) { "https://redirect1.example/\napp://redirect2.example/" } + + it 'creates an OAuth application with multiple redirect URIs' do + subject + + expect(response).to have_http_status(200) + + app = Doorkeeper::Application.find_by(name: client_name) + + expect(app).to be_present + expect(app.redirect_uri).to eq redirect_uris + expect(app.redirect_uris).to eq redirect_uris.split + + body = body_as_json + + expect(body[:redirect_uri]).to eq redirect_uris + expect(body[:redirect_uris]).to eq redirect_uris.split + end + end + + context 'with multiple redirect_uris as an array' do + let(:redirect_uris) { ['https://redirect1.example/', 'app://redirect2.example/'] } + + it 'creates an OAuth application with multiple redirect URIs' do + subject + + expect(response).to have_http_status(200) + + app = Doorkeeper::Application.find_by(name: client_name) + + expect(app).to be_present + expect(app.redirect_uri).to eq redirect_uris.join "\n" + expect(app.redirect_uris).to eq redirect_uris + + body = body_as_json + + expect(body[:redirect_uri]).to eq redirect_uris.join "\n" + expect(body[:redirect_uris]).to eq redirect_uris + end + end + + context 'with an empty redirect_uris array' do + let(:redirect_uris) { [] } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'with just a newline as the redirect_uris string' do + let(:redirect_uris) { "\n" } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'with an empty redirect_uris string' do let(:redirect_uris) { '' } it 'returns http unprocessable entity' do @@ -97,5 +220,30 @@ RSpec.describe 'Apps' do expect(response).to have_http_status(422) end end + + context 'without a required param' do + let(:client_name) { '' } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'with a website' do + let(:website) { 'https://app.example/' } + + it 'creates an OAuth application with the website specified' do + subject + + expect(response).to have_http_status(200) + + app = Doorkeeper::Application.find_by(name: client_name) + + expect(app).to be_present + expect(app.website).to eq website + end + end end end diff --git a/spec/requests/self_destruct_spec.rb b/spec/requests/self_destruct_spec.rb new file mode 100644 index 0000000000..f71a2325e2 --- /dev/null +++ b/spec/requests/self_destruct_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Self-destruct mode' do + before do + allow(SelfDestructHelper).to receive(:self_destruct?).and_return(true) + end + + shared_examples 'generic logged out request' do |path| + it 'returns 410 gone and mentions self-destruct' do + get path, headers: { 'Accept' => 'text/html' } + + expect(response).to have_http_status(410) + expect(response.body).to include(I18n.t('self_destruct.title')) + end + end + + shared_examples 'accessible logged-in endpoint' do |path| + it 'returns 200 ok' do + get path + + expect(response).to have_http_status(200) + end + end + + shared_examples 'ActivityPub request' do |path| + context 'without signature' do + it 'returns 410 gone' do + get path, headers: { + 'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + } + + expect(response).to have_http_status(410) + end + end + + context 'with invalid signature' do + it 'returns 410 gone' do + get path, headers: { + 'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="bar"', + } + + expect(response).to have_http_status(410) + end + end + end + + context 'when requesting various unavailable endpoints' do + it_behaves_like 'generic logged out request', '/' + it_behaves_like 'generic logged out request', '/about' + it_behaves_like 'generic logged out request', '/public' + end + + context 'when requesting a suspended account' do + let(:suspended) { Fabricate(:account, username: 'suspended') } + + before do + suspended.suspend! + end + + it_behaves_like 'generic logged out request', '/@suspended' + it_behaves_like 'ActivityPub request', '/users/suspended' + it_behaves_like 'ActivityPub request', '/users/suspended/followers' + it_behaves_like 'ActivityPub request', '/users/suspended/outbox' + end + + context 'when requesting a non-suspended account' do + before do + Fabricate(:account, username: 'bob') + end + + it_behaves_like 'generic logged out request', '/@bob' + it_behaves_like 'ActivityPub request', '/users/bob' + it_behaves_like 'ActivityPub request', '/users/bob/followers' + it_behaves_like 'ActivityPub request', '/users/bob/outbox' + end + + context 'when accessing still-enabled endpoints when logged in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + end + + it_behaves_like 'accessible logged-in endpoint', '/auth/edit' + it_behaves_like 'accessible logged-in endpoint', '/settings/export' + it_behaves_like 'accessible logged-in endpoint', '/settings/login_activities' + it_behaves_like 'accessible logged-in endpoint', '/settings/exports/follows.csv' + end +end diff --git a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb index 4d9185093a..08ebf82785 100644 --- a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb @@ -163,7 +163,7 @@ describe Scheduler::AccountsStatusesCleanupScheduler do def cleanable_statuses_count Status .where(account_id: [account_alice, account_chris, account_erin]) # Accounts with enabled policies - .where('created_at < ?', 2.weeks.ago) # Policy defaults is 2.weeks + .where(created_at: ...2.weeks.ago) # Policy defaults is 2.weeks .count end end diff --git a/yarn.lock b/yarn.lock index f0e9771f30..e4e5eee8a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2215,13 +2215,13 @@ __metadata: linkType: hard "@formatjs/cli@npm:^6.1.1": - version: 6.2.10 - resolution: "@formatjs/cli@npm:6.2.10" + version: 6.2.12 + resolution: "@formatjs/cli@npm:6.2.12" peerDependencies: "@glimmer/env": ^0.1.7 - "@glimmer/reference": ^0.91.1 - "@glimmer/syntax": ^0.91.1 - "@glimmer/validator": ^0.91.1 + "@glimmer/reference": ^0.91.1 || ^0.92.0 + "@glimmer/syntax": ^0.92.0 + "@glimmer/validator": ^0.92.0 "@vue/compiler-core": ^3.4.0 content-tag: ^2.0.1 ember-template-recast: ^6.1.4 @@ -2245,7 +2245,7 @@ __metadata: optional: true bin: formatjs: bin/formatjs - checksum: 10c0/34b1b0b3be25d945111c1f57913f50da7308ecd05501a27eaca210a774eb50c616b5706ba796d37ffa223ac4c5cddd5f36fe0ca8d31ad8c8ade79cdd497ccfb9 + checksum: 10c0/3bd05a9fad6c837e22988e6638f426c128efa46ab80ff88cf2ad81fb3bc10cf4f228907577fc01e24c2d7d505cfabfaa69f0496d2ec8f0ab2d6b5eaccb5e475c languageName: node linkType: hard @@ -2758,7 +2758,7 @@ __metadata: "@formatjs/intl-pluralrules": "npm:^5.2.2" "@gamestdio/websocket": "npm:^0.3.2" "@github/webauthn-json": "npm:^2.1.1" - "@rails/ujs": "npm:7.1.3-3" + "@rails/ujs": "npm:7.1.3" "@reduxjs/toolkit": "npm:^2.0.1" "@svgr/webpack": "npm:^5.5.0" "@testing-library/jest-dom": "npm:^6.0.0" @@ -3045,10 +3045,10 @@ __metadata: languageName: node linkType: hard -"@rails/ujs@npm:7.1.3-3": - version: 7.1.3-3 - resolution: "@rails/ujs@npm:7.1.3-3" - checksum: 10c0/9eee95372b72d8f704b67f14a3bf9f2681ab5b11c7b79919bfde3341f2970771876af5b40de5b3e4fca6a97c76a41046eff71d96490617c1fc80ef3ad8bbac47 +"@rails/ujs@npm:7.1.3": + version: 7.1.3 + resolution: "@rails/ujs@npm:7.1.3" + checksum: 10c0/68112d9add9dbc59b40c2ec1bc095a67445c57d20d0ab7d817ce3de0cd90374e2690af8ad54ce6ecc2d1c748b34c0c44d0fbd2f515ce2c443d7c5d23d00b9ce5 languageName: node linkType: hard @@ -3708,9 +3708,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.195": - version: 4.17.1 - resolution: "@types/lodash@npm:4.17.1" - checksum: 10c0/af2ad8a3c8d7deb170a7ec6e18afc5ae8980576e5f7fe798d8a95a1df7222c15bdf967a25a35879f575a3b64743de00145710ee461a0051e055e94e4fe253f45 + version: 4.17.4 + resolution: "@types/lodash@npm:4.17.4" + checksum: 10c0/0124c64cb9fe7a0f78b6777955abd05ef0d97844d49118652eae45f8fa57bfb7f5a7a9bccc0b5a84c0a6dc09631042e4590cb665acb9d58dfd5e6543c75341ec languageName: node linkType: hard @@ -14293,14 +14293,14 @@ __metadata: linkType: hard "react-dom@npm:^18.2.0": - version: 18.2.0 - resolution: "react-dom@npm:18.2.0" + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" dependencies: loose-envify: "npm:^1.1.0" - scheduler: "npm:^0.23.0" + scheduler: "npm:^0.23.2" peerDependencies: - react: ^18.2.0 - checksum: 10c0/66dfc5f93e13d0674e78ef41f92ed21dfb80f9c4ac4ac25a4b51046d41d4d2186abc915b897f69d3d0ebbffe6184e7c5876f2af26bfa956f179225d921be713a + react: ^18.3.1 + checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85 languageName: node linkType: hard @@ -14703,11 +14703,11 @@ __metadata: linkType: hard "react@npm:^18.2.0": - version: 18.2.0 - resolution: "react@npm:18.2.0" + version: 18.3.1 + resolution: "react@npm:18.3.1" dependencies: loose-envify: "npm:^1.1.0" - checksum: 10c0/b562d9b569b0cb315e44b48099f7712283d93df36b19a39a67c254c6686479d3980b7f013dc931f4a5a3ae7645eae6386b4aa5eea933baa54ecd0f9acb0902b8 + checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3 languageName: node linkType: hard @@ -15368,7 +15368,7 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2": +"scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" dependencies: