mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 08:19:05 +00:00
Merge commit '658addcbf783f6baa922d11c9524ebb9ddbcbc59' into glitch-soc/merge-upstream
This commit is contained in:
@@ -64,6 +64,14 @@ export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMIS
|
||||
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
|
||||
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
|
||||
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
|
||||
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST';
|
||||
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
|
||||
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
|
||||
|
||||
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
|
||||
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
|
||||
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
|
||||
@@ -496,6 +504,62 @@ export const dismissNotificationRequestFail = (id, error) => ({
|
||||
error,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequests = (ids) => (dispatch, getState) => {
|
||||
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
|
||||
dispatch(acceptNotificationRequestsRequest(ids));
|
||||
|
||||
api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
|
||||
dispatch(acceptNotificationRequestsSuccess(ids));
|
||||
dispatch(decreasePendingNotificationsCount(count));
|
||||
}).catch(err => {
|
||||
dispatch(acceptNotificationRequestFail(ids, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const acceptNotificationRequestsRequest = ids => ({
|
||||
type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
|
||||
ids,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequestsSuccess = ids => ({
|
||||
type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
|
||||
ids,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequestsFail = (ids, error) => ({
|
||||
type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
|
||||
ids,
|
||||
error,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
|
||||
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
|
||||
dispatch(acceptNotificationRequestsRequest(ids));
|
||||
|
||||
api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
|
||||
dispatch(dismissNotificationRequestsSuccess(ids));
|
||||
dispatch(decreasePendingNotificationsCount(count));
|
||||
}).catch(err => {
|
||||
dispatch(dismissNotificationRequestFail(ids, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const dismissNotificationRequestsRequest = ids => ({
|
||||
type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
|
||||
ids,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequestsSuccess = ids => ({
|
||||
type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
|
||||
ids,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequestsFail = (ids, error) => ({
|
||||
type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
|
||||
ids,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
|
||||
const current = getState().getIn(['notificationRequests', 'current']);
|
||||
const params = { account_id: accountId };
|
||||
|
||||
@@ -2,8 +2,8 @@ import { apiRequestGet, apiRequestPut } from 'mastodon/api';
|
||||
import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies';
|
||||
|
||||
export const apiGetNotificationPolicy = () =>
|
||||
apiRequestGet<NotificationPolicyJSON>('/v1/notifications/policy');
|
||||
apiRequestGet<NotificationPolicyJSON>('/v2/notifications/policy');
|
||||
|
||||
export const apiUpdateNotificationsPolicy = (
|
||||
policy: Partial<NotificationPolicyJSON>,
|
||||
) => apiRequestPut<NotificationPolicyJSON>('/v1/notifications/policy', policy);
|
||||
) => apiRequestPut<NotificationPolicyJSON>('/v2/notifications/policy', policy);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// See app/serializers/rest/notification_policy_serializer.rb
|
||||
|
||||
export type NotificationPolicyValue = 'accept' | 'filter' | 'drop';
|
||||
|
||||
export interface NotificationPolicyJSON {
|
||||
filter_not_following: boolean;
|
||||
filter_not_followers: boolean;
|
||||
filter_new_accounts: boolean;
|
||||
filter_private_mentions: boolean;
|
||||
for_not_following: NotificationPolicyValue;
|
||||
for_not_followers: NotificationPolicyValue;
|
||||
for_new_accounts: NotificationPolicyValue;
|
||||
for_private_mentions: NotificationPolicyValue;
|
||||
for_limited_accounts: NotificationPolicyValue;
|
||||
summary: {
|
||||
pending_requests_count: number;
|
||||
pending_notifications_count: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CheckIndeterminateSmallIcon from '@/material-icons/400-24px/check_indeterminate_small.svg?react';
|
||||
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||
|
||||
import { Icon } from './icon';
|
||||
@@ -7,6 +8,7 @@ import { Icon } from './icon';
|
||||
interface Props {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
name: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label: React.ReactNode;
|
||||
@@ -16,6 +18,7 @@ export const CheckBox: React.FC<Props> = ({
|
||||
name,
|
||||
value,
|
||||
checked,
|
||||
indeterminate,
|
||||
onChange,
|
||||
label,
|
||||
}) => {
|
||||
@@ -29,8 +32,14 @@ export const CheckBox: React.FC<Props> = ({
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<span className={classNames('check-box__input', { checked })}>
|
||||
{checked && <Icon id='check' icon={DoneIcon} />}
|
||||
<span
|
||||
className={classNames('check-box__input', { checked, indeterminate })}
|
||||
>
|
||||
{indeterminate ? (
|
||||
<Icon id='indeterminate' icon={CheckIndeterminateSmallIcon} />
|
||||
) : (
|
||||
checked && <Icon id='check' icon={DoneIcon} />
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span>{label}</span>
|
||||
|
||||
@@ -13,7 +13,7 @@ const listenerOptions = supportsPassiveEvents
|
||||
? { passive: true, capture: true }
|
||||
: true;
|
||||
|
||||
interface SelectItem {
|
||||
export interface SelectItem {
|
||||
value: string;
|
||||
icon?: string;
|
||||
iconComponent?: IconProp;
|
||||
|
||||
@@ -3,15 +3,21 @@ import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import { initBlockModal } from 'mastodon/actions/blocks';
|
||||
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||
import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
|
||||
import { initReport } from 'mastodon/actions/reports';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
import { toCappedNumber } from 'mastodon/utils/numbers';
|
||||
|
||||
@@ -20,12 +26,18 @@ const getAccount = makeGetAccount();
|
||||
const messages = defineMessages({
|
||||
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
|
||||
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
|
||||
view: { id: 'notification_requests.view', defaultMessage: 'View notifications' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||
export const NotificationRequest = ({ id, accountId, notificationsCount, checked, showCheckbox, toggleCheck }) => {
|
||||
const dispatch = useDispatch();
|
||||
const account = useSelector(state => getAccount(state, accountId));
|
||||
const intl = useIntl();
|
||||
const { push: historyPush } = useHistory();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dispatch(dismissNotificationRequest(id));
|
||||
@@ -35,9 +47,51 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||
dispatch(acceptNotificationRequest(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleMute = useCallback(() => {
|
||||
dispatch(initMuteModal(account));
|
||||
}, [dispatch, account]);
|
||||
|
||||
const handleBlock = useCallback(() => {
|
||||
dispatch(initBlockModal(account));
|
||||
}, [dispatch, account]);
|
||||
|
||||
const handleReport = useCallback(() => {
|
||||
dispatch(initReport(account));
|
||||
}, [dispatch, account]);
|
||||
|
||||
const handleView = useCallback(() => {
|
||||
historyPush(`/notifications/requests/${id}`);
|
||||
}, [historyPush, id]);
|
||||
|
||||
const menu = [
|
||||
{ text: intl.formatMessage(messages.view), action: handleView },
|
||||
null,
|
||||
{ text: intl.formatMessage(messages.accept), action: handleAccept },
|
||||
null,
|
||||
{ text: intl.formatMessage(messages.mute, { name: account.username }), action: handleMute, dangerous: true },
|
||||
{ text: intl.formatMessage(messages.block, { name: account.username }), action: handleBlock, dangerous: true },
|
||||
{ text: intl.formatMessage(messages.report, { name: account.username }), action: handleReport, dangerous: true },
|
||||
];
|
||||
|
||||
const handleCheck = useCallback(() => {
|
||||
toggleCheck(id);
|
||||
}, [toggleCheck, id]);
|
||||
|
||||
const handleClick = useCallback((e) => {
|
||||
if (showCheckbox) {
|
||||
toggleCheck(id);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, [toggleCheck, id, showCheckbox]);
|
||||
|
||||
return (
|
||||
<div className='notification-request'>
|
||||
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
|
||||
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- this is just a minor affordance, but we will need a comprehensive accessibility pass */
|
||||
<div className={classNames('notification-request', showCheckbox && 'notification-request--forced-checkbox')} onClick={handleClick}>
|
||||
<div className='notification-request__checkbox' aria-hidden={!showCheckbox}>
|
||||
<CheckBox checked={checked} onChange={handleCheck} />
|
||||
</div>
|
||||
<Link to={`/notifications/requests/${id}`} className='notification-request__link' onClick={handleClick} title={account?.acct}>
|
||||
<Avatar account={account} size={40} counter={toCappedNumber(notificationsCount)} />
|
||||
|
||||
<div className='notification-request__name'>
|
||||
@@ -51,7 +105,13 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||
|
||||
<div className='notification-request__actions'>
|
||||
<IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||
<IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
icons='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -61,4 +121,7 @@ NotificationRequest.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
accountId: PropTypes.string.isRequired,
|
||||
notificationsCount: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
showCheckbox: PropTypes.bool,
|
||||
toggleCheck: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,16 +1,52 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { updateNotificationsPolicy } from 'mastodon/actions/notification_policies';
|
||||
import type { AppDispatch } from 'mastodon/store';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { CheckboxWithLabel } from './checkbox_with_label';
|
||||
import { SelectWithLabel } from './select_with_label';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
const messages = defineMessages({
|
||||
accept: { id: 'notifications.policy.accept', defaultMessage: 'Accept' },
|
||||
accept_hint: {
|
||||
id: 'notifications.policy.accept_hint',
|
||||
defaultMessage: 'Show in notifications',
|
||||
},
|
||||
filter: { id: 'notifications.policy.filter', defaultMessage: 'Filter' },
|
||||
filter_hint: {
|
||||
id: 'notifications.policy.filter_hint',
|
||||
defaultMessage: 'Send to filtered notifications inbox',
|
||||
},
|
||||
drop: { id: 'notifications.policy.drop', defaultMessage: 'Ignore' },
|
||||
drop_hint: {
|
||||
id: 'notifications.policy.drop_hint',
|
||||
defaultMessage: 'Send to the void, never to be seen again',
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: change the following when we change the API
|
||||
const changeFilter = (
|
||||
dispatch: AppDispatch,
|
||||
filterType: string,
|
||||
value: string,
|
||||
) => {
|
||||
if (value === 'drop') {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'IGNORE_NOTIFICATIONS',
|
||||
modalProps: { filterType },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
void dispatch(updateNotificationsPolicy({ [filterType]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
export const PolicyControls: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const notificationPolicy = useAppSelector(
|
||||
@@ -18,56 +54,74 @@ export const PolicyControls: React.FC = () => {
|
||||
);
|
||||
|
||||
const handleFilterNotFollowing = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_not_following: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_not_following', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterNotFollowers = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_not_followers: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_not_followers', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterNewAccounts = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_new_accounts: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_new_accounts', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterPrivateMentions = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_private_mentions: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_private_mentions', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterLimitedAccounts = useCallback(
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_limited_accounts', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
if (!notificationPolicy) return null;
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'accept',
|
||||
text: intl.formatMessage(messages.accept),
|
||||
meta: intl.formatMessage(messages.accept_hint),
|
||||
},
|
||||
{
|
||||
value: 'filter',
|
||||
text: intl.formatMessage(messages.filter),
|
||||
meta: intl.formatMessage(messages.filter_hint),
|
||||
},
|
||||
{
|
||||
value: 'drop',
|
||||
text: intl.formatMessage(messages.drop),
|
||||
meta: intl.formatMessage(messages.drop_hint),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='notifications.policy.title'
|
||||
defaultMessage='Filter out notifications from…'
|
||||
defaultMessage='Manage notifications from…'
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_not_following}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_not_following}
|
||||
onChange={handleFilterNotFollowing}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
@@ -81,11 +135,12 @@ export const PolicyControls: React.FC = () => {
|
||||
defaultMessage='Until you manually approve them'
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_not_followers}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_not_followers}
|
||||
onChange={handleFilterNotFollowers}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
@@ -100,11 +155,12 @@ export const PolicyControls: React.FC = () => {
|
||||
values={{ days: 3 }}
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_new_accounts}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_new_accounts}
|
||||
onChange={handleFilterNewAccounts}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
@@ -119,11 +175,12 @@ export const PolicyControls: React.FC = () => {
|
||||
values={{ days: 30 }}
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_private_mentions}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_private_mentions}
|
||||
onChange={handleFilterPrivateMentions}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
@@ -137,9 +194,13 @@ export const PolicyControls: React.FC = () => {
|
||||
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<CheckboxWithLabel checked disabled onChange={noop}>
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_limited_accounts}
|
||||
onChange={handleFilterLimitedAccounts}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_limited_accounts_title'
|
||||
@@ -152,7 +213,7 @@ export const PolicyControls: React.FC = () => {
|
||||
defaultMessage='Limited by server moderators'
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { Placement, State as PopperState } from '@popperjs/core';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
||||
import type { SelectItem } from 'mastodon/components/dropdown_selector';
|
||||
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
interface DropdownProps {
|
||||
value: string;
|
||||
options: SelectItem[];
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = ({
|
||||
value,
|
||||
options,
|
||||
disabled,
|
||||
onChange,
|
||||
placement: initialPlacement = 'bottom-end',
|
||||
}) => {
|
||||
const activeElementRef = useRef<Element | null>(null);
|
||||
const containerRef = useRef(null);
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
const [placement, setPlacement] = useState<Placement>(initialPlacement);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (
|
||||
isOpen &&
|
||||
activeElementRef.current &&
|
||||
activeElementRef.current instanceof HTMLElement
|
||||
) {
|
||||
activeElementRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
setOpen(!isOpen);
|
||||
}, [isOpen, setOpen]);
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
if (!isOpen) activeElementRef.current = document.activeElement;
|
||||
}, [isOpen]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
if (!isOpen) activeElementRef.current = document.activeElement;
|
||||
break;
|
||||
}
|
||||
},
|
||||
[isOpen],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (
|
||||
isOpen &&
|
||||
activeElementRef.current &&
|
||||
activeElementRef.current instanceof HTMLElement
|
||||
)
|
||||
activeElementRef.current.focus({ preventScroll: true });
|
||||
setOpen(false);
|
||||
}, [isOpen]);
|
||||
|
||||
const handleOverlayEnter = useCallback(
|
||||
(state: Partial<PopperState>) => {
|
||||
if (state.placement) setPlacement(state.placement);
|
||||
},
|
||||
[setPlacement],
|
||||
);
|
||||
|
||||
const valueOption = options.find((item) => item.value === value);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleToggle}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
className={classNames('dropdown-button', { active: isOpen })}
|
||||
>
|
||||
<span className='dropdown-button__label'>{valueOption?.text}</span>
|
||||
<Icon id='down' icon={ArrowDropDownIcon} />
|
||||
</button>
|
||||
|
||||
<Overlay
|
||||
show={isOpen}
|
||||
offset={[5, 5]}
|
||||
placement={placement}
|
||||
flip
|
||||
target={containerRef}
|
||||
popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
|
||||
>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div
|
||||
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
|
||||
>
|
||||
<DropdownSelector
|
||||
items={options}
|
||||
value={value}
|
||||
onClose={handleClose}
|
||||
onChange={onChange}
|
||||
classNamePrefix='privacy-dropdown'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
options: SelectItem[];
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
|
||||
value,
|
||||
options,
|
||||
disabled,
|
||||
children,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<label className='app-form__toggle'>
|
||||
<div className='app-form__toggle__label'>{children}</div>
|
||||
|
||||
<div className='app-form__toggle__toggle'>
|
||||
<div>
|
||||
<Dropdown
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
import { useRef, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
@@ -8,11 +8,15 @@ import { Helmet } from 'react-helmet';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||
import { fetchNotificationRequests, expandNotificationRequests } from 'mastodon/actions/notifications';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { fetchNotificationRequests, expandNotificationRequests, acceptNotificationRequests, dismissNotificationRequests } from 'mastodon/actions/notifications';
|
||||
import { changeSetting } from 'mastodon/actions/settings';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
|
||||
import { NotificationRequest } from './components/notification_request';
|
||||
import { PolicyControls } from './components/policy_controls';
|
||||
@@ -20,7 +24,18 @@ import SettingToggle from './components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
|
||||
maximize: { id: 'notification_requests.maximize', defaultMessage: 'Maximize' }
|
||||
maximize: { id: 'notification_requests.maximize', defaultMessage: 'Maximize' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
acceptAll: { id: 'notification_requests.accept_all', defaultMessage: 'Accept all' },
|
||||
dismissAll: { id: 'notification_requests.dismiss_all', defaultMessage: 'Dismiss all' },
|
||||
acceptMultiple: { id: 'notification_requests.accept_multiple', defaultMessage: '{count, plural, one {Accept # request} other {Accept # requests}}' },
|
||||
dismissMultiple: { id: 'notification_requests.dismiss_multiple', defaultMessage: '{count, plural, one {Dismiss # request} other {Dismiss # requests}}' },
|
||||
confirmAcceptAllTitle: { id: 'notification_requests.confirm_accept_all.title', defaultMessage: 'Accept notification requests?' },
|
||||
confirmAcceptAllMessage: { id: 'notification_requests.confirm_accept_all.message', defaultMessage: 'You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?' },
|
||||
confirmAcceptAllButton: { id: 'notification_requests.confirm_accept_all.button', defaultMessage: 'Accept all' },
|
||||
confirmDismissAllTitle: { id: 'notification_requests.confirm_dismiss_all.title', defaultMessage: 'Dismiss notification requests?' },
|
||||
confirmDismissAllMessage: { id: 'notification_requests.confirm_dismiss_all.message', defaultMessage: "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?" },
|
||||
confirmDismissAllButton: { id: 'notification_requests.confirm_dismiss_all.button', defaultMessage: 'Dismiss all' },
|
||||
});
|
||||
|
||||
const ColumnSettings = () => {
|
||||
@@ -55,6 +70,94 @@ const ColumnSettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionMode, setSelectionMode}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const selectedCount = selectedItems.length;
|
||||
|
||||
const handleAcceptAll = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
title: intl.formatMessage(messages.confirmAcceptAllTitle),
|
||||
message: intl.formatMessage(messages.confirmAcceptAllMessage, { count: selectedItems.length }),
|
||||
confirm: intl.formatMessage(messages.confirmAcceptAllButton),
|
||||
onConfirm: () =>
|
||||
dispatch(acceptNotificationRequests(selectedItems)),
|
||||
},
|
||||
}));
|
||||
}, [dispatch, intl, selectedItems]);
|
||||
|
||||
const handleDismissAll = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
title: intl.formatMessage(messages.confirmDismissAllTitle),
|
||||
message: intl.formatMessage(messages.confirmDismissAllMessage, { count: selectedItems.length }),
|
||||
confirm: intl.formatMessage(messages.confirmDismissAllButton),
|
||||
onConfirm: () =>
|
||||
dispatch(dismissNotificationRequests(selectedItems)),
|
||||
},
|
||||
}));
|
||||
}, [dispatch, intl, selectedItems]);
|
||||
|
||||
const handleToggleSelectionMode = useCallback(() => {
|
||||
setSelectionMode((mode) => !mode);
|
||||
}, [setSelectionMode]);
|
||||
|
||||
const menu = selectedCount === 0 ?
|
||||
[
|
||||
{ text: intl.formatMessage(messages.acceptAll), action: handleAcceptAll },
|
||||
{ text: intl.formatMessage(messages.dismissAll), action: handleDismissAll },
|
||||
] : [
|
||||
{ text: intl.formatMessage(messages.acceptMultiple, { count: selectedCount }), action: handleAcceptAll },
|
||||
{ text: intl.formatMessage(messages.dismissMultiple, { count: selectedCount }), action: handleDismissAll },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className='column-header__select-row'>
|
||||
{selectionMode && (
|
||||
<div className='column-header__select-row__checkbox'>
|
||||
<CheckBox checked={selectAllChecked} indeterminate={selectedCount > 0 && !selectAllChecked} onChange={toggleSelectAll} />
|
||||
</div>
|
||||
)}
|
||||
<div className='column-header__select-row__selection-mode'>
|
||||
<button className='text-btn' tabIndex={0} onClick={handleToggleSelectionMode}>
|
||||
{selectionMode ? (
|
||||
<FormattedMessage id='notification_requests.exit_selection_mode' defaultMessage='Cancel' />
|
||||
) :
|
||||
(
|
||||
<FormattedMessage id='notification_requests.enter_selection_mode' defaultMessage='Select' />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{selectedCount > 0 &&
|
||||
<div className='column-header__select-row__selected-count'>
|
||||
{selectedCount} selected
|
||||
</div>
|
||||
}
|
||||
<div className='column-header__select-row__actions'>
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
icons='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SelectRow.propTypes = {
|
||||
selectAllChecked: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
selectionMode: PropTypes.bool,
|
||||
setSelectionMode: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const NotificationRequests = ({ multiColumn }) => {
|
||||
const columnRef = useRef();
|
||||
const intl = useIntl();
|
||||
@@ -63,10 +166,40 @@ export const NotificationRequests = ({ multiColumn }) => {
|
||||
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
|
||||
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
|
||||
|
||||
const [selectionMode, setSelectionMode] = useState(false);
|
||||
const [checkedRequestIds, setCheckedRequestIds] = useState([]);
|
||||
const [selectAllChecked, setSelectAllChecked] = useState(false);
|
||||
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
columnRef.current?.scrollTop();
|
||||
}, [columnRef]);
|
||||
|
||||
const handleCheck = useCallback(id => {
|
||||
setCheckedRequestIds(ids => {
|
||||
const position = ids.indexOf(id);
|
||||
|
||||
if(position > -1)
|
||||
ids.splice(position, 1);
|
||||
else
|
||||
ids.push(id);
|
||||
|
||||
setSelectAllChecked(ids.length === notificationRequests.size);
|
||||
|
||||
return [...ids];
|
||||
});
|
||||
}, [setCheckedRequestIds, notificationRequests]);
|
||||
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
setSelectAllChecked(checked => {
|
||||
if(checked)
|
||||
setCheckedRequestIds([]);
|
||||
else
|
||||
setCheckedRequestIds(notificationRequests.map(request => request.get('id')).toArray());
|
||||
|
||||
return !checked;
|
||||
});
|
||||
}, [notificationRequests]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
dispatch(expandNotificationRequests());
|
||||
}, [dispatch]);
|
||||
@@ -84,6 +217,8 @@ export const NotificationRequests = ({ multiColumn }) => {
|
||||
onClick={handleHeaderClick}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
appendContent={
|
||||
<SelectRow selectionMode={selectionMode} setSelectionMode={setSelectionMode} selectAllChecked={selectAllChecked} toggleSelectAll={toggleSelectAll} selectedItems={checkedRequestIds} />}
|
||||
>
|
||||
<ColumnSettings />
|
||||
</ColumnHeader>
|
||||
@@ -104,6 +239,9 @@ export const NotificationRequests = ({ multiColumn }) => {
|
||||
id={request.get('id')}
|
||||
accountId={request.get('account')}
|
||||
notificationsCount={request.get('notifications_count')}
|
||||
showCheckbox={selectionMode}
|
||||
checked={checkedRequestIds.includes(request.get('id'))}
|
||||
toggleCheck={handleCheck}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
@@ -2,26 +2,33 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
|
||||
import type { StatusVisibility } from 'mastodon/api_types/statuses';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import type { NotificationGroupMention } from 'mastodon/models/notification_group';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import type { LabelRenderer } from './notification_group_with_status';
|
||||
import { NotificationWithStatus } from './notification_with_status';
|
||||
|
||||
const labelRenderer: LabelRenderer = (values) => (
|
||||
const mentionLabelRenderer: LabelRenderer = () => (
|
||||
<FormattedMessage id='notification.label.mention' defaultMessage='Mention' />
|
||||
);
|
||||
|
||||
const privateMentionLabelRenderer: LabelRenderer = () => (
|
||||
<FormattedMessage
|
||||
id='notification.mention'
|
||||
defaultMessage='{name} mentioned you'
|
||||
values={values}
|
||||
id='notification.label.private_mention'
|
||||
defaultMessage='Private mention'
|
||||
/>
|
||||
);
|
||||
|
||||
const privateMentionLabelRenderer: LabelRenderer = (values) => (
|
||||
const replyLabelRenderer: LabelRenderer = () => (
|
||||
<FormattedMessage id='notification.label.reply' defaultMessage='Reply' />
|
||||
);
|
||||
|
||||
const privateReplyLabelRenderer: LabelRenderer = () => (
|
||||
<FormattedMessage
|
||||
id='notification.private_mention'
|
||||
defaultMessage='{name} privately mentioned you'
|
||||
values={values}
|
||||
id='notification.label.private_reply'
|
||||
defaultMessage='Private reply'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -29,27 +36,30 @@ export const NotificationMention: React.FC<{
|
||||
notification: NotificationGroupMention;
|
||||
unread: boolean;
|
||||
}> = ({ notification, unread }) => {
|
||||
const statusVisibility = useAppSelector(
|
||||
(state) =>
|
||||
state.statuses.getIn([
|
||||
notification.statusId,
|
||||
'visibility',
|
||||
]) as StatusVisibility,
|
||||
);
|
||||
const [isDirect, isReply] = useAppSelector((state) => {
|
||||
const status = state.statuses.get(notification.statusId) as Status;
|
||||
|
||||
return [
|
||||
status.get('visibility') === 'direct',
|
||||
status.get('in_reply_to_account_id') === me,
|
||||
] as const;
|
||||
});
|
||||
|
||||
let labelRenderer = mentionLabelRenderer;
|
||||
|
||||
if (isReply && isDirect) labelRenderer = privateReplyLabelRenderer;
|
||||
else if (isReply) labelRenderer = replyLabelRenderer;
|
||||
else if (isDirect) labelRenderer = privateMentionLabelRenderer;
|
||||
|
||||
return (
|
||||
<NotificationWithStatus
|
||||
type='mention'
|
||||
icon={statusVisibility === 'direct' ? AlternateEmailIcon : ReplyIcon}
|
||||
icon={isReply ? ReplyIcon : AlternateEmailIcon}
|
||||
iconId='reply'
|
||||
accountIds={notification.sampleAccountIds}
|
||||
count={notification.notifications_count}
|
||||
statusId={notification.statusId}
|
||||
labelRenderer={
|
||||
statusVisibility === 'direct'
|
||||
? privateMentionLabelRenderer
|
||||
: labelRenderer
|
||||
}
|
||||
labelRenderer={labelRenderer}
|
||||
unread={unread}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
|
||||
@@ -27,16 +25,13 @@ import {
|
||||
selectUnreadNotificationGroupsCount,
|
||||
selectPendingNotificationGroupsCount,
|
||||
selectAnyPendingNotification,
|
||||
selectNotificationGroups,
|
||||
} from 'mastodon/selectors/notifications';
|
||||
import {
|
||||
selectNeedsNotificationPermission,
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsQuickFilterShow,
|
||||
selectSettingsNotificationsShowUnread,
|
||||
} from 'mastodon/selectors/settings';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
import type { RootState } from 'mastodon/store';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { submitMarkers } from '../../actions/markers';
|
||||
@@ -62,34 +57,12 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const getNotifications = createSelector(
|
||||
[
|
||||
selectSettingsNotificationsQuickFilterShow,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
(state: RootState) => state.notificationGroups.groups,
|
||||
],
|
||||
(showFilterBar, allowedType, excludedTypes, notifications) => {
|
||||
if (!showFilterBar || allowedType === 'all') {
|
||||
// used if user changed the notification settings after loading the notifications from the server
|
||||
// otherwise a list of notifications will come pre-filtered from the backend
|
||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||
return notifications.filter(
|
||||
(item) => item.type === 'gap' || !excludedTypes.includes(item.type),
|
||||
);
|
||||
}
|
||||
return notifications.filter(
|
||||
(item) => item.type === 'gap' || allowedType === item.type,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const Notifications: React.FC<{
|
||||
columnId?: string;
|
||||
multiColumn?: boolean;
|
||||
}> = ({ columnId, multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const notifications = useAppSelector(getNotifications);
|
||||
const notifications = useAppSelector(selectNotificationGroups);
|
||||
const dispatch = useAppDispatch();
|
||||
const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
|
||||
const hasMore = notifications.at(-1)?.type === 'gap';
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||
import PersonAlertIcon from '@/material-icons/400-24px/person_alert.svg?react';
|
||||
import ShieldQuestionIcon from '@/material-icons/400-24px/shield_question.svg?react';
|
||||
import { closeModal } from 'mastodon/actions/modal';
|
||||
import { updateNotificationsPolicy } from 'mastodon/actions/notification_policies';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
export const IgnoreNotificationsModal = ({ filterType }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
|
||||
void dispatch(updateNotificationsPolicy({ [filterType]: 'drop' }));
|
||||
}, [dispatch, filterType]);
|
||||
|
||||
const handleSecondaryClick = useCallback(() => {
|
||||
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
|
||||
void dispatch(updateNotificationsPolicy({ [filterType]: 'filter' }));
|
||||
}, [dispatch, filterType]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
|
||||
}, [dispatch]);
|
||||
|
||||
let title = null;
|
||||
|
||||
switch(filterType) {
|
||||
case 'for_not_following':
|
||||
title = <FormattedMessage id='ignore_notifications_modal.not_following_title' defaultMessage="Ignore notifications from people you don't follow?" />;
|
||||
break;
|
||||
case 'for_not_followers':
|
||||
title = <FormattedMessage id='ignore_notifications_modal.not_followers_title' defaultMessage='Ignore notifications from people not following you?' />;
|
||||
break;
|
||||
case 'for_new_accounts':
|
||||
title = <FormattedMessage id='ignore_notifications_modal.new_accounts_title' defaultMessage='Ignore notifications from new accounts?' />;
|
||||
break;
|
||||
case 'for_private_mentions':
|
||||
title = <FormattedMessage id='ignore_notifications_modal.private_mentions_title' defaultMessage='Ignore notifications from unsolicited Private Mentions?' />;
|
||||
break;
|
||||
case 'for_limited_accounts':
|
||||
title = <FormattedMessage id='ignore_notifications_modal.limited_accounts_title' defaultMessage='Ignore notifications from moderated accounts?' />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal safety-action-modal'>
|
||||
<div className='safety-action-modal__top'>
|
||||
<div className='safety-action-modal__header'>
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
|
||||
<div className='safety-action-modal__bullet-points'>
|
||||
<div>
|
||||
<div className='safety-action-modal__bullet-points__icon'><Icon icon={InventoryIcon} /></div>
|
||||
<div><FormattedMessage id='ignore_notifications_modal.filter_to_review_separately' defaultMessage='You can review filtered notifications speparately' /></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='safety-action-modal__bullet-points__icon'><Icon icon={PersonAlertIcon} /></div>
|
||||
<div><FormattedMessage id='ignore_notifications_modal.filter_to_act_users' defaultMessage="You'll still be able to accept, reject, or report users" /></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ShieldQuestionIcon} /></div>
|
||||
<div><FormattedMessage id='ignore_notifications_modal.filter_to_avoid_confusion' defaultMessage='Filtering helps avoid potential confusion' /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormattedMessage id='ignore_notifications_modal.disclaimer' defaultMessage="Mastodon cannot inform users that you've ignored their notifications. Ignoring notifications will not stop the messages themselves from being sent." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='safety-action-modal__bottom'>
|
||||
<div className='safety-action-modal__actions'>
|
||||
<Button onClick={handleSecondaryClick} secondary>
|
||||
<FormattedMessage id='ignore_notifications_modal.filter_instead' defaultMessage='Filter instead' />
|
||||
</Button>
|
||||
|
||||
<div className='spacer' />
|
||||
|
||||
<button onClick={handleCancel} className='link-button'>
|
||||
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||
</button>
|
||||
|
||||
<button onClick={handleClick} className='link-button'>
|
||||
<FormattedMessage id='ignore_notifications_modal.ignore' defaultMessage='Ignore notifications' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
IgnoreNotificationsModal.propTypes = {
|
||||
filterType: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default IgnoreNotificationsModal;
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
InteractionModal,
|
||||
SubscribedLanguagesModal,
|
||||
ClosedRegistrationsModal,
|
||||
IgnoreNotificationsModal,
|
||||
} from 'mastodon/features/ui/util/async-components';
|
||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||
|
||||
@@ -70,6 +71,7 @@ export const MODAL_COMPONENTS = {
|
||||
'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,
|
||||
'INTERACTION': InteractionModal,
|
||||
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
||||
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
||||
};
|
||||
|
||||
export default class ModalRoot extends PureComponent {
|
||||
|
||||
@@ -134,6 +134,10 @@ export function ReportModal () {
|
||||
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
||||
}
|
||||
|
||||
export function IgnoreNotificationsModal () {
|
||||
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/ignore_notifications_modal');
|
||||
}
|
||||
|
||||
export function MediaGallery () {
|
||||
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
|
||||
}
|
||||
|
||||
@@ -356,6 +356,17 @@
|
||||
"home.pending_critical_update.link": "See updates",
|
||||
"home.pending_critical_update.title": "Critical security update available!",
|
||||
"home.show_announcements": "Show announcements",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon cannot inform users that you've ignored their notifications. Ignoring notifications will not stop the messages themselves from being sent.",
|
||||
"ignore_notifications_modal.filter_instead": "Filter instead",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Filtering helps avoid potential confusion",
|
||||
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtering helps avoid potential confusion",
|
||||
"ignore_notifications_modal.filter_to_review_separately": "You can review filtered notifications speparately",
|
||||
"ignore_notifications_modal.ignore": "Ignore notifications",
|
||||
"ignore_notifications_modal.limited_accounts_title": "Ignore notifications from moderated accounts?",
|
||||
"ignore_notifications_modal.new_accounts_title": "Ignore notifications from new accounts?",
|
||||
"ignore_notifications_modal.not_followers_title": "Ignore notifications from people not following you?",
|
||||
"ignore_notifications_modal.not_following_title": "Ignore notifications from people you don't follow?",
|
||||
"ignore_notifications_modal.private_mentions_title": "Ignore notifications from unsolicited Private Mentions?",
|
||||
"interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
|
||||
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
|
||||
"interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.",
|
||||
@@ -482,7 +493,11 @@
|
||||
"notification.favourite": "{name} favorited your post",
|
||||
"notification.follow": "{name} followed you",
|
||||
"notification.follow_request": "{name} has requested to follow you",
|
||||
"notification.mention": "{name} mentioned you",
|
||||
"notification.label.mention": "Mention",
|
||||
"notification.label.private_mention": "Private mention",
|
||||
"notification.label.private_reply": "Private reply",
|
||||
"notification.label.reply": "Reply",
|
||||
"notification.mention": "Mention",
|
||||
"notification.moderation-warning.learn_more": "Learn more",
|
||||
"notification.moderation_warning": "You have received a moderation warning",
|
||||
"notification.moderation_warning.action_delete_statuses": "Some of your posts have been removed.",
|
||||
@@ -494,7 +509,6 @@
|
||||
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
|
||||
"notification.own_poll": "Your poll has ended",
|
||||
"notification.poll": "A poll you voted in has ended",
|
||||
"notification.private_mention": "{name} privately mentioned you",
|
||||
"notification.reblog": "{name} boosted your post",
|
||||
"notification.relationships_severance_event": "Lost connections with {name}",
|
||||
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
|
||||
@@ -504,13 +518,26 @@
|
||||
"notification.status": "{name} just posted",
|
||||
"notification.update": "{name} edited a post",
|
||||
"notification_requests.accept": "Accept",
|
||||
"notification_requests.accept_all": "Accept all",
|
||||
"notification_requests.accept_multiple": "{count, plural, one {Accept # request} other {Accept # requests}}",
|
||||
"notification_requests.confirm_accept_all.button": "Accept all",
|
||||
"notification_requests.confirm_accept_all.message": "You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?",
|
||||
"notification_requests.confirm_accept_all.title": "Accept notification requests?",
|
||||
"notification_requests.confirm_dismiss_all.button": "Dismiss all",
|
||||
"notification_requests.confirm_dismiss_all.message": "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?",
|
||||
"notification_requests.confirm_dismiss_all.title": "Dismiss notification requests?",
|
||||
"notification_requests.dismiss": "Dismiss",
|
||||
"notification_requests.dismiss_all": "Dismiss all",
|
||||
"notification_requests.dismiss_multiple": "{count, plural, one {Dismiss # request} other {Dismiss # requests}}",
|
||||
"notification_requests.enter_selection_mode": "Select",
|
||||
"notification_requests.exit_selection_mode": "Cancel",
|
||||
"notification_requests.explainer_for_limited_account": "Notifications from this account have been filtered because the account has been limited by a moderator.",
|
||||
"notification_requests.explainer_for_limited_remote_account": "Notifications from this account have been filtered because the account or its server has been limited by a moderator.",
|
||||
"notification_requests.maximize": "Maximize",
|
||||
"notification_requests.minimize_banner": "Minimize filtered notifications banner",
|
||||
"notification_requests.notifications_from": "Notifications from {name}",
|
||||
"notification_requests.title": "Filtered notifications",
|
||||
"notification_requests.view": "View notifications",
|
||||
"notifications.clear": "Clear notifications",
|
||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||
"notifications.clear_title": "Clear notifications?",
|
||||
@@ -547,6 +574,12 @@
|
||||
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
|
||||
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
|
||||
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
|
||||
"notifications.policy.accept": "Accept",
|
||||
"notifications.policy.accept_hint": "Show in notifications",
|
||||
"notifications.policy.drop": "Ignore",
|
||||
"notifications.policy.drop_hint": "Send to the void, never to be seen again",
|
||||
"notifications.policy.filter": "Filter",
|
||||
"notifications.policy.filter_hint": "Send to filtered notifications inbox",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Limited by server moderators",
|
||||
"notifications.policy.filter_limited_accounts_title": "Moderated accounts",
|
||||
"notifications.policy.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}",
|
||||
@@ -557,7 +590,7 @@
|
||||
"notifications.policy.filter_not_following_title": "People you don't follow",
|
||||
"notifications.policy.filter_private_mentions_hint": "Filtered unless it's in reply to your own mention or if you follow the sender",
|
||||
"notifications.policy.filter_private_mentions_title": "Unsolicited private mentions",
|
||||
"notifications.policy.title": "Filter out notifications from…",
|
||||
"notifications.policy.title": "Manage notifications from…",
|
||||
"notifications_permission_banner.enable": "Enable desktop notifications",
|
||||
"notifications_permission_banner.how_to_control": "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.",
|
||||
"notifications_permission_banner.title": "Never miss a thing",
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
NOTIFICATION_REQUEST_FETCH_FAIL,
|
||||
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
||||
NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
||||
NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
|
||||
NOTIFICATION_REQUESTS_DISMISS_REQUEST,
|
||||
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
||||
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
||||
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
||||
@@ -83,6 +85,9 @@ export const notificationRequestsReducer = (state = initialState, action) => {
|
||||
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
|
||||
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
|
||||
return removeRequest(state, action.id);
|
||||
case NOTIFICATION_REQUESTS_ACCEPT_REQUEST:
|
||||
case NOTIFICATION_REQUESTS_DISMISS_REQUEST:
|
||||
return action.ids.reduce((state, id) => removeRequest(state, id), state);
|
||||
case blockAccountSuccess.type:
|
||||
return removeRequestByAccount(state, action.payload.relationship.id);
|
||||
case muteAccountSuccess.type:
|
||||
|
||||
@@ -1,15 +1,62 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
import { compareId } from 'mastodon/compare_id';
|
||||
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
||||
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
|
||||
import type { RootState } from 'mastodon/store';
|
||||
|
||||
import {
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsQuickFilterShow,
|
||||
} from './settings';
|
||||
|
||||
const filterNotificationsByAllowedTypes = (
|
||||
showFilterBar: boolean,
|
||||
allowedType: string,
|
||||
excludedTypes: string[],
|
||||
notifications: (NotificationGroup | NotificationGap)[],
|
||||
) => {
|
||||
if (!showFilterBar || allowedType === 'all') {
|
||||
// used if user changed the notification settings after loading the notifications from the server
|
||||
// otherwise a list of notifications will come pre-filtered from the backend
|
||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||
return notifications.filter(
|
||||
(item) => item.type === 'gap' || !excludedTypes.includes(item.type),
|
||||
);
|
||||
}
|
||||
return notifications.filter(
|
||||
(item) => item.type === 'gap' || allowedType === item.type,
|
||||
);
|
||||
};
|
||||
|
||||
export const selectNotificationGroups = createSelector(
|
||||
[
|
||||
selectSettingsNotificationsQuickFilterShow,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
(state: RootState) => state.notificationGroups.groups,
|
||||
],
|
||||
filterNotificationsByAllowedTypes,
|
||||
);
|
||||
|
||||
const selectPendingNotificationGroups = createSelector(
|
||||
[
|
||||
selectSettingsNotificationsQuickFilterShow,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
(state: RootState) => state.notificationGroups.pendingGroups,
|
||||
],
|
||||
filterNotificationsByAllowedTypes,
|
||||
);
|
||||
|
||||
export const selectUnreadNotificationGroupsCount = createSelector(
|
||||
[
|
||||
(s: RootState) => s.notificationGroups.lastReadId,
|
||||
(s: RootState) => s.notificationGroups.pendingGroups,
|
||||
(s: RootState) => s.notificationGroups.groups,
|
||||
selectNotificationGroups,
|
||||
selectPendingNotificationGroups,
|
||||
],
|
||||
(notificationMarker, pendingGroups, groups) => {
|
||||
(notificationMarker, groups, pendingGroups) => {
|
||||
return (
|
||||
groups.filter(
|
||||
(group) =>
|
||||
@@ -31,7 +78,7 @@ export const selectUnreadNotificationGroupsCount = createSelector(
|
||||
export const selectAnyPendingNotification = createSelector(
|
||||
[
|
||||
(s: RootState) => s.notificationGroups.readMarkerId,
|
||||
(s: RootState) => s.notificationGroups.groups,
|
||||
selectNotificationGroups,
|
||||
],
|
||||
(notificationMarker, groups) => {
|
||||
return groups.some(
|
||||
@@ -44,7 +91,7 @@ export const selectAnyPendingNotification = createSelector(
|
||||
);
|
||||
|
||||
export const selectPendingNotificationGroupsCount = createSelector(
|
||||
[(s: RootState) => s.notificationGroups.pendingGroups],
|
||||
[selectPendingNotificationGroups],
|
||||
(pendingGroups) =>
|
||||
pendingGroups.filter((group) => group.type !== 'gap').length,
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M240-440v-80h480v80H240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 130 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M240-440v-80h480v80H240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 130 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-520q-17 0-28.5-11.5T760-560q0-17 11.5-28.5T800-600q17 0 28.5 11.5T840-560q0 17-11.5 28.5T800-520Zm-40-120v-200h80v200h-80ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Z"/></svg>
|
||||
|
After Width: | Height: | Size: 433 B |
1
app/javascript/material-icons/400-24px/person_alert.svg
Normal file
1
app/javascript/material-icons/400-24px/person_alert.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-520q-17 0-28.5-11.5T760-560q0-17 11.5-28.5T800-600q17 0 28.5 11.5T840-560q0 17-11.5 28.5T800-520Zm-40-120v-200h80v200h-80ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm80-80h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0-80Zm0 400Z"/></svg>
|
||||
|
After Width: | Height: | Size: 654 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-200q17 0 29.5-12.5T522-322q0-17-12.5-29.5T480-364q-17 0-29.5 12.5T438-322q0 17 12.5 29.5T480-280Zm-29-128h60v-22q0-11 5-21 6-14 16-23.5t21-19.5q17-17 29.5-38t12.5-46q0-45-34.5-73.5T480-680q-40 0-71.5 23T366-596l54 22q6-20 22.5-34t37.5-14q22 0 38.5 13t16.5 33q0 17-10.5 31.5T501-518q-12 11-24 22.5T458-469q-7 14-7 29.5v31.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Zm0 200q17 0 29.5-12.5T522-322q0-17-12.5-29.5T480-364q-17 0-29.5 12.5T438-322q0 17 12.5 29.5T480-280Zm-29-128h60v-22q0-11 5-21 6-14 16-23.5t21-19.5q17-17 29.5-38t12.5-46q0-45-34.5-73.5T480-680q-40 0-71.5 23T366-596l54 22q6-20 22.5-34t37.5-14q22 0 38.5 13t16.5 33q0 17-10.5 31.5T501-518q-12 11-24 22.5T458-469q-7 14-7 29.5v31.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 597 B |
@@ -214,12 +214,6 @@ html {
|
||||
border-top-color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.column-header__collapsible-inner {
|
||||
background: darken($ui-base-color, 4%);
|
||||
border: 1px solid var(--background-border-color);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.column-settings__hashtags .column-select__option {
|
||||
color: $white;
|
||||
}
|
||||
@@ -557,3 +551,11 @@ a.sparkline {
|
||||
background: darken($ui-base-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-text {
|
||||
background: darken($ui-base-color, 10%);
|
||||
}
|
||||
|
||||
.report-dialog-modal__textarea {
|
||||
background: darken($ui-base-color, 10%);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ $valid-value-color: $success-green !default;
|
||||
|
||||
$ui-base-color: $classic-secondary-color !default;
|
||||
$ui-base-lighter-color: #b0c0cf;
|
||||
$ui-primary-color: #9bcbed;
|
||||
$ui-primary-color: $classic-primary-color !default;
|
||||
$ui-secondary-color: $classic-base-color !default;
|
||||
$ui-highlight-color: $classic-highlight-color !default;
|
||||
|
||||
|
||||
@@ -877,6 +877,13 @@ body > [data-popper-placement] {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
color: $highlight-text-color;
|
||||
border-color: $highlight-text-color;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
@@ -2779,6 +2786,11 @@ $ui-header-logo-wordmark-width: 99px;
|
||||
&.privacy-policy {
|
||||
border-top: 1px solid var(--background-border-color);
|
||||
border-radius: 4px;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3876,18 +3888,17 @@ $ui-header-logo-wordmark-width: 99px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
color: $inverted-text-color;
|
||||
background: $white;
|
||||
color: $primary-text-color;
|
||||
background: $ui-base-color;
|
||||
padding: 7px 10px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $white;
|
||||
border: 1px solid var(--background-border-color);
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: lighten($ui-highlight-color, 12%);
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
@@ -4309,6 +4320,36 @@ a.status-card {
|
||||
}
|
||||
}
|
||||
|
||||
.column-header__select-row {
|
||||
border-width: 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--background-border-color);
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&__checkbox .check-box {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__selection-mode {
|
||||
flex-grow: 1;
|
||||
|
||||
.text-btn:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
.icon-button {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-border-color);
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
@@ -4472,6 +4513,11 @@ a.status-card {
|
||||
.column-header__collapsible-inner {
|
||||
border: 1px solid var(--background-border-color);
|
||||
border-top: 0;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.column-header__setting-btn {
|
||||
@@ -6235,9 +6281,10 @@ a.status-card {
|
||||
max-width: 90vw;
|
||||
width: 480px;
|
||||
height: 80vh;
|
||||
background: lighten($ui-secondary-color, 8%);
|
||||
color: $inverted-text-color;
|
||||
border-radius: 8px;
|
||||
background: var(--background-color);
|
||||
color: $primary-text-color;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-border-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
@@ -6245,7 +6292,7 @@ a.status-card {
|
||||
|
||||
&__container {
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid $ui-secondary-color;
|
||||
border-top: 1px solid var(--background-border-color);
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
@@ -6275,7 +6322,7 @@ a.status-card {
|
||||
&__lead {
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
color: lighten($inverted-text-color, 16%);
|
||||
color: $secondary-text-color;
|
||||
margin-bottom: 30px;
|
||||
|
||||
a {
|
||||
@@ -6310,7 +6357,7 @@ a.status-card {
|
||||
|
||||
.status__content,
|
||||
.status__content p {
|
||||
color: $inverted-text-color;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
.status__content__spoiler-link {
|
||||
@@ -6355,7 +6402,7 @@ a.status-card {
|
||||
.poll__option.dialog-option {
|
||||
padding: 15px 0;
|
||||
flex: 0 0 auto;
|
||||
border-bottom: 1px solid $ui-secondary-color;
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
@@ -6363,13 +6410,13 @@ a.status-card {
|
||||
|
||||
& > .poll__option__text {
|
||||
font-size: 13px;
|
||||
color: lighten($inverted-text-color, 16%);
|
||||
color: $secondary-text-color;
|
||||
|
||||
strong {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: $inverted-text-color;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
|
||||
@@ -6388,22 +6435,19 @@ a.status-card {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: $inverted-text-color;
|
||||
background: $simple-background-color;
|
||||
color: $primary-text-color;
|
||||
background: $ui-base-color;
|
||||
padding: 10px;
|
||||
font-family: inherit;
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
resize: vertical;
|
||||
border: 0;
|
||||
border: 1px solid var(--background-border-color);
|
||||
outline: 0;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
|
||||
&::placeholder {
|
||||
color: $dark-text-color;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
@@ -6424,16 +6468,16 @@ a.status-card {
|
||||
}
|
||||
|
||||
.button.button-secondary {
|
||||
border-color: $inverted-text-color;
|
||||
color: $inverted-text-color;
|
||||
border-color: $ui-button-destructive-background-color;
|
||||
color: $ui-button-destructive-background-color;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: transparent;
|
||||
border-color: $ui-button-background-color;
|
||||
color: $ui-button-background-color;
|
||||
background: $ui-button-destructive-background-color;
|
||||
border-color: $ui-button-destructive-background-color;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7453,20 +7497,9 @@ a.status-card {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
|
||||
&.checked {
|
||||
&.checked,
|
||||
&.indeterminate {
|
||||
border-color: $ui-highlight-color;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: $ui-highlight-color;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -7476,19 +7509,28 @@ a.status-card {
|
||||
}
|
||||
}
|
||||
|
||||
.radio-button.checked::before {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: $ui-highlight-color;
|
||||
}
|
||||
|
||||
.check-box {
|
||||
&__input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 2px;
|
||||
|
||||
&.checked {
|
||||
&.checked,
|
||||
&.indeterminate {
|
||||
background: $ui-highlight-color;
|
||||
color: $white;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7657,6 +7699,11 @@ noscript {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__backdrop {
|
||||
@@ -10204,12 +10251,28 @@ noscript {
|
||||
}
|
||||
|
||||
.notification-request {
|
||||
$padding: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 15px;
|
||||
padding: $padding;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
|
||||
&__checkbox {
|
||||
position: absolute;
|
||||
inset-inline-start: $padding;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
|
||||
.check-box {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -10267,6 +10330,31 @@ noscript {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-request__link {
|
||||
transition: padding-inline-start 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
&--forced-checkbox {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: lighten($ui-base-color, 1%);
|
||||
}
|
||||
|
||||
.notification-request__checkbox {
|
||||
opacity: 1;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.notification-request__link {
|
||||
padding-inline-start: 30px;
|
||||
}
|
||||
|
||||
.notification-request__actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.more-from-author {
|
||||
|
||||
@@ -83,11 +83,6 @@
|
||||
max-height: 35vh;
|
||||
padding: 0 6px 6px;
|
||||
will-change: transform;
|
||||
|
||||
&::-webkit-scrollbar-track:hover,
|
||||
&::-webkit-scrollbar-track:active {
|
||||
background-color: rgba($base-overlay-background, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-mart-search {
|
||||
@@ -116,7 +111,6 @@
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border-width: 1px !important;
|
||||
border-color: $ui-button-background-color;
|
||||
}
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
|
||||
@@ -56,40 +56,3 @@ table {
|
||||
html {
|
||||
scrollbar-color: lighten($ui-base-color, 4%) rgba($base-overlay-background, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border: 0px none $base-border-color;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: lighten($ui-base-color, 6%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
border: 0px none $base-border-color;
|
||||
border-radius: 0;
|
||||
background: rgba($base-overlay-background, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track:hover {
|
||||
background: $ui-base-color;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track:active {
|
||||
background: $ui-base-color;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user