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:
commit
f7b4580b49
52 files changed
+616
-351
No files matched your search
-53
@@ -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));
|
||||
+74
@@ -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;
|
||||
};
|
||||
+8
-4
@@ -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 && (
|
||||
|
||||
+104
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user