diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8a3ce69111..24e78c9ab3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -531,13 +531,6 @@ RSpec/MultipleMemoizedHelpers: RSpec/NestedGroups: Max: 6 -# Configuration parameters: AllowedPatterns. -# AllowedPatterns: ^expect_, ^assert_ -RSpec/NoExpectationExample: - Exclude: - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/services/precompute_feed_service_spec.rb' - RSpec/PendingWithoutReason: Exclude: - 'spec/models/account_spec.rb' diff --git a/app/javascript/mastodon/actions/blocks.js b/app/javascript/mastodon/actions/blocks.js index 66421ed455..e293657ad3 100644 --- a/app/javascript/mastodon/actions/blocks.js +++ b/app/javascript/mastodon/actions/blocks.js @@ -95,6 +95,6 @@ export function initBlockModal(account) { account, }); - dispatch(openModal('BLOCK')); + dispatch(openModal({ modalType: 'BLOCK' })); }; } diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js index c0f0f3acc5..1fc2e391e2 100644 --- a/app/javascript/mastodon/actions/boosts.js +++ b/app/javascript/mastodon/actions/boosts.js @@ -14,7 +14,10 @@ export function initBoostModal(props) { privacy, }); - dispatch(openModal('BOOST', props)); + dispatch(openModal({ + modalType: 'BOOST', + modalProps: props, + })); }; } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index d1eb2496a3..a372ada40b 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -383,7 +383,10 @@ export function initMediaEditModal(id) { id, }); - dispatch(openModal('FOCAL_POINT', { id })); + dispatch(openModal({ + modalType: 'FOCAL_POINT', + modalProps: { id }, + })); }; } diff --git a/app/javascript/mastodon/actions/filters.js b/app/javascript/mastodon/actions/filters.js index 3774ee042a..a11956ac56 100644 --- a/app/javascript/mastodon/actions/filters.js +++ b/app/javascript/mastodon/actions/filters.js @@ -15,9 +15,12 @@ export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS'; export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL'; export const initAddFilter = (status, { contextType }) => dispatch => - dispatch(openModal('FILTER', { - statusId: status?.get('id'), - contextType: contextType, + dispatch(openModal({ + modalType: 'FILTER', + modalProps: { + statusId: status?.get('id'), + contextType: contextType, + }, })); export const fetchFilters = () => (dispatch, getState) => { diff --git a/app/javascript/mastodon/actions/modal.js b/app/javascript/mastodon/actions/modal.js deleted file mode 100644 index ef2ae0e4c7..0000000000 --- a/app/javascript/mastodon/actions/modal.js +++ /dev/null @@ -1,18 +0,0 @@ -export const MODAL_OPEN = 'MODAL_OPEN'; -export const MODAL_CLOSE = 'MODAL_CLOSE'; - -export function openModal(type, props) { - return { - type: MODAL_OPEN, - modalType: type, - modalProps: props, - }; -} - -export function closeModal(type, options = { ignoreFocus: false }) { - return { - type: MODAL_CLOSE, - modalType: type, - ignoreFocus: options.ignoreFocus, - }; -} diff --git a/app/javascript/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts new file mode 100644 index 0000000000..af34f5d6af --- /dev/null +++ b/app/javascript/mastodon/actions/modal.ts @@ -0,0 +1,17 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root'; + +export type ModalType = keyof typeof MODAL_COMPONENTS; + +interface OpenModalPayload { + modalType: ModalType; + modalProps: unknown; +} +export const openModal = createAction('MODAL_OPEN'); + +interface CloseModalPayload { + modalType: ModalType | undefined; + ignoreFocus: boolean; +} +export const closeModal = createAction('MODAL_CLOSE'); diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js index e61a14af24..fb041078b8 100644 --- a/app/javascript/mastodon/actions/mutes.js +++ b/app/javascript/mastodon/actions/mutes.js @@ -97,7 +97,7 @@ export function initMuteModal(account) { account, }); - dispatch(openModal('MUTE')); + dispatch(openModal({ modalType: 'MUTE' })); }; } diff --git a/app/javascript/mastodon/actions/reports.js b/app/javascript/mastodon/actions/reports.js index b3f9bf1ac3..756b8cd05e 100644 --- a/app/javascript/mastodon/actions/reports.js +++ b/app/javascript/mastodon/actions/reports.js @@ -7,9 +7,12 @@ export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; export const initReport = (account, status) => dispatch => - dispatch(openModal('REPORT', { - accountId: account.get('id'), - statusId: status?.get('id'), + dispatch(openModal({ + modalType: 'REPORT', + modalProps: { + accountId: account.get('id'), + statusId: status?.get('id'), + }, })); export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index 07cb72be98..68a456c1b1 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -23,7 +23,7 @@ import { RelativeTimestamp } from './relative_timestamp'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, diff --git a/app/javascript/mastodon/components/edited_timestamp/index.jsx b/app/javascript/mastodon/components/edited_timestamp/index.jsx index 42d3d93fe8..987b7c8272 100644 --- a/app/javascript/mastodon/components/edited_timestamp/index.jsx +++ b/app/javascript/mastodon/components/edited_timestamp/index.jsx @@ -15,7 +15,10 @@ import DropdownMenu from './containers/dropdown_menu_container'; const mapDispatchToProps = (dispatch, { statusId }) => ({ onItemClick (index) { - dispatch(openModal('COMPARE_HISTORY', { index, statusId })); + dispatch(openModal({ + modalType: 'COMPARE_HISTORY', + modalProps: { index, statusId }, + })); }, }); diff --git a/app/javascript/mastodon/components/load_gap.tsx b/app/javascript/mastodon/components/load_gap.tsx index f741f68341..e6d3060eb3 100644 --- a/app/javascript/mastodon/components/load_gap.tsx +++ b/app/javascript/mastodon/components/load_gap.tsx @@ -16,21 +16,21 @@ interface Props { intl: InjectedIntl; } -export const LoadGap = injectIntl( - ({ disabled, maxId, onClick, intl }) => { - const handleClick = useCallback(() => { - onClick(maxId); - }, [maxId, onClick]); +const _LoadGap: React.FC = ({ disabled, maxId, onClick, intl }) => { + const handleClick = useCallback(() => { + onClick(maxId); + }, [maxId, onClick]); - return ( - - ); - } -); + return ( + + ); +}; + +export const LoadGap = injectIntl(_LoadGap); diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index d8a21f64a0..3f3c292ea3 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -59,7 +59,7 @@ export const defaultMediaVisibility = (status) => { const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, }); diff --git a/app/javascript/mastodon/containers/account_container.jsx b/app/javascript/mastodon/containers/account_container.jsx index 09a755e877..a134452e77 100644 --- a/app/javascript/mastodon/containers/account_container.jsx +++ b/app/javascript/mastodon/containers/account_container.jsx @@ -35,10 +35,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); diff --git a/app/javascript/mastodon/containers/domain_container.jsx b/app/javascript/mastodon/containers/domain_container.jsx index 4828445c48..c719a5775c 100644 --- a/app/javascript/mastodon/containers/domain_container.jsx +++ b/app/javascript/mastodon/containers/domain_container.jsx @@ -18,10 +18,13 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch, { intl }) => ({ onBlockDomain (domain) { - dispatch(openModal('CONFIRM', { - message: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + }, })); }, diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js index 5dd5273b27..6cf180cd53 100644 --- a/app/javascript/mastodon/containers/dropdown_menu_container.js +++ b/app/javascript/mastodon/containers/dropdown_menu_container.js @@ -18,15 +18,21 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ dispatch(fetchRelationships([status.getIn(['account', 'id'])])); } - dispatch(isUserTouching() ? openModal('ACTIONS', { - status, - actions: items, - onClick: onItemClick, + dispatch(isUserTouching() ? openModal({ + modalType: 'ACTIONS', + modalProps: { + status, + actions: items, + onClick: onItemClick, + }, }) : openDropdownMenu(id, keyboard, scrollKey)); }, onClose(id) { - dispatch(closeModal('ACTIONS')); + dispatch(closeModal({ + modalType: 'ACTIONS', + ignoreFocus: false, + })); dispatch(closeDropdownMenu(id)); }, }); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 5e5f75d092..3026dde0a8 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -59,7 +59,7 @@ const messages = defineMessages({ replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' }, editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, }); const makeMapStateToProps = () => { @@ -82,10 +82,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ let state = getState(); if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status, router)) }, })); } else { dispatch(replyCompose(status, router)); @@ -134,9 +136,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onEmbed (status) { - dispatch(openModal('EMBED', { - url: status.get('url'), - onError: error => dispatch(showAlertForError(error)), + dispatch(openModal({ + modalType: 'EMBED', + modalProps: { + url: status.get('url'), + onError: error => dispatch(showAlertForError(error)), + }, })); }, @@ -144,10 +149,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ if (!deleteModal) { dispatch(deleteStatus(status.get('id'), history, withRedraft)); } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + }, })); } }, @@ -156,10 +164,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch((_, getState) => { let state = getState(); if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.editMessage), - confirm: intl.formatMessage(messages.editConfirm), - onConfirm: () => dispatch(editStatus(status.get('id'), history)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.editMessage), + confirm: intl.formatMessage(messages.editConfirm), + onConfirm: () => dispatch(editStatus(status.get('id'), history)), + }, })); } else { dispatch(editStatus(status.get('id'), history)); @@ -184,11 +195,17 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onOpenMedia (statusId, media, index, lang) { - dispatch(openModal('MEDIA', { statusId, media, index, lang })); + dispatch(openModal({ + modalType: 'MEDIA', + modalProps: { statusId, media, index, lang }, + })); }, onOpenVideo (statusId, media, lang, options) { - dispatch(openModal('VIDEO', { statusId, media, lang, options })); + dispatch(openModal({ + modalType: 'VIDEO', + modalProps: { statusId, media, lang, options }, + })); }, onBlock (status) { @@ -237,10 +254,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onBlockDomain (domain) { - dispatch(openModal('CONFIRM', { - message: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + }, })); }, @@ -253,10 +273,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onInteractionModal (type, status) { - dispatch(openModal('INTERACTION', { - type, - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type, + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); }, diff --git a/app/javascript/mastodon/features/account_gallery/index.jsx b/app/javascript/mastodon/features/account_gallery/index.jsx index 16fb9ef577..27de4740ca 100644 --- a/app/javascript/mastodon/features/account_gallery/index.jsx +++ b/app/javascript/mastodon/features/account_gallery/index.jsx @@ -143,14 +143,23 @@ class AccountGallery extends ImmutablePureComponent { const lang = attachment.getIn(['status', 'language']); if (attachment.get('type') === 'video') { - dispatch(openModal('VIDEO', { media: attachment, statusId, lang, options: { autoPlay: true } })); + dispatch(openModal({ + modalType: 'VIDEO', + modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, + })); } else if (attachment.get('type') === 'audio') { - dispatch(openModal('AUDIO', { media: attachment, statusId, lang, options: { autoPlay: true } })); + dispatch(openModal({ + modalType: 'AUDIO', + modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, + })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - dispatch(openModal('MEDIA', { media, index, statusId, lang })); + dispatch(openModal({ + modalType: 'MEDIA', + modalProps: { media, index, statusId, lang }, + })); } }; diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx index 6cb8a105cc..2b3a66c55e 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx @@ -28,7 +28,7 @@ import Header from '../components/header'; const messages = defineMessages({ cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, }); const makeMapStateToProps = () => { @@ -48,20 +48,26 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); } } else if (account.getIn(['relationship', 'requested'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); @@ -72,10 +78,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onInteractionModal (account) { - dispatch(openModal('INTERACTION', { - type: 'follow', - accountId: account.get('id'), - url: account.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'follow', + accountId: account.get('id'), + url: account.get('url'), + }, })); }, @@ -132,10 +141,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onBlockDomain (domain) { - dispatch(openModal('CONFIRM', { - message: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + }, })); }, @@ -144,21 +156,30 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onAddToList (account) { - dispatch(openModal('LIST_ADDER', { - accountId: account.get('id'), + dispatch(openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: account.get('id'), + }, })); }, onChangeLanguages (account) { - dispatch(openModal('SUBSCRIBED_LANGUAGES', { - accountId: account.get('id'), + dispatch(openModal({ + modalType: 'SUBSCRIBED_LANGUAGES', + modalProps: { + accountId: account.get('id'), + }, })); }, onOpenAvatar (account) { - dispatch(openModal('IMAGE', { - src: account.get('avatar'), - alt: account.get('acct'), + dispatch(openModal({ + modalType: 'IMAGE', + modalProps: { + src: account.get('avatar'), + alt: account.get('acct'), + }, })); }, diff --git a/app/javascript/mastodon/features/compose/components/action_bar.jsx b/app/javascript/mastodon/features/compose/components/action_bar.jsx index 60914b4626..726b5aa30d 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -16,7 +16,7 @@ const messages = defineMessages({ lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, diff --git a/app/javascript/mastodon/features/compose/containers/navigation_container.js b/app/javascript/mastodon/features/compose/containers/navigation_container.js index f881c4fa13..70b4026d14 100644 --- a/app/javascript/mastodon/features/compose/containers/navigation_container.js +++ b/app/javascript/mastodon/features/compose/containers/navigation_container.js @@ -21,11 +21,14 @@ const mapStateToProps = state => { const mapDispatchToProps = (dispatch, { intl }) => ({ onLogout () { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.logoutMessage), - confirm: intl.formatMessage(messages.logoutConfirm), - closeWhenConfirm: false, - onConfirm: () => logOut(), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, + onConfirm: () => logOut(), + }, })); }, }); diff --git a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js index 1ca3fe550a..6d26abf4f6 100644 --- a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js @@ -16,8 +16,14 @@ const mapDispatchToProps = dispatch => ({ }, isUserTouching, - onModalOpen: props => dispatch(openModal('ACTIONS', props)), - onModalClose: () => dispatch(closeModal()), + onModalOpen: props => dispatch(openModal({ + modalType: 'ACTIONS', + modalProps: props, + })), + onModalClose: () => dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })), }); diff --git a/app/javascript/mastodon/features/compose/index.jsx b/app/javascript/mastodon/features/compose/index.jsx index 1923558862..8997f1bc6c 100644 --- a/app/javascript/mastodon/features/compose/index.jsx +++ b/app/javascript/mastodon/features/compose/index.jsx @@ -71,11 +71,14 @@ class Compose extends PureComponent { e.preventDefault(); e.stopPropagation(); - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.logoutMessage), - confirm: intl.formatMessage(messages.logoutConfirm), - closeWhenConfirm: false, - onConfirm: () => logOut(), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, + onConfirm: () => logOut(), + }, })); return false; diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js index 978c031aab..456fc7d7cc 100644 --- a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js +++ b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js @@ -41,10 +41,13 @@ const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({ let state = getState(); if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status, router)), + }, })); } else { dispatch(replyCompose(status, router)); diff --git a/app/javascript/mastodon/features/directory/components/account_card.jsx b/app/javascript/mastodon/features/directory/components/account_card.jsx index 87ddcf94f7..cf1c63f9e4 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.jsx +++ b/app/javascript/mastodon/features/directory/components/account_card.jsx @@ -50,27 +50,32 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { dispatch( - openModal('CONFIRM', { - message: ( - @{account.get('acct')} }} - /> - ), - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }), + openModal({ + modalType: 'CONFIRM', + modalProps: { + message: ( + @{account.get('acct')} }} + /> + ), + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + } }), ); } else { dispatch(unfollowAccount(account.get('id'))); } } else if (account.getIn(['relationship', 'requested'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); diff --git a/app/javascript/mastodon/features/getting_started/index.jsx b/app/javascript/mastodon/features/getting_started/index.jsx index 2d1ac6670d..e31ca79af1 100644 --- a/app/javascript/mastodon/features/getting_started/index.jsx +++ b/app/javascript/mastodon/features/getting_started/index.jsx @@ -34,7 +34,7 @@ const messages = defineMessages({ follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx index db6a3c4a53..be03b8f5fe 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.jsx +++ b/app/javascript/mastodon/features/interaction_modal/index.jsx @@ -18,8 +18,11 @@ const mapStateToProps = (state, { accountId }) => ({ const mapDispatchToProps = (dispatch) => ({ onSignupClick() { - dispatch(closeModal()); - dispatch(openModal('CLOSED_REGISTRATIONS')); + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })); + dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); }, }); diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx index 1d80a9cd50..f41e8e6f23 100644 --- a/app/javascript/mastodon/features/list_timeline/index.jsx +++ b/app/javascript/mastodon/features/list_timeline/index.jsx @@ -114,24 +114,30 @@ class ListTimeline extends PureComponent { }; handleEditClick = () => { - this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id })); + this.props.dispatch(openModal({ + modalType: 'LIST_EDITOR', + modalProps: { listId: this.props.params.id }, + })); }; handleDeleteClick = () => { const { dispatch, columnId, intl } = this.props; const { id } = this.props.params; - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => { - dispatch(deleteList(id)); + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.deleteMessage), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => { + dispatch(deleteList(id)); - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - this.context.router.history.push('/lists'); - } + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + this.context.router.history.push('/lists'); + } + }, }, })); }; diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index 292767badb..b63796a8b2 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -59,10 +59,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onClear () { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(clearNotifications()), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.clearMessage), + confirm: intl.formatMessage(messages.clearConfirm), + onConfirm: () => dispatch(clearNotifications()), + }, })); }, 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 1743edae99..c167d93dce 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -74,19 +74,25 @@ class Footer extends ImmutablePureComponent { if (signedIn) { if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: this._performReply, + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: this._performReply, + }, })); } else { this._performReply(); } } else { - dispatch(openModal('INTERACTION', { - type: 'reply', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; @@ -102,10 +108,13 @@ class Footer extends ImmutablePureComponent { dispatch(favourite(status)); } } else { - dispatch(openModal('INTERACTION', { - type: 'favourite', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; @@ -128,10 +137,13 @@ class Footer extends ImmutablePureComponent { dispatch(initBoostModal({ status, onReblog: this._performReblog })); } } else { - dispatch(openModal('INTERACTION', { - type: 'reblog', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; diff --git a/app/javascript/mastodon/features/report/components/status_check_box.jsx b/app/javascript/mastodon/features/report/components/status_check_box.jsx index c22745ee5d..8125b5b3bb 100644 --- a/app/javascript/mastodon/features/report/components/status_check_box.jsx +++ b/app/javascript/mastodon/features/report/components/status_check_box.jsx @@ -17,7 +17,7 @@ import Option from './option'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index bd565c72c6..2ce94d9d84 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -38,9 +38,9 @@ const messages = defineMessages({ unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, - admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' }, admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, - copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 00fd0d2cd6..187e04ad17 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -26,8 +26,8 @@ import Card from './card'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); class DetailedStatus extends ImmutablePureComponent { diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index ddae8c0283..e76790a9f6 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -60,10 +60,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch((_, getState) => { let state = getState(); if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, router)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status, router)), + }, })); } else { dispatch(replyCompose(status, router)); @@ -104,9 +107,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onEmbed (status) { - dispatch(openModal('EMBED', { - url: status.get('url'), - onError: error => dispatch(showAlertForError(error)), + dispatch(openModal({ + modalType: 'EMBED', + modalProps: { + url: status.get('url'), + onError: error => dispatch(showAlertForError(error)), + }, })); }, @@ -114,10 +120,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (!deleteModal) { dispatch(deleteStatus(status.get('id'), history, withRedraft)); } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + }, })); } }, @@ -131,11 +140,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onOpenMedia (media, index, lang) { - dispatch(openModal('MEDIA', { media, index, lang })); + dispatch(openModal({ + modalType: 'MEDIA', + modalProps: { media, index, lang }, + })); }, onOpenVideo (media, lang, options) { - dispatch(openModal('VIDEO', { media, lang, options })); + dispatch(openModal({ + modalType: 'VIDEO', + modalProps: { media, lang, options }, + })); }, onBlock (status) { diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 4c41958aae..0ed94d34c0 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -79,7 +79,7 @@ const messages = defineMessages({ detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, }); const makeMapStateToProps = () => { @@ -251,10 +251,13 @@ class Status extends ImmutablePureComponent { dispatch(favourite(status)); } } else { - dispatch(openModal('INTERACTION', { - type: 'favourite', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; @@ -273,19 +276,25 @@ class Status extends ImmutablePureComponent { if (signedIn) { if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), + }, })); } else { dispatch(replyCompose(status, this.context.router.history)); } } else { - dispatch(openModal('INTERACTION', { - type: 'reply', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; @@ -309,10 +318,13 @@ class Status extends ImmutablePureComponent { } } } else { - dispatch(openModal('INTERACTION', { - type: 'reblog', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; @@ -331,10 +343,13 @@ class Status extends ImmutablePureComponent { if (!deleteModal) { dispatch(deleteStatus(status.get('id'), history, withRedraft)); } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + }, })); } }; @@ -352,11 +367,17 @@ class Status extends ImmutablePureComponent { }; handleOpenMedia = (media, index, lang) => { - this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index, lang })); + this.props.dispatch(openModal({ + modalType: 'MEDIA', + modalProps: { statusId: this.props.status.get('id'), media, index, lang }, + })); }; handleOpenVideo = (media, lang, options) => { - this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, lang, options })); + this.props.dispatch(openModal({ + modalType: 'VIDEO', + modalProps: { statusId: this.props.status.get('id'), media, lang, options }, + })); }; handleHotkeyOpenMedia = e => { @@ -425,7 +446,10 @@ class Status extends ImmutablePureComponent { }; handleEmbed = (status) => { - this.props.dispatch(openModal('EMBED', { url: status.get('url') })); + this.props.dispatch(openModal({ + modalType: 'EMBED', + modalProps: { url: status.get('url') }, + })); }; handleUnmuteClick = account => { @@ -437,10 +461,13 @@ class Status extends ImmutablePureComponent { }; handleBlockDomainClick = domain => { - this.props.dispatch(openModal('CONFIRM', { - message: {domain} }} />, - confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => this.props.dispatch(blockDomain(domain)), + this.props.dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: {domain} }} />, + confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => this.props.dispatch(blockDomain(domain)), + }, })); }; diff --git a/app/javascript/mastodon/features/ui/components/block_modal.jsx b/app/javascript/mastodon/features/ui/components/block_modal.jsx index 962b19344e..7cfd252e0b 100644 --- a/app/javascript/mastodon/features/ui/components/block_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/block_modal.jsx @@ -33,7 +33,10 @@ const mapDispatchToProps = dispatch => { }, onClose() { - dispatch(closeModal()); + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })); }, }; }; diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.jsx b/app/javascript/mastodon/features/ui/components/boost_modal.jsx index af6c08134a..ee87cf2cdc 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/boost_modal.jsx @@ -24,8 +24,8 @@ const messages = defineMessages({ reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); const mapStateToProps = state => { diff --git a/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx index 4a55f25cbe..740537fa6e 100644 --- a/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx @@ -23,7 +23,10 @@ const mapStateToProps = (state, { statusId }) => ({ const mapDispatchToProps = dispatch => ({ onClose() { - dispatch(closeModal()); + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })); }, }); diff --git a/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx b/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx index 6c4fefb0d5..6a71bb2465 100644 --- a/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx +++ b/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx @@ -23,11 +23,14 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({ onLogout () { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.logoutMessage), - confirm: intl.formatMessage(messages.logoutConfirm), - closeWhenConfirm: false, - onConfirm: () => logOut(), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, + onConfirm: () => logOut(), + }, })); }, }); diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index 04651bab18..bb6747c00c 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -26,7 +26,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ openClosedRegistrationsModal() { - dispatch(openModal('CLOSED_REGISTRATIONS')); + dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); }, }); diff --git a/app/javascript/mastodon/features/ui/components/link_footer.jsx b/app/javascript/mastodon/features/ui/components/link_footer.jsx index 4ae6f1dcf4..b025174409 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.jsx +++ b/app/javascript/mastodon/features/ui/components/link_footer.jsx @@ -19,11 +19,14 @@ const messages = defineMessages({ const mapDispatchToProps = (dispatch, { intl }) => ({ onLogout () { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.logoutMessage), - confirm: intl.formatMessage(messages.logoutConfirm), - closeWhenConfirm: false, - onConfirm: () => logOut(), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, + onConfirm: () => logOut(), + }, })); }, }); diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 5700e0f54c..d5edb45b36 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -32,7 +32,7 @@ import MediaModal from './media_modal'; import ModalLoading from './modal_loading'; import VideoModal from './video_modal'; -const MODAL_COMPONENTS = { +export const MODAL_COMPONENTS = { 'MEDIA': () => Promise.resolve({ default: MediaModal }), 'VIDEO': () => Promise.resolve({ default: VideoModal }), 'AUDIO': () => Promise.resolve({ default: AudioModal }), diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.jsx b/app/javascript/mastodon/features/ui/components/mute_modal.jsx index dd21fa0465..708bd757cf 100644 --- a/app/javascript/mastodon/features/ui/components/mute_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/mute_modal.jsx @@ -34,7 +34,10 @@ const mapDispatchToProps = dispatch => { }, onClose() { - dispatch(closeModal()); + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })); }, onToggleNotifications() { diff --git a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx index 25de95fd37..dad36134cf 100644 --- a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx +++ b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx @@ -11,7 +11,7 @@ const SignInBanner = () => { const dispatch = useAppDispatch(); const openClosedRegistrationsModal = useCallback( - () => dispatch(openModal('CLOSED_REGISTRATIONS')), + () => dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })), [dispatch], ); diff --git a/app/javascript/mastodon/features/ui/containers/modal_container.js b/app/javascript/mastodon/features/ui/containers/modal_container.js index b14d0410d1..1c3872cd50 100644 --- a/app/javascript/mastodon/features/ui/containers/modal_container.js +++ b/app/javascript/mastodon/features/ui/containers/modal_container.js @@ -13,14 +13,22 @@ const mapDispatchToProps = dispatch => ({ onClose (confirmationMessage, ignoreFocus = false) { if (confirmationMessage) { dispatch( - openModal('CONFIRM', { - message: confirmationMessage.message, - confirm: confirmationMessage.confirm, - onConfirm: () => dispatch(closeModal(undefined, { ignoreFocus })), - }), + openModal({ + modalType: 'CONFIRM', + modalProps: { + message: confirmationMessage.message, + confirm: confirmationMessage.confirm, + onConfirm: () => dispatch(closeModal({ + modalType: undefined, + ignoreFocus: { ignoreFocus }, + })), + } }), ); } else { - dispatch(closeModal(undefined, { ignoreFocus })); + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: { ignoreFocus }, + })); } }, }); diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index c64a970ead..d446989ab6 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -57,7 +57,7 @@ "id": "account.unfollow" }, { - "defaultMessage": "Awaiting approval", + "defaultMessage": "Awaiting approval. Click to cancel follow request", "id": "account.requested" }, { @@ -721,7 +721,7 @@ "id": "privacy.unlisted.short" }, { - "defaultMessage": "Followers-only", + "defaultMessage": "Followers only", "id": "privacy.private.short" }, { @@ -792,7 +792,7 @@ "id": "confirmations.domain_block.confirm" }, { - "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", + "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", "id": "confirmations.domain_block.message" } ], @@ -833,7 +833,7 @@ "id": "confirmations.edit.message" }, { - "defaultMessage": "Hide entire domain", + "defaultMessage": "Block entire domain", "id": "confirmations.domain_block.confirm" }, { @@ -980,7 +980,7 @@ "id": "confirmations.unfollow.confirm" }, { - "defaultMessage": "Hide entire domain", + "defaultMessage": "Block entire domain", "id": "confirmations.domain_block.confirm" }, { @@ -1412,7 +1412,7 @@ "id": "navigation_bar.blocks" }, { - "defaultMessage": "Hidden domains", + "defaultMessage": "Blocked domains", "id": "navigation_bar.domain_blocks" }, { @@ -2430,7 +2430,7 @@ "id": "navigation_bar.blocks" }, { - "defaultMessage": "Hidden domains", + "defaultMessage": "Blocked domains", "id": "navigation_bar.domain_blocks" }, { @@ -3535,7 +3535,7 @@ "id": "privacy.unlisted.short" }, { - "defaultMessage": "Followers-only", + "defaultMessage": "Followers only", "id": "privacy.private.short" }, { @@ -3743,7 +3743,7 @@ "id": "status.admin_account" }, { - "defaultMessage": "Open this status in the moderation interface", + "defaultMessage": "Open this post in the moderation interface", "id": "status.admin_status" }, { @@ -3751,7 +3751,7 @@ "id": "status.admin_domain" }, { - "defaultMessage": "Copy link to status", + "defaultMessage": "Copy link to post", "id": "status.copy" }, { @@ -3797,11 +3797,11 @@ "id": "privacy.unlisted.short" }, { - "defaultMessage": "Followers-only", + "defaultMessage": "Followers only", "id": "privacy.private.short" }, { - "defaultMessage": "Direct", + "defaultMessage": "Mentioned people only", "id": "privacy.direct.short" }, { @@ -3854,7 +3854,7 @@ "id": "confirmations.reply.message" }, { - "defaultMessage": "Hide entire domain", + "defaultMessage": "Block entire domain", "id": "confirmations.domain_block.confirm" }, { @@ -3925,11 +3925,11 @@ "id": "privacy.unlisted.short" }, { - "defaultMessage": "Followers-only", + "defaultMessage": "Followers only", "id": "privacy.private.short" }, { - "defaultMessage": "Direct", + "defaultMessage": "Mentioned people only", "id": "privacy.direct.short" }, { diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index 29c9abe68b..16047b26d8 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -25,7 +25,7 @@ import markers from './markers'; import media_attachments from './media_attachments'; import meta from './meta'; import { missedUpdatesReducer } from './missed_updates'; -import modal from './modal'; +import { modalReducer } from './modal'; import mutes from './mutes'; import notifications from './notifications'; import picture_in_picture from './picture_in_picture'; @@ -50,7 +50,7 @@ const reducers = { meta, alerts, loadingBar: loadingBarReducer, - modal, + modal: modalReducer, user_lists, domain_lists, status_lists, diff --git a/app/javascript/mastodon/reducers/modal.js b/app/javascript/mastodon/reducers/modal.js deleted file mode 100644 index 348f538991..0000000000 --- a/app/javascript/mastodon/reducers/modal.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Stack as ImmutableStack, Map as ImmutableMap } from 'immutable'; - -import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; -import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; -import { TIMELINE_DELETE } from '../actions/timelines'; - -const initialState = ImmutableMap({ - ignoreFocus: false, - stack: ImmutableStack(), -}); - -const popModal = (state, { modalType, ignoreFocus }) => { - if (modalType === undefined || modalType === state.getIn(['stack', 0, 'modalType'])) { - return state.set('ignoreFocus', !!ignoreFocus).update('stack', stack => stack.shift()); - } else { - return state; - } -}; - -const pushModal = (state, modalType, modalProps) => { - return state.withMutations(map => { - map.set('ignoreFocus', false); - map.update('stack', stack => stack.unshift(ImmutableMap({ modalType, modalProps }))); - }); -}; - -export default function modal(state = initialState, action) { - switch(action.type) { - case MODAL_OPEN: - return pushModal(state, action.modalType, action.modalProps); - case MODAL_CLOSE: - return popModal(state, action); - case COMPOSE_UPLOAD_CHANGE_SUCCESS: - return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false }); - case TIMELINE_DELETE: - return state.update('stack', stack => stack.filterNot((modal) => modal.get('modalProps').statusId === action.id)); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/modal.ts b/app/javascript/mastodon/reducers/modal.ts new file mode 100644 index 0000000000..6afbcc367c --- /dev/null +++ b/app/javascript/mastodon/reducers/modal.ts @@ -0,0 +1,94 @@ +import { Record as ImmutableRecord, Stack } from 'immutable'; + +import type { PayloadAction } from '@reduxjs/toolkit'; + +import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; +import type { ModalType } from '../actions/modal'; +import { openModal, closeModal } from '../actions/modal'; +import { TIMELINE_DELETE } from '../actions/timelines'; + +type ModalProps = Record; +interface Modal { + modalType: ModalType; + modalProps: ModalProps; +} + +const Modal = ImmutableRecord({ + modalType: 'ACTIONS', + modalProps: ImmutableRecord({})(), +}); + +interface ModalState { + ignoreFocus: boolean; + stack: Stack>; +} + +const initialState = ImmutableRecord({ + ignoreFocus: false, + stack: Stack(), +})(); +type State = typeof initialState; + +interface PopModalOption { + modalType: ModalType | undefined; + ignoreFocus: boolean; +} +const popModal = ( + state: State, + { modalType, ignoreFocus }: PopModalOption +): State => { + if ( + modalType === undefined || + modalType === state.get('stack').get(0)?.get('modalType') + ) { + return state + .set('ignoreFocus', !!ignoreFocus) + .update('stack', (stack) => stack.shift()); + } else { + return state; + } +}; + +const pushModal = ( + state: State, + modalType: ModalType, + modalProps: ModalProps +): State => { + return state.withMutations((record) => { + record.set('ignoreFocus', false); + record.update('stack', (stack) => + stack.unshift(Modal({ modalType, modalProps })) + ); + }); +}; + +export function modalReducer( + state: State = initialState, + action: PayloadAction<{ + modalType: ModalType; + ignoreFocus: boolean; + modalProps: Record; + }> +) { + switch (action.type) { + case openModal.type: + return pushModal( + state, + action.payload.modalType, + action.payload.modalProps + ); + case closeModal.type: + return popModal(state, action.payload); + case COMPOSE_UPLOAD_CHANGE_SUCCESS: + return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false }); + case TIMELINE_DELETE: + return state.update('stack', (stack) => + stack.filterNot( + // @ts-expect-error TIMELINE_DELETE action is not typed yet. + (modal) => modal.get('modalProps').statusId === action.id + ) + ); + default: + return state; + } +} diff --git a/app/models/account.rb b/app/models/account.rb index bbc9bda021..2549279c58 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -123,7 +123,7 @@ class Account < ApplicationRecord scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) } scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) } scope :popular, -> { order('account_stats.followers_count desc') } - scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) } + scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomain(domain).select(:domain)) } scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) } diff --git a/app/models/instance.rb b/app/models/instance.rb index 95231c52a7..a80e91e94c 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -22,6 +22,7 @@ class Instance < ApplicationRecord end scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } + scope :by_domain_and_subdomain, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") } def self.refresh Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false) diff --git a/db/migrate/20230524190515_add_index_accounts_on_domain_and_id.rb b/db/migrate/20230524190515_add_index_accounts_on_domain_and_id.rb new file mode 100644 index 0000000000..4ee52b0ca8 --- /dev/null +++ b/db/migrate/20230524190515_add_index_accounts_on_domain_and_id.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddIndexAccountsOnDomainAndId < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :accounts, [:domain, :id], name: :index_accounts_on_domain_and_id, algorithm: :concurrently + end +end diff --git a/db/migrate/20230524192812_fix_account_domain_casing.rb b/db/migrate/20230524192812_fix_account_domain_casing.rb new file mode 100644 index 0000000000..e1edf23749 --- /dev/null +++ b/db/migrate/20230524192812_fix_account_domain_casing.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class FixAccountDomainCasing < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def up + safety_assured do + execute 'UPDATE accounts SET domain = lower(domain) WHERE domain IS NOT NULL AND domain != lower(domain)' + end + end + + def down; end +end diff --git a/db/migrate/20230524194155_add_index_instances_on_reverse_domain.rb b/db/migrate/20230524194155_add_index_instances_on_reverse_domain.rb new file mode 100644 index 0000000000..c3e774936c --- /dev/null +++ b/db/migrate/20230524194155_add_index_instances_on_reverse_domain.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddIndexInstancesOnReverseDomain < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :instances, "reverse('.' || domain), domain", name: :index_instances_on_reverse_domain, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index e834d7e09b..b1fbaecab8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_03_30_155710) do +ActiveRecord::Schema.define(version: 2023_05_24_194155) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -188,6 +188,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do t.datetime "requested_review_at" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true + t.index ["domain", "id"], name: "index_accounts_on_domain_and_id" t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id", where: "(moved_to_account_id IS NOT NULL)" t.index ["uri"], name: "index_accounts_on_uri" t.index ["url"], name: "index_accounts_on_url", opclass: :text_pattern_ops, where: "(url IS NOT NULL)" @@ -1286,6 +1287,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do FROM (domain_allows LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text))); SQL + add_index "instances", "reverse(('.'::text || (domain)::text)), domain", name: "index_instances_on_reverse_domain" add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true create_view "user_ips", sql_definition: <<-SQL diff --git a/lib/mastodon/cli/maintenance.rb b/lib/mastodon/cli/maintenance.rb index 660adef80e..b107480359 100644 --- a/lib/mastodon/cli/maintenance.rb +++ b/lib/mastodon/cli/maintenance.rb @@ -231,34 +231,9 @@ module Mastodon::CLI end end - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| - users = User.where(id: row['ids'].split(',')).sort_by(&:created_at).reverse.drop(1) - @prompt.warn "Unsetting confirmation token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}" - - users.each do |user| - user.update!(confirmation_token: nil) - end - end - - if ActiveRecord::Migrator.current_version < 2022_01_18_183010 - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row| - users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1) - @prompt.warn "Unsetting remember token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}" - - users.each do |user| - user.update!(remember_token: nil) - end - end - end - - ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| - users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1) - @prompt.warn "Unsetting password reset token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}" - - users.each do |user| - user.update!(reset_password_token: nil) - end - end + deduplicate_users_process_confirmation_token + deduplicate_users_process_remember_token + deduplicate_users_process_password_token @prompt.say 'Restoring users indexes…' ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true @@ -272,6 +247,41 @@ module Mastodon::CLI end end + def deduplicate_users_process_confirmation_token + ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row| + users = User.where(id: row['ids'].split(',')).sort_by(&:created_at).reverse.drop(1) + @prompt.warn "Unsetting confirmation token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}" + + users.each do |user| + user.update!(confirmation_token: nil) + end + end + end + + def deduplicate_users_process_remember_token + if ActiveRecord::Migrator.current_version < 2022_01_18_183010 + ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row| + users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1) + @prompt.warn "Unsetting remember token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}" + + users.each do |user| + user.update!(remember_token: nil) + end + end + end + end + + def deduplicate_users_process_password_token + ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row| + users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1) + @prompt.warn "Unsetting password reset token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}" + + users.each do |user| + user.update!(reset_password_token: nil) + end + end + end + def deduplicate_account_domain_blocks! remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain') diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index 60399c9de1..3c88ce4509 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -58,6 +58,11 @@ namespace :tests do puts 'User settings not kept as expected' exit(1) end + + unless Account.find_remote('bob', 'ActivityPub.com').domain == 'activitypub.com' + puts 'Account domains not properly normalized' + exit(1) + end end desc 'Populate the database with test data for 2.4.3' @@ -160,7 +165,7 @@ namespace :tests do INSERT INTO "accounts" (id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url) VALUES - (6, 'bob', 'activitypub.com', NULL, #{remote_public_key_ap}, now(), now(), + (6, 'bob', 'ActivityPub.com', NULL, #{remote_public_key_ap}, now(), now(), 1, 'https://activitypub.com/users/bob/inbox', 'https://activitypub.com/users/bob/outbox', 'https://activitypub.com/users/bob/followers'); INSERT INTO "accounts" diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index a80273635d..b52d7c6ce4 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -244,9 +244,26 @@ RSpec.describe Auth::RegistrationsController do end end - it 'does nothing if user already exists' do - Fabricate(:account, username: 'test') - subject + context 'with an already taken username' do + subject do + Setting.registrations_mode = 'open' + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' } } + end + + before do + Fabricate(:account, username: 'test') + end + + it 'responds with an error message about the username' do + subject + + expect(response).to have_http_status(:success) + expect(username_error_text).to eq(I18n.t('errors.messages.taken')) + end + + def username_error_text + Nokogiri::Slop(response.body).css('.user_account_username .error').text + end end include_examples 'checks for enabled registrations', :create diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb index 86ab59b29e..18ba00244d 100644 --- a/spec/services/precompute_feed_service_spec.rb +++ b/spec/services/precompute_feed_service_spec.rb @@ -19,7 +19,7 @@ RSpec.describe PrecomputeFeedService, type: :service do it 'does not raise an error even if it could not find any status' do account = Fabricate(:account) - subject.call(account) + expect { subject.call(account) }.to_not raise_error end it 'filters statuses' do