Merge branch 'main' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2023-05-28 17:01:25 +02:00
commit 0669783da8
59 changed files with 650 additions and 349 deletions

View File

@ -531,13 +531,6 @@ RSpec/MultipleMemoizedHelpers:
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 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: RSpec/PendingWithoutReason:
Exclude: Exclude:
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'

View File

@ -95,6 +95,6 @@ export function initBlockModal(account) {
account, account,
}); });
dispatch(openModal('BLOCK')); dispatch(openModal({ modalType: 'BLOCK' }));
}; };
} }

View File

@ -14,7 +14,10 @@ export function initBoostModal(props) {
privacy, privacy,
}); });
dispatch(openModal('BOOST', props)); dispatch(openModal({
modalType: 'BOOST',
modalProps: props,
}));
}; };
} }

View File

@ -383,7 +383,10 @@ export function initMediaEditModal(id) {
id, id,
}); });
dispatch(openModal('FOCAL_POINT', { id })); dispatch(openModal({
modalType: 'FOCAL_POINT',
modalProps: { id },
}));
}; };
} }

View File

@ -15,9 +15,12 @@ export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL'; export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
export const initAddFilter = (status, { contextType }) => dispatch => export const initAddFilter = (status, { contextType }) => dispatch =>
dispatch(openModal('FILTER', { dispatch(openModal({
statusId: status?.get('id'), modalType: 'FILTER',
contextType: contextType, modalProps: {
statusId: status?.get('id'),
contextType: contextType,
},
})); }));
export const fetchFilters = () => (dispatch, getState) => { export const fetchFilters = () => (dispatch, getState) => {

View File

@ -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,
};
}

View File

@ -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<OpenModalPayload>('MODAL_OPEN');
interface CloseModalPayload {
modalType: ModalType | undefined;
ignoreFocus: boolean;
}
export const closeModal = createAction<CloseModalPayload>('MODAL_CLOSE');

View File

@ -97,7 +97,7 @@ export function initMuteModal(account) {
account, account,
}); });
dispatch(openModal('MUTE')); dispatch(openModal({ modalType: 'MUTE' }));
}; };
} }

View File

@ -7,9 +7,12 @@ export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
export const initReport = (account, status) => dispatch => export const initReport = (account, status) => dispatch =>
dispatch(openModal('REPORT', { dispatch(openModal({
accountId: account.get('id'), modalType: 'REPORT',
statusId: status?.get('id'), modalProps: {
accountId: account.get('id'),
statusId: status?.get('id'),
},
})); }));
export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {

View File

@ -23,7 +23,7 @@ import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, 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}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },

View File

@ -15,7 +15,10 @@ import DropdownMenu from './containers/dropdown_menu_container';
const mapDispatchToProps = (dispatch, { statusId }) => ({ const mapDispatchToProps = (dispatch, { statusId }) => ({
onItemClick (index) { onItemClick (index) {
dispatch(openModal('COMPARE_HISTORY', { index, statusId })); dispatch(openModal({
modalType: 'COMPARE_HISTORY',
modalProps: { index, statusId },
}));
}, },
}); });

View File

@ -16,21 +16,21 @@ interface Props {
intl: InjectedIntl; intl: InjectedIntl;
} }
export const LoadGap = injectIntl<Props>( const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => {
({ disabled, maxId, onClick, intl }) => { const handleClick = useCallback(() => {
const handleClick = useCallback(() => { onClick(maxId);
onClick(maxId); }, [maxId, onClick]);
}, [maxId, onClick]);
return ( return (
<button <button
className='load-more load-gap' className='load-more load-gap'
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
aria-label={intl.formatMessage(messages.load_more)} aria-label={intl.formatMessage(messages.load_more)}
> >
<Icon id='ellipsis-h' /> <Icon id='ellipsis-h' />
</button> </button>
); );
} };
);
export const LoadGap = injectIntl(_LoadGap);

View File

@ -59,7 +59,7 @@ export const defaultMediaVisibility = (status) => {
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, 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' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
}); });

View File

@ -35,10 +35,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.unfollowConfirm), modalProps: {
onConfirm: () => dispatch(unfollowAccount(account.get('id'))), message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
},
})); }));
} else { } else {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));

View File

@ -18,10 +18,13 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onBlockDomain (domain) { onBlockDomain (domain) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />, modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.blockDomainConfirm), modalProps: {
onConfirm: () => dispatch(blockDomain(domain)), message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
})); }));
}, },

View File

@ -18,15 +18,21 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
dispatch(fetchRelationships([status.getIn(['account', 'id'])])); dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
} }
dispatch(isUserTouching() ? openModal('ACTIONS', { dispatch(isUserTouching() ? openModal({
status, modalType: 'ACTIONS',
actions: items, modalProps: {
onClick: onItemClick, status,
actions: items,
onClick: onItemClick,
},
}) : openDropdownMenu(id, keyboard, scrollKey)); }) : openDropdownMenu(id, keyboard, scrollKey));
}, },
onClose(id) { onClose(id) {
dispatch(closeModal('ACTIONS')); dispatch(closeModal({
modalType: 'ACTIONS',
ignoreFocus: false,
}));
dispatch(closeDropdownMenu(id)); dispatch(closeDropdownMenu(id));
}, },
}); });

View File

@ -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?' }, 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' }, 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?' }, 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 = () => { const makeMapStateToProps = () => {
@ -82,10 +82,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
let state = getState(); let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) { if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.replyMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.replyConfirm), modalProps: {
onConfirm: () => dispatch(replyCompose(status, router)), message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)) },
})); }));
} else { } else {
dispatch(replyCompose(status, router)); dispatch(replyCompose(status, router));
@ -134,9 +136,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
onEmbed (status) { onEmbed (status) {
dispatch(openModal('EMBED', { dispatch(openModal({
url: status.get('url'), modalType: 'EMBED',
onError: error => dispatch(showAlertForError(error)), modalProps: {
url: status.get('url'),
onError: error => dispatch(showAlertForError(error)),
},
})); }));
}, },
@ -144,10 +149,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft)); dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), modalProps: {
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), 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) => { dispatch((_, getState) => {
let state = getState(); let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) { if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.editMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.editConfirm), modalProps: {
onConfirm: () => dispatch(editStatus(status.get('id'), history)), message: intl.formatMessage(messages.editMessage),
confirm: intl.formatMessage(messages.editConfirm),
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
},
})); }));
} else { } else {
dispatch(editStatus(status.get('id'), history)); dispatch(editStatus(status.get('id'), history));
@ -184,11 +195,17 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
onOpenMedia (statusId, media, index, lang) { 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) { onOpenVideo (statusId, media, lang, options) {
dispatch(openModal('VIDEO', { statusId, media, lang, options })); dispatch(openModal({
modalType: 'VIDEO',
modalProps: { statusId, media, lang, options },
}));
}, },
onBlock (status) { onBlock (status) {
@ -237,10 +254,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
onBlockDomain (domain) { onBlockDomain (domain) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />, modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.blockDomainConfirm), modalProps: {
onConfirm: () => dispatch(blockDomain(domain)), message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
})); }));
}, },
@ -253,10 +273,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
onInteractionModal (type, status) { onInteractionModal (type, status) {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type, modalType: 'INTERACTION',
accountId: status.getIn(['account', 'id']), modalProps: {
url: status.get('url'), type,
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
})); }));
}, },

View File

@ -143,14 +143,23 @@ class AccountGallery extends ImmutablePureComponent {
const lang = attachment.getIn(['status', 'language']); const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') { 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') { } 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 { } else {
const media = attachment.getIn(['status', 'media_attachments']); const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id')); 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 },
}));
} }
}; };

View File

@ -28,7 +28,7 @@ import Header from '../components/header';
const messages = defineMessages({ const messages = defineMessages({
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' }, cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, 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 = () => { const makeMapStateToProps = () => {
@ -48,20 +48,26 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'following'])) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.unfollowConfirm), modalProps: {
onConfirm: () => dispatch(unfollowAccount(account.get('id'))), message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
},
})); }));
} else { } else {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));
} }
} else if (account.getIn(['relationship', 'requested'])) { } else if (account.getIn(['relationship', 'requested'])) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), modalProps: {
onConfirm: () => dispatch(unfollowAccount(account.get('id'))), message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
},
})); }));
} else { } else {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));
@ -72,10 +78,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onInteractionModal (account) { onInteractionModal (account) {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type: 'follow', modalType: 'INTERACTION',
accountId: account.get('id'), modalProps: {
url: account.get('url'), type: 'follow',
accountId: account.get('id'),
url: account.get('url'),
},
})); }));
}, },
@ -132,10 +141,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onBlockDomain (domain) { onBlockDomain (domain) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />, modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.blockDomainConfirm), modalProps: {
onConfirm: () => dispatch(blockDomain(domain)), message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
})); }));
}, },
@ -144,21 +156,30 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onAddToList (account) { onAddToList (account) {
dispatch(openModal('LIST_ADDER', { dispatch(openModal({
accountId: account.get('id'), modalType: 'LIST_ADDER',
modalProps: {
accountId: account.get('id'),
},
})); }));
}, },
onChangeLanguages (account) { onChangeLanguages (account) {
dispatch(openModal('SUBSCRIBED_LANGUAGES', { dispatch(openModal({
accountId: account.get('id'), modalType: 'SUBSCRIBED_LANGUAGES',
modalProps: {
accountId: account.get('id'),
},
})); }));
}, },
onOpenAvatar (account) { onOpenAvatar (account) {
dispatch(openModal('IMAGE', { dispatch(openModal({
src: account.get('avatar'), modalType: 'IMAGE',
alt: account.get('acct'), modalProps: {
src: account.get('avatar'),
alt: account.get('acct'),
},
})); }));
}, },

View File

@ -16,7 +16,7 @@ const messages = defineMessages({
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, 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' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },

View File

@ -21,11 +21,14 @@ const mapStateToProps = state => {
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onLogout () { onLogout () {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.logoutMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.logoutConfirm), modalProps: {
closeWhenConfirm: false, message: intl.formatMessage(messages.logoutMessage),
onConfirm: () => logOut(), confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
})); }));
}, },
}); });

View File

@ -16,8 +16,14 @@ const mapDispatchToProps = dispatch => ({
}, },
isUserTouching, isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)), onModalOpen: props => dispatch(openModal({
onModalClose: () => dispatch(closeModal()), modalType: 'ACTIONS',
modalProps: props,
})),
onModalClose: () => dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
})),
}); });

View File

@ -71,11 +71,14 @@ class Compose extends PureComponent {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.logoutMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.logoutConfirm), modalProps: {
closeWhenConfirm: false, message: intl.formatMessage(messages.logoutMessage),
onConfirm: () => logOut(), confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
})); }));
return false; return false;

View File

@ -41,10 +41,13 @@ const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
let state = getState(); let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) { if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.replyMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.replyConfirm), modalProps: {
onConfirm: () => dispatch(replyCompose(status, router)), message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)),
},
})); }));
} else { } else {
dispatch(replyCompose(status, router)); dispatch(replyCompose(status, router));

View File

@ -50,27 +50,32 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'following'])) {
if (unfollowModal) { if (unfollowModal) {
dispatch( dispatch(
openModal('CONFIRM', { openModal({
message: ( modalType: 'CONFIRM',
<FormattedMessage modalProps: {
id='confirmations.unfollow.message' message: (
defaultMessage='Are you sure you want to unfollow {name}?' <FormattedMessage
values={{ name: <strong>@{account.get('acct')}</strong> }} id='confirmations.unfollow.message'
/> defaultMessage='Are you sure you want to unfollow {name}?'
), values={{ name: <strong>@{account.get('acct')}</strong> }}
confirm: intl.formatMessage(messages.unfollowConfirm), />
onConfirm: () => dispatch(unfollowAccount(account.get('id'))), ),
}), confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
} }),
); );
} else { } else {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));
} }
} else if (account.getIn(['relationship', 'requested'])) { } else if (account.getIn(['relationship', 'requested'])) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), modalProps: {
onConfirm: () => dispatch(unfollowAccount(account.get('id'))), message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
},
})); }));
} else { } else {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));

View File

@ -34,7 +34,7 @@ const messages = defineMessages({
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, 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' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },

View File

@ -18,8 +18,11 @@ const mapStateToProps = (state, { accountId }) => ({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
onSignupClick() { onSignupClick() {
dispatch(closeModal()); dispatch(closeModal({
dispatch(openModal('CLOSED_REGISTRATIONS')); modalType: undefined,
ignoreFocus: false,
}));
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
}, },
}); });

View File

@ -114,24 +114,30 @@ class ListTimeline extends PureComponent {
}; };
handleEditClick = () => { 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 = () => { handleDeleteClick = () => {
const { dispatch, columnId, intl } = this.props; const { dispatch, columnId, intl } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.deleteMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.deleteConfirm), modalProps: {
onConfirm: () => { message: intl.formatMessage(messages.deleteMessage),
dispatch(deleteList(id)); confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => {
dispatch(deleteList(id));
if (columnId) { if (columnId) {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
this.context.router.history.push('/lists'); this.context.router.history.push('/lists');
} }
},
}, },
})); }));
}; };

View File

@ -59,10 +59,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onClear () { onClear () {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.clearMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.clearConfirm), modalProps: {
onConfirm: () => dispatch(clearNotifications()), message: intl.formatMessage(messages.clearMessage),
confirm: intl.formatMessage(messages.clearConfirm),
onConfirm: () => dispatch(clearNotifications()),
},
})); }));
}, },

View File

@ -74,19 +74,25 @@ class Footer extends ImmutablePureComponent {
if (signedIn) { if (signedIn) {
if (askReplyConfirmation) { if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.replyMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.replyConfirm), modalProps: {
onConfirm: this._performReply, message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: this._performReply,
},
})); }));
} else { } else {
this._performReply(); this._performReply();
} }
} else { } else {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type: 'reply', modalType: 'INTERACTION',
accountId: status.getIn(['account', 'id']), modalProps: {
url: status.get('url'), type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
})); }));
} }
}; };
@ -102,10 +108,13 @@ class Footer extends ImmutablePureComponent {
dispatch(favourite(status)); dispatch(favourite(status));
} }
} else { } else {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type: 'favourite', modalType: 'INTERACTION',
accountId: status.getIn(['account', 'id']), modalProps: {
url: status.get('url'), 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 })); dispatch(initBoostModal({ status, onReblog: this._performReblog }));
} }
} else { } else {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type: 'reblog', modalType: 'INTERACTION',
accountId: status.getIn(['account', 'id']), modalProps: {
url: status.get('url'), type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
})); }));
} }
}; };

View File

@ -17,7 +17,7 @@ import Option from './option';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, 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' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
}); });

View File

@ -38,9 +38,9 @@ const messages = defineMessages({
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, 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}' }, 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}' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },

View File

@ -26,8 +26,8 @@ import Card from './card';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, 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: 'Direct' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
}); });
class DetailedStatus extends ImmutablePureComponent { class DetailedStatus extends ImmutablePureComponent {

View File

@ -60,10 +60,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch((_, getState) => { dispatch((_, getState) => {
let state = getState(); let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) { if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.replyMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.replyConfirm), modalProps: {
onConfirm: () => dispatch(replyCompose(status, router)), message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)),
},
})); }));
} else { } else {
dispatch(replyCompose(status, router)); dispatch(replyCompose(status, router));
@ -104,9 +107,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onEmbed (status) { onEmbed (status) {
dispatch(openModal('EMBED', { dispatch(openModal({
url: status.get('url'), modalType: 'EMBED',
onError: error => dispatch(showAlertForError(error)), modalProps: {
url: status.get('url'),
onError: error => dispatch(showAlertForError(error)),
},
})); }));
}, },
@ -114,10 +120,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft)); dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), modalProps: {
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), 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) { onOpenMedia (media, index, lang) {
dispatch(openModal('MEDIA', { media, index, lang })); dispatch(openModal({
modalType: 'MEDIA',
modalProps: { media, index, lang },
}));
}, },
onOpenVideo (media, lang, options) { onOpenVideo (media, lang, options) {
dispatch(openModal('VIDEO', { media, lang, options })); dispatch(openModal({
modalType: 'VIDEO',
modalProps: { media, lang, options },
}));
}, },
onBlock (status) { onBlock (status) {

View File

@ -79,7 +79,7 @@ const messages = defineMessages({
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, 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?' }, 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 = () => { const makeMapStateToProps = () => {
@ -251,10 +251,13 @@ class Status extends ImmutablePureComponent {
dispatch(favourite(status)); dispatch(favourite(status));
} }
} else { } else {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type: 'favourite', modalType: 'INTERACTION',
accountId: status.getIn(['account', 'id']), modalProps: {
url: status.get('url'), type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
})); }));
} }
}; };
@ -273,19 +276,25 @@ class Status extends ImmutablePureComponent {
if (signedIn) { if (signedIn) {
if (askReplyConfirmation) { if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.replyMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.replyConfirm), modalProps: {
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
},
})); }));
} else { } else {
dispatch(replyCompose(status, this.context.router.history)); dispatch(replyCompose(status, this.context.router.history));
} }
} else { } else {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type: 'reply', modalType: 'INTERACTION',
accountId: status.getIn(['account', 'id']), modalProps: {
url: status.get('url'), type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
})); }));
} }
}; };
@ -309,10 +318,13 @@ class Status extends ImmutablePureComponent {
} }
} }
} else { } else {
dispatch(openModal('INTERACTION', { dispatch(openModal({
type: 'reblog', modalType: 'INTERACTION',
accountId: status.getIn(['account', 'id']), modalProps: {
url: status.get('url'), type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
})); }));
} }
}; };
@ -331,10 +343,13 @@ class Status extends ImmutablePureComponent {
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft)); dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), modalProps: {
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), 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) => { 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) => { 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 => { handleHotkeyOpenMedia = e => {
@ -425,7 +446,10 @@ class Status extends ImmutablePureComponent {
}; };
handleEmbed = (status) => { handleEmbed = (status) => {
this.props.dispatch(openModal('EMBED', { url: status.get('url') })); this.props.dispatch(openModal({
modalType: 'EMBED',
modalProps: { url: status.get('url') },
}));
}; };
handleUnmuteClick = account => { handleUnmuteClick = account => {
@ -437,10 +461,13 @@ class Status extends ImmutablePureComponent {
}; };
handleBlockDomainClick = domain => { handleBlockDomainClick = domain => {
this.props.dispatch(openModal('CONFIRM', { this.props.dispatch(openModal({
message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />, modalType: 'CONFIRM',
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), modalProps: {
onConfirm: () => this.props.dispatch(blockDomain(domain)), message: <FormattedMessage id='confirmations.domain_block.message' 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.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
},
})); }));
}; };

View File

@ -33,7 +33,10 @@ const mapDispatchToProps = dispatch => {
}, },
onClose() { onClose() {
dispatch(closeModal()); dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
}, },
}; };
}; };

View File

@ -24,8 +24,8 @@ const messages = defineMessages({
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, 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: 'Direct' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
}); });
const mapStateToProps = state => { const mapStateToProps = state => {

View File

@ -23,7 +23,10 @@ const mapStateToProps = (state, { statusId }) => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onClose() { onClose() {
dispatch(closeModal()); dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
}, },
}); });

View File

@ -23,11 +23,14 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onLogout () { onLogout () {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.logoutMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.logoutConfirm), modalProps: {
closeWhenConfirm: false, message: intl.formatMessage(messages.logoutMessage),
onConfirm: () => logOut(), confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
})); }));
}, },
}); });

View File

@ -26,7 +26,7 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
openClosedRegistrationsModal() { openClosedRegistrationsModal() {
dispatch(openModal('CLOSED_REGISTRATIONS')); dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
}, },
}); });

View File

@ -19,11 +19,14 @@ const messages = defineMessages({
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onLogout () { onLogout () {
dispatch(openModal('CONFIRM', { dispatch(openModal({
message: intl.formatMessage(messages.logoutMessage), modalType: 'CONFIRM',
confirm: intl.formatMessage(messages.logoutConfirm), modalProps: {
closeWhenConfirm: false, message: intl.formatMessage(messages.logoutMessage),
onConfirm: () => logOut(), confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
},
})); }));
}, },
}); });

View File

@ -32,7 +32,7 @@ import MediaModal from './media_modal';
import ModalLoading from './modal_loading'; import ModalLoading from './modal_loading';
import VideoModal from './video_modal'; import VideoModal from './video_modal';
const MODAL_COMPONENTS = { export const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }), 'MEDIA': () => Promise.resolve({ default: MediaModal }),
'VIDEO': () => Promise.resolve({ default: VideoModal }), 'VIDEO': () => Promise.resolve({ default: VideoModal }),
'AUDIO': () => Promise.resolve({ default: AudioModal }), 'AUDIO': () => Promise.resolve({ default: AudioModal }),

View File

@ -34,7 +34,10 @@ const mapDispatchToProps = dispatch => {
}, },
onClose() { onClose() {
dispatch(closeModal()); dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
}, },
onToggleNotifications() { onToggleNotifications() {

View File

@ -11,7 +11,7 @@ const SignInBanner = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const openClosedRegistrationsModal = useCallback( const openClosedRegistrationsModal = useCallback(
() => dispatch(openModal('CLOSED_REGISTRATIONS')), () => dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })),
[dispatch], [dispatch],
); );

View File

@ -13,14 +13,22 @@ const mapDispatchToProps = dispatch => ({
onClose (confirmationMessage, ignoreFocus = false) { onClose (confirmationMessage, ignoreFocus = false) {
if (confirmationMessage) { if (confirmationMessage) {
dispatch( dispatch(
openModal('CONFIRM', { openModal({
message: confirmationMessage.message, modalType: 'CONFIRM',
confirm: confirmationMessage.confirm, modalProps: {
onConfirm: () => dispatch(closeModal(undefined, { ignoreFocus })), message: confirmationMessage.message,
}), confirm: confirmationMessage.confirm,
onConfirm: () => dispatch(closeModal({
modalType: undefined,
ignoreFocus: { ignoreFocus },
})),
} }),
); );
} else { } else {
dispatch(closeModal(undefined, { ignoreFocus })); dispatch(closeModal({
modalType: undefined,
ignoreFocus: { ignoreFocus },
}));
} }
}, },
}); });

View File

@ -57,7 +57,7 @@
"id": "account.unfollow" "id": "account.unfollow"
}, },
{ {
"defaultMessage": "Awaiting approval", "defaultMessage": "Awaiting approval. Click to cancel follow request",
"id": "account.requested" "id": "account.requested"
}, },
{ {
@ -721,7 +721,7 @@
"id": "privacy.unlisted.short" "id": "privacy.unlisted.short"
}, },
{ {
"defaultMessage": "Followers-only", "defaultMessage": "Followers only",
"id": "privacy.private.short" "id": "privacy.private.short"
}, },
{ {
@ -792,7 +792,7 @@
"id": "confirmations.domain_block.confirm" "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" "id": "confirmations.domain_block.message"
} }
], ],
@ -833,7 +833,7 @@
"id": "confirmations.edit.message" "id": "confirmations.edit.message"
}, },
{ {
"defaultMessage": "Hide entire domain", "defaultMessage": "Block entire domain",
"id": "confirmations.domain_block.confirm" "id": "confirmations.domain_block.confirm"
}, },
{ {
@ -980,7 +980,7 @@
"id": "confirmations.unfollow.confirm" "id": "confirmations.unfollow.confirm"
}, },
{ {
"defaultMessage": "Hide entire domain", "defaultMessage": "Block entire domain",
"id": "confirmations.domain_block.confirm" "id": "confirmations.domain_block.confirm"
}, },
{ {
@ -1412,7 +1412,7 @@
"id": "navigation_bar.blocks" "id": "navigation_bar.blocks"
}, },
{ {
"defaultMessage": "Hidden domains", "defaultMessage": "Blocked domains",
"id": "navigation_bar.domain_blocks" "id": "navigation_bar.domain_blocks"
}, },
{ {
@ -2430,7 +2430,7 @@
"id": "navigation_bar.blocks" "id": "navigation_bar.blocks"
}, },
{ {
"defaultMessage": "Hidden domains", "defaultMessage": "Blocked domains",
"id": "navigation_bar.domain_blocks" "id": "navigation_bar.domain_blocks"
}, },
{ {
@ -3535,7 +3535,7 @@
"id": "privacy.unlisted.short" "id": "privacy.unlisted.short"
}, },
{ {
"defaultMessage": "Followers-only", "defaultMessage": "Followers only",
"id": "privacy.private.short" "id": "privacy.private.short"
}, },
{ {
@ -3743,7 +3743,7 @@
"id": "status.admin_account" "id": "status.admin_account"
}, },
{ {
"defaultMessage": "Open this status in the moderation interface", "defaultMessage": "Open this post in the moderation interface",
"id": "status.admin_status" "id": "status.admin_status"
}, },
{ {
@ -3751,7 +3751,7 @@
"id": "status.admin_domain" "id": "status.admin_domain"
}, },
{ {
"defaultMessage": "Copy link to status", "defaultMessage": "Copy link to post",
"id": "status.copy" "id": "status.copy"
}, },
{ {
@ -3797,11 +3797,11 @@
"id": "privacy.unlisted.short" "id": "privacy.unlisted.short"
}, },
{ {
"defaultMessage": "Followers-only", "defaultMessage": "Followers only",
"id": "privacy.private.short" "id": "privacy.private.short"
}, },
{ {
"defaultMessage": "Direct", "defaultMessage": "Mentioned people only",
"id": "privacy.direct.short" "id": "privacy.direct.short"
}, },
{ {
@ -3854,7 +3854,7 @@
"id": "confirmations.reply.message" "id": "confirmations.reply.message"
}, },
{ {
"defaultMessage": "Hide entire domain", "defaultMessage": "Block entire domain",
"id": "confirmations.domain_block.confirm" "id": "confirmations.domain_block.confirm"
}, },
{ {
@ -3925,11 +3925,11 @@
"id": "privacy.unlisted.short" "id": "privacy.unlisted.short"
}, },
{ {
"defaultMessage": "Followers-only", "defaultMessage": "Followers only",
"id": "privacy.private.short" "id": "privacy.private.short"
}, },
{ {
"defaultMessage": "Direct", "defaultMessage": "Mentioned people only",
"id": "privacy.direct.short" "id": "privacy.direct.short"
}, },
{ {

View File

@ -25,7 +25,7 @@ import markers from './markers';
import media_attachments from './media_attachments'; import media_attachments from './media_attachments';
import meta from './meta'; import meta from './meta';
import { missedUpdatesReducer } from './missed_updates'; import { missedUpdatesReducer } from './missed_updates';
import modal from './modal'; import { modalReducer } from './modal';
import mutes from './mutes'; import mutes from './mutes';
import notifications from './notifications'; import notifications from './notifications';
import picture_in_picture from './picture_in_picture'; import picture_in_picture from './picture_in_picture';
@ -50,7 +50,7 @@ const reducers = {
meta, meta,
alerts, alerts,
loadingBar: loadingBarReducer, loadingBar: loadingBarReducer,
modal, modal: modalReducer,
user_lists, user_lists,
domain_lists, domain_lists,
status_lists, status_lists,

View File

@ -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;
}
}

View File

@ -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<string, unknown>;
interface Modal {
modalType: ModalType;
modalProps: ModalProps;
}
const Modal = ImmutableRecord<Modal>({
modalType: 'ACTIONS',
modalProps: ImmutableRecord({})(),
});
interface ModalState {
ignoreFocus: boolean;
stack: Stack<ImmutableRecord<Modal>>;
}
const initialState = ImmutableRecord<ModalState>({
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<string, unknown>;
}>
) {
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;
}
}

View File

@ -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_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 :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 :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_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))) } scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }

View File

@ -22,6 +22,7 @@ class Instance < ApplicationRecord
end end
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } 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 def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false) Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -188,6 +188,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.datetime "requested_review_at" 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 "(((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 "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 ["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 ["uri"], name: "index_accounts_on_uri"
t.index ["url"], name: "index_accounts_on_url", opclass: :text_pattern_ops, where: "(url IS NOT NULL)" 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 FROM (domain_allows
LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text))); LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text)));
SQL 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 add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true
create_view "user_ips", sql_definition: <<-SQL create_view "user_ips", sql_definition: <<-SQL

View File

@ -231,34 +231,9 @@ module Mastodon::CLI
end end
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| deduplicate_users_process_confirmation_token
users = User.where(id: row['ids'].split(',')).sort_by(&:created_at).reverse.drop(1) deduplicate_users_process_remember_token
@prompt.warn "Unsetting confirmation token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}" deduplicate_users_process_password_token
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
@prompt.say 'Restoring users indexes…' @prompt.say 'Restoring users indexes…'
ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true 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
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! def deduplicate_account_domain_blocks!
remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain') remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain')

View File

@ -58,6 +58,11 @@ namespace :tests do
puts 'User settings not kept as expected' puts 'User settings not kept as expected'
exit(1) exit(1)
end end
unless Account.find_remote('bob', 'ActivityPub.com').domain == 'activitypub.com'
puts 'Account domains not properly normalized'
exit(1)
end
end end
desc 'Populate the database with test data for 2.4.3' desc 'Populate the database with test data for 2.4.3'
@ -160,7 +165,7 @@ namespace :tests do
INSERT INTO "accounts" INSERT INTO "accounts"
(id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url) (id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url)
VALUES 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'); 1, 'https://activitypub.com/users/bob/inbox', 'https://activitypub.com/users/bob/outbox', 'https://activitypub.com/users/bob/followers');
INSERT INTO "accounts" INSERT INTO "accounts"

View File

@ -244,9 +244,26 @@ RSpec.describe Auth::RegistrationsController do
end end
end end
it 'does nothing if user already exists' do context 'with an already taken username' do
Fabricate(:account, username: 'test') subject do
subject 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 end
include_examples 'checks for enabled registrations', :create include_examples 'checks for enabled registrations', :create

View File

@ -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 it 'does not raise an error even if it could not find any status' do
account = Fabricate(:account) account = Fabricate(:account)
subject.call(account) expect { subject.call(account) }.to_not raise_error
end end
it 'filters statuses' do it 'filters statuses' do