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

Conflicts:
- `.prettierignore`:
  Upstream added a file, glitch-soc had extra files.
  Took upstream's changes and moved glitch-soc's additions at the end.
This commit is contained in:
Claire committed 2025-06-26 18:04:37 +02:00
commit f7b4580b49
52 files changed
+616 -351

No files matched your search

@@ -1,53 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import SettingsIcon from '@/material-icons/400-20px/settings.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { requestBrowserPermission } from 'mastodon/actions/notifications';
import { changeSetting } from 'mastodon/actions/settings';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
class NotificationsPermissionBanner extends PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.dispatch(requestBrowserPermission());
};
handleClose = () => {
this.props.dispatch(changeSetting(['notifications', 'dismissPermissionBanner'], true));
};
render () {
const { intl } = this.props;
return (
<div className='notifications-permission-banner'>
<div className='notifications-permission-banner__close'>
<IconButton icon='times' iconComponent={CloseIcon} onClick={this.handleClose} title={intl.formatMessage(messages.close)} />
</div>
<h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2>
<p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' icon={SettingsIcon} /> }} /></p>
<Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button>
</div>
);
}
}
export default connect()(injectIntl(NotificationsPermissionBanner));
@@ -0,0 +1,74 @@
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAppDispatch } from '@/mastodon/store';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react';
import { requestBrowserPermission } from 'mastodon/actions/notifications';
import { changeSetting } from 'mastodon/actions/settings';
import { Button } from 'mastodon/components/button';
import { messages as columnHeaderMessages } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const NotificationsPermissionBanner: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleClick = useCallback(() => {
dispatch(requestBrowserPermission());
}, [dispatch]);
const handleClose = useCallback(() => {
dispatch(changeSetting(['notifications', 'dismissPermissionBanner'], true));
}, [dispatch]);
return (
<div className='notifications-permission-banner'>
<div className='notifications-permission-banner__close'>
<IconButton
icon='times'
iconComponent={CloseIcon}
onClick={handleClose}
title={intl.formatMessage(messages.close)}
/>
</div>
<h2>
<FormattedMessage
id='notifications_permission_banner.title'
defaultMessage='Never miss a thing'
/>
</h2>
<p>
<FormattedMessage
id='notifications_permission_banner.how_to_control'
defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled."
values={{
icon: (
<Icon
id='sliders'
icon={UnfoldMoreIcon}
aria-label={intl.formatMessage(columnHeaderMessages.show)}
/>
),
}}
/>
</p>
<Button onClick={handleClick}>
<FormattedMessage
id='notifications_permission_banner.enable'
defaultMessage='Enable desktop notifications'
/>
</Button>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default NotificationsPermissionBanner;
@@ -1,71 +1,30 @@
import { useLayoutEffect, useEffect, useState } from 'react';
import { useLayoutEffect } from 'react';
import { createAppSelector, useAppSelector } from 'mastodon/store';
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
const getShouldLockBodyScroll = createAppSelector(
[
(state) => state.navigation.open,
(state) => state.modal.get('stack').size > 0,
],
(isMobileMenuOpen: boolean, isModalOpen: boolean) => {
return isMobileMenuOpen || isModalOpen;
},
(isMobileMenuOpen: boolean, isModalOpen: boolean) =>
isMobileMenuOpen || isModalOpen,
);
/**
* This component locks scrolling on the `body` element when
* This component locks scrolling on the body when
* `getShouldLockBodyScroll` returns true.
*
* The scrollbar width is taken into account and written to
* a CSS custom property `--root-scrollbar-width`
*/
export const BodyScrollLock: React.FC = () => {
const shouldLockBodyScroll = useAppSelector(getShouldLockBodyScroll);
useLayoutEffect(() => {
document.body.classList.toggle('with-modals--active', shouldLockBodyScroll);
document.documentElement.classList.toggle(
'has-modal',
shouldLockBodyScroll,
);
}, [shouldLockBodyScroll]);
const [scrollbarWidth, setScrollbarWidth] = useState(() =>
getScrollbarWidth(),
);
useEffect(() => {
const handleResize = () => {
setScrollbarWidth(getScrollbarWidth());
};
window.addEventListener('resize', handleResize, { passive: true });
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// Inject style element to make scrollbar width available
// as CSS custom property
useLayoutEffect(() => {
const nonce = document
.querySelector('meta[name=style-nonce]')
?.getAttribute('content');
if (nonce) {
const styleEl = document.createElement('style');
styleEl.nonce = nonce;
styleEl.innerHTML = `
:root {
--root-scrollbar-width: ${scrollbarWidth}px;
}
`;
document.head.appendChild(styleEl);
return () => {
document.head.removeChild(styleEl);
};
}
return () => '';
}, [scrollbarWidth]);
return null;
};
@@ -13,6 +13,7 @@ export const ConfirmationModal: React.FC<
title: React.ReactNode;
message: React.ReactNode;
confirm: React.ReactNode;
cancel?: React.ReactNode;
secondary?: React.ReactNode;
onSecondary?: () => void;
onConfirm: () => void;
@@ -22,6 +23,7 @@ export const ConfirmationModal: React.FC<
title,
message,
confirm,
cancel,
onClose,
onConfirm,
secondary,
@@ -57,10 +59,12 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<button onClick={handleCancel} className='link-button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
{cancel ?? (
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
)}
</button>
{secondary && (
@@ -0,0 +1,104 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { replyCompose } from 'mastodon/actions/compose';
import { editStatus } from 'mastodon/actions/statuses';
import type { Status } from 'mastodon/models/status';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const editMessages = defineMessages({
title: {
id: 'confirmations.discard_draft.edit.title',
defaultMessage: 'Discard changes to your post?',
},
message: {
id: 'confirmations.discard_draft.edit.message',
defaultMessage:
'Continuing will discard any changes you have made to the post you are currently editing.',
},
cancel: {
id: 'confirmations.discard_draft.edit.cancel',
defaultMessage: 'Resume editing',
},
});
const postMessages = defineMessages({
title: {
id: 'confirmations.discard_draft.post.title',
defaultMessage: 'Discard your draft post?',
},
message: {
id: 'confirmations.discard_draft.post.message',
defaultMessage:
'Continuing will discard the post you are currently composing.',
},
cancel: {
id: 'confirmations.discard_draft.post.cancel',
defaultMessage: 'Resume draft',
},
});
const messages = defineMessages({
confirm: {
id: 'confirmations.discard_draft.confirm',
defaultMessage: 'Discard and continue',
},
});
const DiscardDraftConfirmationModal: React.FC<
{
onConfirm: () => void;
} & BaseConfirmationModalProps
> = ({ onConfirm, onClose }) => {
const intl = useIntl();
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
const contextualMessages = isEditing ? editMessages : postMessages;
return (
<ConfirmationModal
title={intl.formatMessage(contextualMessages.title)}
message={intl.formatMessage(contextualMessages.message)}
cancel={intl.formatMessage(contextualMessages.cancel)}
confirm={intl.formatMessage(messages.confirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};
export const ConfirmReplyModal: React.FC<
{
status: Status;
} & BaseConfirmationModalProps
> = ({ status, onClose }) => {
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(replyCompose(status));
}, [dispatch, status]);
return (
<DiscardDraftConfirmationModal onConfirm={onConfirm} onClose={onClose} />
);
};
export const ConfirmEditStatusModal: React.FC<
{
statusId: string;
} & BaseConfirmationModalProps
> = ({ statusId, onClose }) => {
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(editStatus(statusId));
}, [dispatch, statusId]);
return (
<DiscardDraftConfirmationModal onConfirm={onConfirm} onClose={onClose} />
);
};
@@ -1,45 +0,0 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { editStatus } from 'mastodon/actions/statuses';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
editTitle: {
id: 'confirmations.edit.title',
defaultMessage: 'Overwrite post?',
},
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?',
},
});
export const ConfirmEditStatusModal: React.FC<
{
statusId: string;
} & BaseConfirmationModalProps
> = ({ statusId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(editStatus(statusId));
}, [dispatch, statusId]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.editTitle)}
message={intl.formatMessage(messages.editMessage)}
confirm={intl.formatMessage(messages.editConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};
@@ -1,8 +1,10 @@
export { ConfirmationModal } from './confirmation_modal';
export { ConfirmDeleteStatusModal } from './delete_status';
export { ConfirmDeleteListModal } from './delete_list';
export { ConfirmReplyModal } from './reply';
export { ConfirmEditStatusModal } from './edit_status';
export {
ConfirmReplyModal,
ConfirmEditStatusModal,
} from './discard_draft_confirmation';
export { ConfirmUnfollowModal } from './unfollow';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
@@ -1,46 +0,0 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { replyCompose } from 'mastodon/actions/compose';
import type { Status } from 'mastodon/models/status';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
replyTitle: {
id: 'confirmations.reply.title',
defaultMessage: 'Overwrite post?',
},
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?',
},
});
export const ConfirmReplyModal: React.FC<
{
status: Status;
} & BaseConfirmationModalProps
> = ({ status, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(replyCompose(status));
}, [dispatch, status]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.replyTitle)}
message={intl.formatMessage(messages.replyMessage)}
confirm={intl.formatMessage(messages.replyConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};