Add notification policies and notification requests in web UI (#29433)
This commit is contained in:
parent
19efa1b9f1
commit
c10bbf5fe3
35 changed files with 1278 additions and 252 deletions
|
@ -18,6 +18,10 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
|||
render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @request, serializer: REST::NotificationRequestSerializer
|
||||
end
|
||||
|
||||
def accept
|
||||
AcceptNotificationRequestService.new.call(@request)
|
||||
render_empty
|
||||
|
@ -31,7 +35,7 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_requests
|
||||
requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed)).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
|
||||
requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
|
|
|
@ -44,6 +44,38 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
|||
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
|
||||
|
||||
export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
|
||||
export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
|
||||
export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
|
||||
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
|
||||
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
|
||||
export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
|
||||
export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
|
||||
export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
|
||||
export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
|
||||
export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
|
||||
export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
|
||||
|
||||
export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
|
||||
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
|
||||
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_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';
|
||||
|
||||
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
|
||||
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
|
||||
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
|
||||
|
||||
defineMessages({
|
||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||
|
@ -313,3 +345,264 @@ export function setBrowserPermission (value) {
|
|||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchNotificationPolicy = () => (dispatch, getState) => {
|
||||
dispatch(fetchNotificationPolicyRequest());
|
||||
|
||||
api(getState).get('/api/v1/notifications/policy').then(({ data }) => {
|
||||
dispatch(fetchNotificationPolicySuccess(data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationPolicyFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationPolicyRequest = () => ({
|
||||
type: NOTIFICATION_POLICY_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchNotificationPolicySuccess = policy => ({
|
||||
type: NOTIFICATION_POLICY_FETCH_SUCCESS,
|
||||
policy,
|
||||
});
|
||||
|
||||
export const fetchNotificationPolicyFail = error => ({
|
||||
type: NOTIFICATION_POLICY_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateNotificationsPolicy = params => (dispatch, getState) => {
|
||||
dispatch(fetchNotificationPolicyRequest());
|
||||
|
||||
api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => {
|
||||
dispatch(fetchNotificationPolicySuccess(data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationPolicyFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationRequests = () => (dispatch, getState) => {
|
||||
const params = {};
|
||||
|
||||
if (getState().getIn(['notificationRequests', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
|
||||
params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
|
||||
}
|
||||
|
||||
dispatch(fetchNotificationRequestsRequest());
|
||||
|
||||
api(getState).get('/api/v1/notifications/requests', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationRequestsFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationRequestsRequest = () => ({
|
||||
type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchNotificationRequestsSuccess = (requests, next) => ({
|
||||
type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
|
||||
requests,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchNotificationRequestsFail = error => ({
|
||||
type: NOTIFICATION_REQUESTS_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const expandNotificationRequests = () => (dispatch, getState) => {
|
||||
const url = getState().getIn(['notificationRequests', 'next']);
|
||||
|
||||
if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandNotificationRequestsRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
|
||||
}).catch(err => {
|
||||
dispatch(expandNotificationRequestsFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const expandNotificationRequestsRequest = () => ({
|
||||
type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
export const expandNotificationRequestsSuccess = (requests, next) => ({
|
||||
type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
|
||||
requests,
|
||||
next,
|
||||
});
|
||||
|
||||
export const expandNotificationRequestsFail = error => ({
|
||||
type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchNotificationRequest = id => (dispatch, getState) => {
|
||||
const current = getState().getIn(['notificationRequests', 'current']);
|
||||
|
||||
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchNotificationRequestRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
|
||||
dispatch(fetchNotificationRequestSuccess(data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationRequestFail(id, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationRequestRequest = id => ({
|
||||
type: NOTIFICATION_REQUEST_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchNotificationRequestSuccess = request => ({
|
||||
type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
|
||||
request,
|
||||
});
|
||||
|
||||
export const fetchNotificationRequestFail = (id, error) => ({
|
||||
type: NOTIFICATION_REQUEST_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequest = id => (dispatch, getState) => {
|
||||
dispatch(acceptNotificationRequestRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
||||
dispatch(acceptNotificationRequestSuccess(id));
|
||||
}).catch(err => {
|
||||
dispatch(acceptNotificationRequestFail(id, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const acceptNotificationRequestRequest = id => ({
|
||||
type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequestSuccess = id => ({
|
||||
type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
|
||||
id,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequestFail = (id, error) => ({
|
||||
type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequest = id => (dispatch, getState) => {
|
||||
dispatch(dismissNotificationRequestRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
||||
dispatch(dismissNotificationRequestSuccess(id));
|
||||
}).catch(err => {
|
||||
dispatch(dismissNotificationRequestFail(id, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const dismissNotificationRequestRequest = id => ({
|
||||
type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequestSuccess = id => ({
|
||||
type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
|
||||
id,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequestFail = (id, error) => ({
|
||||
type: NOTIFICATION_REQUEST_DISMISS_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
|
||||
const current = getState().getIn(['notificationRequests', 'current']);
|
||||
const params = { account_id: accountId };
|
||||
|
||||
if (current.getIn(['item', 'account']) === accountId) {
|
||||
if (current.getIn(['notifications', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.getIn(['notifications', 'items'])?.size > 0) {
|
||||
params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(fetchNotificationsForRequestRequest());
|
||||
|
||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationsForRequestFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationsForRequestRequest = () => ({
|
||||
type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchNotificationsForRequestSuccess = (notifications, next) => ({
|
||||
type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
||||
notifications,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchNotificationsForRequestFail = (error) => ({
|
||||
type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const expandNotificationsForRequest = () => (dispatch, getState) => {
|
||||
const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']);
|
||||
|
||||
if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandNotificationsForRequestRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
|
||||
}).catch(err => {
|
||||
dispatch(expandNotificationsForRequestFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const expandNotificationsForRequestRequest = () => ({
|
||||
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
export const expandNotificationsForRequestSuccess = (notifications, next) => ({
|
||||
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
|
||||
notifications,
|
||||
next,
|
||||
});
|
||||
|
||||
export const expandNotificationsForRequestFail = (error) => ({
|
||||
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
@ -11,7 +11,7 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
|||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
|
||||
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { ButtonInTabsBar, useColumnsContext } from 'mastodon/features/ui/util/columns_context';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
@ -23,10 +23,12 @@ const messages = defineMessages({
|
|||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
});
|
||||
|
||||
const BackButton = ({ pinned, show }) => {
|
||||
const BackButton = ({ pinned, show, onlyIcon }) => {
|
||||
const history = useAppHistory();
|
||||
const intl = useIntl();
|
||||
const { multiColumn } = useColumnsContext();
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
|
@ -39,18 +41,20 @@ const BackButton = ({ pinned, show }) => {
|
|||
|
||||
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
|
||||
|
||||
if(!showButton) return null;
|
||||
|
||||
return (<button onClick={handleBackClick} className='column-header__back-button'>
|
||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</button>);
|
||||
if (!showButton) return null;
|
||||
|
||||
return (
|
||||
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
|
||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
||||
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
BackButton.propTypes = {
|
||||
pinned: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
onlyIcon: PropTypes.bool,
|
||||
};
|
||||
|
||||
class ColumnHeader extends PureComponent {
|
||||
|
@ -145,27 +149,31 @@ class ColumnHeader extends PureComponent {
|
|||
}
|
||||
|
||||
if (multiColumn && pinned) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
|
||||
moveButtons = (
|
||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
||||
<div className='column-header__setting-arrows'>
|
||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn && this.props.onPin) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
}
|
||||
|
||||
backButton = <BackButton pinned={pinned} show={showBackButton} />;
|
||||
backButton = <BackButton pinned={pinned} show={showBackButton} onlyIcon={!!title} />;
|
||||
|
||||
const collapsedContent = [
|
||||
extraContent,
|
||||
];
|
||||
|
||||
if (multiColumn) {
|
||||
collapsedContent.push(pinButton);
|
||||
collapsedContent.push(moveButtons);
|
||||
collapsedContent.push(
|
||||
<div key='buttons' className='column-header__advanced-buttons'>
|
||||
{pinButton}
|
||||
{moveButtons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||
|
@ -177,7 +185,7 @@ class ColumnHeader extends PureComponent {
|
|||
onClick={this.handleToggleClick}
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id='sliders' icon={TuneIcon} />
|
||||
<Icon id='sliders' icon={SettingsIcon} />
|
||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||
</i>
|
||||
</button>
|
||||
|
@ -190,16 +198,19 @@ class ColumnHeader extends PureComponent {
|
|||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<button onClick={this.handleTitleClick}>
|
||||
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
|
||||
{title}
|
||||
</button>
|
||||
<>
|
||||
{backButton}
|
||||
|
||||
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
||||
{title}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasTitle && backButton}
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{hasTitle && backButton}
|
||||
{extraButton}
|
||||
{collapseButton}
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@ class ColumnSettings extends PureComponent {
|
|||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings'>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||
</div>
|
||||
|
|
|
@ -42,15 +42,17 @@ const ColumnSettings = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['onlyMedia']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
||||
/>
|
||||
</div>
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['onlyMedia']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -107,28 +107,28 @@ class ColumnSettings extends PureComponent {
|
|||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||
|
||||
<span className='setting-toggle__label'>
|
||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||
</span>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||
|
||||
<span className='setting-toggle__label'>
|
||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.open && (
|
||||
<div className='column-settings__hashtags'>
|
||||
{this.modeSelect('any')}
|
||||
{this.modeSelect('all')}
|
||||
{this.modeSelect('none')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||
</div>
|
||||
{this.state.open && (
|
||||
<div className='column-settings__hashtags'>
|
||||
{this.modeSelect('any')}
|
||||
{this.modeSelect('all')}
|
||||
{this.modeSelect('none')}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,43 +24,36 @@ export const ColumnSettings: React.FC = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className='column-settings__section'>
|
||||
<FormattedMessage
|
||||
id='home.column_settings.basic'
|
||||
defaultMessage='Basic'
|
||||
/>
|
||||
</span>
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reblog']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_reblogs'
|
||||
defaultMessage='Show boosts'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reblog']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_reblogs'
|
||||
defaultMessage='Show boosts'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reply']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_replies'
|
||||
defaultMessage='Show replies'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reply']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_replies'
|
||||
defaultMessage='Show replies'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -193,35 +193,38 @@ class ListTimeline extends PureComponent {
|
|||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-settings__row column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
<div className='column-settings'>
|
||||
<section className='column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</div>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{ replies_policy !== undefined && (
|
||||
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
||||
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
||||
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
|
||||
</span>
|
||||
<div className='column-settings__row'>
|
||||
{ ['none', 'list', 'followed'].map(policy => (
|
||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||
))}
|
||||
<section>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{replies_policy !== undefined && (
|
||||
<section aria-labelledby={`list-${id}-replies-policy`}>
|
||||
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
{ ['none', 'list', 'followed'].map(policy => (
|
||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
export const CheckboxWithLabel = ({ checked, disabled, children, onChange }) => {
|
||||
const handleChange = useCallback(({ target }) => {
|
||||
onChange(target.checked);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<label className='app-form__toggle'>
|
||||
<div className='app-form__toggle__label'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className='app-form__toggle__toggle'>
|
||||
<div>
|
||||
<Toggle checked={checked} onChange={handleChange} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
CheckboxWithLabel.propTypes = {
|
||||
checked: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.children,
|
||||
onChange: PropTypes.func,
|
||||
};
|
|
@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
|
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'mastodon/permissions';
|
||||
|
||||
import { CheckboxWithLabel } from './checkbox_with_label';
|
||||
import ClearColumnButton from './clear_column_button';
|
||||
import GrantPermissionButton from './grant_permission_button';
|
||||
import SettingToggle from './setting_toggle';
|
||||
|
@ -26,18 +27,34 @@ export default class ColumnSettings extends PureComponent {
|
|||
alertsEnabled: PropTypes.bool,
|
||||
browserSupport: PropTypes.bool,
|
||||
browserPermission: PropTypes.string,
|
||||
notificationPolicy: ImmutablePropTypes.map,
|
||||
onChangePolicy: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onPushChange = (path, checked) => {
|
||||
this.props.onChange(['push', ...path], checked);
|
||||
};
|
||||
|
||||
handleFilterNotFollowing = checked => {
|
||||
this.props.onChangePolicy('filter_not_following', checked);
|
||||
};
|
||||
|
||||
handleFilterNotFollowers = checked => {
|
||||
this.props.onChangePolicy('filter_not_followers', checked);
|
||||
};
|
||||
|
||||
handleFilterNewAccounts = checked => {
|
||||
this.props.onChangePolicy('filter_new_accounts', checked);
|
||||
};
|
||||
|
||||
handleFilterPrivateMentions = checked => {
|
||||
this.props.onChangePolicy('filter_private_mentions', checked);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;
|
||||
|
||||
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
||||
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
|
@ -46,48 +63,59 @@ export default class ColumnSettings extends PureComponent {
|
|||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings'>
|
||||
{alertsEnabled && browserSupport && browserPermission === 'denied' && (
|
||||
<div className='column-settings__row column-settings__row--with-margin'>
|
||||
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
|
||||
</div>
|
||||
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
|
||||
)}
|
||||
|
||||
{alertsEnabled && browserSupport && browserPermission === 'default' && (
|
||||
<div className='column-settings__row column-settings__row--with-margin'>
|
||||
<span className='warning-hint'>
|
||||
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
|
||||
</span>
|
||||
</div>
|
||||
<span className='warning-hint'>
|
||||
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<section>
|
||||
<ClearColumnButton onClick={onClear} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-unread-markers'>
|
||||
<span id='notifications-unread-markers' className='column-settings__section'>
|
||||
<section>
|
||||
<h3><FormattedMessage id='notifications.policy.title' defaultMessage='Filter out notifications from…' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_following')} onChange={this.handleFilterNotFollowing}>
|
||||
<strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
|
||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
|
||||
</CheckboxWithLabel>
|
||||
|
||||
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_followers')} onChange={this.handleFilterNotFollowers}>
|
||||
<strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
|
||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_followers_hint' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' values={{ days: 3 }} /></span>
|
||||
</CheckboxWithLabel>
|
||||
|
||||
<CheckboxWithLabel checked={notificationPolicy.get('filter_new_accounts')} onChange={this.handleFilterNewAccounts}>
|
||||
<strong><FormattedMessage id='notifications.policy.filter_new_accounts_title' defaultMessage='New accounts' /></strong>
|
||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_new_accounts.hint' defaultMessage='Created within the past {days, plural, one {one day} other {# days}}' values={{ days: 30 }} /></span>
|
||||
</CheckboxWithLabel>
|
||||
|
||||
<CheckboxWithLabel checked={notificationPolicy.get('filter_private_mentions')} onChange={this.handleFilterPrivateMentions}>
|
||||
<strong><FormattedMessage id='notifications.policy.filter_private_mentions_title' defaultMessage='Unsolicited private mentions' /></strong>
|
||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_private_mentions_hint' defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" /></span>
|
||||
</CheckboxWithLabel>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section role='group' aria-labelledby='notifications-unread-markers'>
|
||||
<h3 id='notifications-unread-markers'>
|
||||
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
|
||||
</span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-follow'>
|
||||
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-follow'>
|
||||
<h3 id='notifications-follow'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
||||
|
@ -95,10 +123,10 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-follow-request'>
|
||||
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-follow-request'>
|
||||
<h3 id='notifications-follow-request'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
|
||||
|
@ -106,10 +134,10 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-favourite'>
|
||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-favourite'>
|
||||
<h3 id='notifications-favourite'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
||||
|
@ -117,10 +145,10 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-mention'>
|
||||
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-mention'>
|
||||
<h3 id='notifications-mention'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
||||
|
@ -128,10 +156,10 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-reblog'>
|
||||
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-reblog'>
|
||||
<h3 id='notifications-reblog'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
||||
|
@ -139,10 +167,10 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-poll'>
|
||||
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-poll'>
|
||||
<h3 id='notifications-poll'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
|
||||
|
@ -150,10 +178,10 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-status'>
|
||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-status'>
|
||||
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
|
||||
|
@ -161,10 +189,10 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-update'>
|
||||
<span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-update'>
|
||||
<h3 id='notifications-update'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
|
||||
|
@ -172,11 +200,11 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
|
||||
<div role='group' aria-labelledby='notifications-admin-sign-up'>
|
||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-admin-sign-up'>
|
||||
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
|
||||
|
@ -184,12 +212,12 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
|
||||
<div role='group' aria-labelledby='notifications-admin-report'>
|
||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-admin-report'>
|
||||
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
|
||||
|
@ -197,7 +225,7 @@ export default class ColumnSettings extends PureComponent {
|
|||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
|
||||
import ArchiveIcon from '@/material-icons/400-24px/archive.svg?react';
|
||||
import { fetchNotificationPolicy } from 'mastodon/actions/notifications';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { toCappedNumber } from 'mastodon/utils/numbers';
|
||||
|
||||
export const FilteredNotificationsBanner = () => {
|
||||
const dispatch = useDispatch();
|
||||
const policy = useSelector(state => state.get('notificationPolicy'));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchNotificationPolicy());
|
||||
|
||||
const interval = setInterval(() => {
|
||||
dispatch(fetchNotificationPolicy());
|
||||
}, 120000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) * 1 === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className='filtered-notifications-banner' to='/notifications/requests'>
|
||||
<Icon icon={ArchiveIcon} />
|
||||
|
||||
<div className='filtered-notifications-banner__text'>
|
||||
<strong><FormattedMessage id='filtered_notifications_banner.title' defaultMessage='Filtered notifications' /></strong>
|
||||
<span><FormattedMessage id='filtered_notifications_banner.pending_requests' defaultMessage='Notifications from {count, plural, =0 {no} one {one person} other {# people}} you may know' values={{ count: policy.getIn(['summary', 'pending_requests_count']) }} /></span>
|
||||
</div>
|
||||
|
||||
<div className='filtered-notifications-banner__badge'>
|
||||
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
||||
import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
import { toCappedNumber } from 'mastodon/utils/numbers';
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const messages = defineMessages({
|
||||
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
|
||||
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
|
||||
});
|
||||
|
||||
export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||
const dispatch = useDispatch();
|
||||
const account = useSelector(state => getAccount(state, accountId));
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dispatch(dismissNotificationRequest(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleAccept = useCallback(() => {
|
||||
dispatch(acceptNotificationRequest(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<div className='notification-request'>
|
||||
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
|
||||
<Avatar account={account} size={36} />
|
||||
|
||||
<div className='notification-request__name'>
|
||||
<div className='notification-request__name__display-name'>
|
||||
<bdi><strong dangerouslySetInnerHTML={{ __html: account?.get('display_name_html') }} /></bdi>
|
||||
<span className='filtered-notifications-banner__badge'>{toCappedNumber(notificationsCount)}</span>
|
||||
</div>
|
||||
|
||||
<span>@{account?.get('acct')}</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className='notification-request__actions'>
|
||||
<IconButton iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||
<IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationRequest.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
accountId: PropTypes.string.isRequired,
|
||||
notificationsCount: PropTypes.string.isRequired,
|
||||
};
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { showAlert } from '../../../actions/alerts';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
|
||||
import { setFilter, clearNotifications, requestBrowserPermission, updateNotificationsPolicy } from '../../../actions/notifications';
|
||||
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
|
@ -21,6 +21,7 @@ const mapStateToProps = state => ({
|
|||
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
||||
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
||||
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
||||
notificationPolicy: state.get('notificationPolicy'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
@ -73,6 +74,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(requestBrowserPermission());
|
||||
},
|
||||
|
||||
onChangePolicy (param, checked) {
|
||||
dispatch(updateNotificationsPolicy({
|
||||
[param]: checked,
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
||||
|
|
|
@ -5,7 +5,7 @@ import FilterBar from '../components/filter_bar';
|
|||
|
||||
const makeMapStateToProps = state => ({
|
||||
selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
|
||||
advancedMode: state.getIn(['settings', 'notifications', 'quickFilter', 'advanced']),
|
||||
advancedMode: false,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
|
|
@ -33,6 +33,7 @@ import ColumnHeader from '../../components/column_header';
|
|||
import { LoadGap } from '../../components/load_gap';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
import { FilteredNotificationsBanner } from './components/filtered_notifications_banner';
|
||||
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import FilterBarContainer from './containers/filter_bar_container';
|
||||
|
@ -65,7 +66,6 @@ const getNotifications = createSelector([
|
|||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
|
||||
notifications: getNotifications(state),
|
||||
isLoading: state.getIn(['notifications', 'isLoading'], 0) > 0,
|
||||
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
|
||||
|
@ -85,7 +85,6 @@ class Notifications extends PureComponent {
|
|||
static propTypes = {
|
||||
columnId: PropTypes.string,
|
||||
notifications: ImmutablePropTypes.list.isRequired,
|
||||
showFilterBar: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
|
@ -188,14 +187,14 @@ class Notifications extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
|
||||
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let scrollableContent = null;
|
||||
|
||||
const filterBarContainer = (signedIn && showFilterBar)
|
||||
const filterBarContainer = signedIn
|
||||
? (<FilterBarContainer />)
|
||||
: null;
|
||||
|
||||
|
@ -285,6 +284,9 @@ class Notifications extends PureComponent {
|
|||
</ColumnHeader>
|
||||
|
||||
{filterBarContainer}
|
||||
|
||||
<FilteredNotificationsBanner />
|
||||
|
||||
{scrollContainer}
|
||||
|
||||
<Helmet>
|
||||
|
|
144
app/javascript/mastodon/features/notifications/request.jsx
Normal file
144
app/javascript/mastodon/features/notifications/request.jsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import ArchiveIcon from '@/material-icons/400-24px/archive.svg?react';
|
||||
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
||||
import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
|
||||
import NotificationContainer from './containers/notification_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'notification_requests.notifications_from', defaultMessage: 'Notifications from {name}' },
|
||||
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
|
||||
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
|
||||
});
|
||||
|
||||
const selectChild = (ref, index, alignTop) => {
|
||||
const container = ref.current.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (alignTop && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!alignTop && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
|
||||
element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
||||
const columnRef = useRef();
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null);
|
||||
const accountId = notificationRequest?.get('account');
|
||||
const account = useSelector(state => state.getIn(['accounts', accountId]));
|
||||
const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items']));
|
||||
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading']));
|
||||
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next']));
|
||||
const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed']));
|
||||
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
columnRef.current?.scrollTop();
|
||||
}, [columnRef]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
dispatch(expandNotificationsForRequest());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dispatch(dismissNotificationRequest(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleAccept = useCallback(() => {
|
||||
dispatch(acceptNotificationRequest(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleMoveUp = useCallback(id => {
|
||||
const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||
selectChild(columnRef, elementIndex, true);
|
||||
}, [columnRef, notifications]);
|
||||
|
||||
const handleMoveDown = useCallback(id => {
|
||||
const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||
selectChild(columnRef, elementIndex, false);
|
||||
}, [columnRef, notifications]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchNotificationRequest(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) {
|
||||
dispatch(fetchNotificationsForRequest(accountId));
|
||||
}
|
||||
}, [dispatch, accountId]);
|
||||
|
||||
const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') });
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={columnTitle}>
|
||||
<ColumnHeader
|
||||
icon='archive'
|
||||
iconComponent={ArchiveIcon}
|
||||
title={columnTitle}
|
||||
onClick={handleHeaderClick}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
extraButton={!removed && (
|
||||
<>
|
||||
<IconButton className='column-header__button' iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||
<IconButton className='column-header__button' iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey={`notification_requests/${id}`}
|
||||
trackScroll={!multiColumn}
|
||||
bindToDocument={!multiColumn}
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && notifications.size === 0}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
>
|
||||
{notifications.map(item => (
|
||||
item && <NotificationContainer
|
||||
key={item.get('id')}
|
||||
notification={item}
|
||||
accountId={item.get('account')}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{columnTitle}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationRequest.propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
params: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
export default NotificationRequest;
|
85
app/javascript/mastodon/features/notifications/requests.jsx
Normal file
85
app/javascript/mastodon/features/notifications/requests.jsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import ArchiveIcon from '@/material-icons/400-24px/archive.svg?react';
|
||||
import { fetchNotificationRequests, expandNotificationRequests } from 'mastodon/actions/notifications';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
|
||||
import { NotificationRequest } from './components/notification_request';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
|
||||
});
|
||||
|
||||
export const NotificationRequests = ({ multiColumn }) => {
|
||||
const columnRef = useRef();
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading']));
|
||||
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
|
||||
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
|
||||
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
columnRef.current?.scrollTop();
|
||||
}, [columnRef]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
dispatch(expandNotificationRequests());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchNotificationRequests());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='archive'
|
||||
iconComponent={ArchiveIcon}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onClick={handleHeaderClick}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='notification_requests'
|
||||
trackScroll={!multiColumn}
|
||||
bindToDocument={!multiColumn}
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && notificationRequests.size === 0}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.notification_requests' defaultMessage='All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.' />}
|
||||
>
|
||||
{notificationRequests.map(request => (
|
||||
<NotificationRequest
|
||||
key={request.get('id')}
|
||||
id={request.get('id')}
|
||||
accountId={request.get('account')}
|
||||
notificationsCount={request.get('notifications_count')}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationRequests.propTypes = {
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default NotificationRequests;
|
|
@ -20,11 +20,13 @@ class ColumnSettings extends PureComponent {
|
|||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
|
||||
</div>
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ import {
|
|||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
Notifications,
|
||||
NotificationRequests,
|
||||
NotificationRequest,
|
||||
FollowRequests,
|
||||
FavouritedStatuses,
|
||||
BookmarkedStatuses,
|
||||
|
@ -203,7 +205,9 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
||||
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
|
||||
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||
<WrappedRoute path='/notifications/requests/:id' component={NotificationRequest} content={children} exact />
|
||||
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
|
||||
|
||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||
|
|
|
@ -189,3 +189,11 @@ export function About () {
|
|||
export function PrivacyPolicy () {
|
||||
return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy');
|
||||
}
|
||||
|
||||
export function NotificationRequests () {
|
||||
return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests');
|
||||
}
|
||||
|
||||
export function NotificationRequest () {
|
||||
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
|
||||
}
|
||||
|
|
|
@ -241,6 +241,7 @@
|
|||
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
|
||||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||
"empty_column.mutes": "You haven't muted any users yet.",
|
||||
"empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.",
|
||||
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
|
||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
|
||||
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
|
||||
|
@ -271,6 +272,8 @@
|
|||
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
||||
"filter_modal.select_filter.title": "Filter this post",
|
||||
"filter_modal.title.status": "Filter a post",
|
||||
"filtered_notifications_banner.pending_requests": "Notifications from {count, plural, =0 {no} one {one person} other {# people}} you may know",
|
||||
"filtered_notifications_banner.title": "Filtered notifications",
|
||||
"firehose.all": "All",
|
||||
"firehose.local": "This server",
|
||||
"firehose.remote": "Other servers",
|
||||
|
@ -314,7 +317,6 @@
|
|||
"hashtag.follow": "Follow hashtag",
|
||||
"hashtag.unfollow": "Unfollow hashtag",
|
||||
"hashtags.and_other": "…and {count, plural, other {# more}}",
|
||||
"home.column_settings.basic": "Basic",
|
||||
"home.column_settings.show_reblogs": "Show boosts",
|
||||
"home.column_settings.show_replies": "Show replies",
|
||||
"home.hide_announcements": "Hide announcements",
|
||||
|
@ -440,15 +442,16 @@
|
|||
"notification.reblog": "{name} boosted your post",
|
||||
"notification.status": "{name} just posted",
|
||||
"notification.update": "{name} edited a post",
|
||||
"notification_requests.accept": "Accept",
|
||||
"notification_requests.dismiss": "Dismiss",
|
||||
"notification_requests.notifications_from": "Notifications from {name}",
|
||||
"notification_requests.title": "Filtered notifications",
|
||||
"notifications.clear": "Clear notifications",
|
||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||
"notifications.column_settings.admin.report": "New reports:",
|
||||
"notifications.column_settings.admin.sign_up": "New sign-ups:",
|
||||
"notifications.column_settings.alert": "Desktop notifications",
|
||||
"notifications.column_settings.favourite": "Favorites:",
|
||||
"notifications.column_settings.filter_bar.advanced": "Display all categories",
|
||||
"notifications.column_settings.filter_bar.category": "Quick filter bar",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
|
||||
"notifications.column_settings.follow": "New followers:",
|
||||
"notifications.column_settings.follow_request": "New follow requests:",
|
||||
"notifications.column_settings.mention": "Mentions:",
|
||||
|
@ -474,6 +477,15 @@
|
|||
"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.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}",
|
||||
"notifications.policy.filter_new_accounts_title": "New accounts",
|
||||
"notifications.policy.filter_not_followers_hint": "Including people who have been following you fewer than {days, plural, one {one day} other {# days}}",
|
||||
"notifications.policy.filter_not_followers_title": "People not following you",
|
||||
"notifications.policy.filter_not_following_hint": "Until you manually approve them",
|
||||
"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_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",
|
||||
|
|
|
@ -27,6 +27,8 @@ import media_attachments from './media_attachments';
|
|||
import meta from './meta';
|
||||
import { modalReducer } from './modal';
|
||||
import mutes from './mutes';
|
||||
import { notificationPolicyReducer } from './notification_policy';
|
||||
import { notificationRequestsReducer } from './notification_requests';
|
||||
import notifications from './notifications';
|
||||
import picture_in_picture from './picture_in_picture';
|
||||
import polls from './polls';
|
||||
|
@ -84,6 +86,8 @@ const reducers = {
|
|||
history,
|
||||
tags,
|
||||
followed_tags,
|
||||
notificationPolicy: notificationPolicyReducer,
|
||||
notificationRequests: notificationRequestsReducer,
|
||||
};
|
||||
|
||||
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
||||
|
|
12
app/javascript/mastodon/reducers/notification_policy.js
Normal file
12
app/javascript/mastodon/reducers/notification_policy.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import { NOTIFICATION_POLICY_FETCH_SUCCESS } from 'mastodon/actions/notifications';
|
||||
|
||||
export const notificationPolicyReducer = (state = null, action) => {
|
||||
switch(action.type) {
|
||||
case NOTIFICATION_POLICY_FETCH_SUCCESS:
|
||||
return fromJS(action.policy);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
96
app/javascript/mastodon/reducers/notification_requests.js
Normal file
96
app/javascript/mastodon/reducers/notification_requests.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
NOTIFICATION_REQUESTS_EXPAND_REQUEST,
|
||||
NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
|
||||
NOTIFICATION_REQUESTS_EXPAND_FAIL,
|
||||
NOTIFICATION_REQUESTS_FETCH_REQUEST,
|
||||
NOTIFICATION_REQUESTS_FETCH_SUCCESS,
|
||||
NOTIFICATION_REQUESTS_FETCH_FAIL,
|
||||
NOTIFICATION_REQUEST_FETCH_REQUEST,
|
||||
NOTIFICATION_REQUEST_FETCH_SUCCESS,
|
||||
NOTIFICATION_REQUEST_FETCH_FAIL,
|
||||
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
||||
NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
||||
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
||||
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
||||
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
||||
NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
|
||||
NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
|
||||
NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
|
||||
} from 'mastodon/actions/notifications';
|
||||
|
||||
import { notificationToMap } from './notifications';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
isLoading: false,
|
||||
next: null,
|
||||
current: ImmutableMap({
|
||||
isLoading: false,
|
||||
item: null,
|
||||
removed: false,
|
||||
notifications: ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
isLoading: false,
|
||||
next: null,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const normalizeRequest = request => fromJS({
|
||||
...request,
|
||||
account: request.account.id,
|
||||
});
|
||||
|
||||
const removeRequest = (state, id) => {
|
||||
if (state.getIn(['current', 'item', 'id']) === id) {
|
||||
state = state.setIn(['current', 'removed'], true);
|
||||
}
|
||||
|
||||
return state.update('items', list => list.filterNot(item => item.get('id') === id));
|
||||
};
|
||||
|
||||
export const notificationRequestsReducer = (state = initialState, action) => {
|
||||
switch(action.type) {
|
||||
case NOTIFICATION_REQUESTS_FETCH_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.update('items', list => ImmutableList(action.requests.map(normalizeRequest)).concat(list));
|
||||
map.set('isLoading', false);
|
||||
map.update('next', next => next ?? action.next);
|
||||
});
|
||||
case NOTIFICATION_REQUESTS_EXPAND_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.update('items', list => list.concat(ImmutableList(action.requests.map(normalizeRequest))));
|
||||
map.set('isLoading', false);
|
||||
map.set('next', action.next);
|
||||
});
|
||||
case NOTIFICATION_REQUESTS_EXPAND_REQUEST:
|
||||
case NOTIFICATION_REQUESTS_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case NOTIFICATION_REQUESTS_EXPAND_FAIL:
|
||||
case NOTIFICATION_REQUESTS_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
|
||||
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
|
||||
return removeRequest(state, action.id);
|
||||
case NOTIFICATION_REQUEST_FETCH_REQUEST:
|
||||
return state.set('current', initialState.get('current').set('isLoading', true));
|
||||
case NOTIFICATION_REQUEST_FETCH_SUCCESS:
|
||||
return state.update('current', map => map.set('isLoading', false).set('item', normalizeRequest(action.request)));
|
||||
case NOTIFICATION_REQUEST_FETCH_FAIL:
|
||||
return state.update('current', map => map.set('isLoading', false));
|
||||
case NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST:
|
||||
case NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST:
|
||||
return state.setIn(['current', 'notifications', 'isLoading'], true);
|
||||
case NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS:
|
||||
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => ImmutableList(action.notifications.map(notificationToMap)).concat(list)).update('next', next => next ?? action.next));
|
||||
case NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS:
|
||||
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => list.concat(ImmutableList(action.notifications.map(notificationToMap)))).set('next', action.next));
|
||||
case NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL:
|
||||
case NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL:
|
||||
return state.setIn(['current', 'notifications', 'isLoading'], false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -48,7 +48,7 @@ const initialState = ImmutableMap({
|
|||
browserPermission: 'default',
|
||||
});
|
||||
|
||||
const notificationToMap = notification => ImmutableMap({
|
||||
export const notificationToMap = notification => ImmutableMap({
|
||||
id: notification.id,
|
||||
type: notification.type,
|
||||
account: notification.account.id,
|
||||
|
|
|
@ -69,3 +69,11 @@ export function pluralReady(
|
|||
export function roundTo10(num: number): number {
|
||||
return Math.round(num * 0.1) / 0.1;
|
||||
}
|
||||
|
||||
export function toCappedNumber(num: string): string {
|
||||
if (parseInt(num) > 99) {
|
||||
return '99+';
|
||||
} else {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
|
1
app/javascript/material-icons/400-24px/archive-fill.svg
Normal file
1
app/javascript/material-icons/400-24px/archive-fill.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="m480-240 160-160-56-56-64 64v-168h-80v168l-64-64-56 56 160 160ZM200-120q-33 0-56.5-23.5T120-200v-499q0-14 4.5-27t13.5-24l50-61q11-14 27.5-21.5T250-840h460q18 0 34.5 7.5T772-811l50 61q9 11 13.5 24t4.5 27v499q0 33-23.5 56.5T760-120H200Zm16-600h528l-34-40H250l-34 40Z"/></svg>
|
After Width: | Height: | Size: 370 B |
1
app/javascript/material-icons/400-24px/archive.svg
Normal file
1
app/javascript/material-icons/400-24px/archive.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="m480-240 160-160-56-56-64 64v-168h-80v168l-64-64-56 56 160 160ZM200-640v440h560v-440H200Zm0 520q-33 0-56.5-23.5T120-200v-499q0-14 4.5-27t13.5-24l50-61q11-14 27.5-21.5T250-840h460q18 0 34.5 7.5T772-811l50 61q9 11 13.5 24t4.5 27v499q0 33-23.5 56.5T760-120H200Zm16-600h528l-34-40H250l-34 40Zm264 300Z"/></svg>
|
After Width: | Height: | Size: 403 B |
|
@ -3289,12 +3289,13 @@ $ui-header-height: 55px;
|
|||
border: 0;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
text-align: unset;
|
||||
padding: 15px;
|
||||
padding: 13px;
|
||||
margin: 0;
|
||||
z-index: 3;
|
||||
outline: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
@ -3304,6 +3305,7 @@ $ui-header-height: 55px;
|
|||
.column-header__back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: $ui-base-color;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
|
@ -3311,23 +3313,19 @@ $ui-header-height: 55px;
|
|||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-size: 16px;
|
||||
padding: 0 5px 0 0;
|
||||
padding: 13px;
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding: 0 15px 0 0;
|
||||
&.compact {
|
||||
padding-inline-end: 5px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.column-back-button__icon {
|
||||
display: inline-block;
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.react-toggle {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
@ -4013,7 +4011,7 @@ a.status-card {
|
|||
z-index: 2;
|
||||
outline: 0;
|
||||
|
||||
& > button {
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
@ -4035,8 +4033,18 @@ a.status-card {
|
|||
}
|
||||
}
|
||||
|
||||
& > .column-header__back-button {
|
||||
.column-header__back-button + &__title {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.column-header__back-button {
|
||||
flex: 1;
|
||||
color: $highlight-text-color;
|
||||
|
||||
&.compact {
|
||||
flex: 0 0 auto;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
@ -4050,6 +4058,18 @@ a.status-card {
|
|||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&__advanced-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
padding-top: 0;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-header__buttons {
|
||||
|
@ -4136,7 +4156,6 @@ a.status-card {
|
|||
|
||||
.column-header__collapsible-inner {
|
||||
background: $ui-base-color;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.column-header__setting-btn {
|
||||
|
@ -4158,20 +4177,8 @@ a.status-card {
|
|||
}
|
||||
|
||||
.column-header__setting-arrows {
|
||||
float: right;
|
||||
|
||||
.column-header__setting-btn {
|
||||
padding: 5px;
|
||||
|
||||
&:first-child {
|
||||
padding-inline-end: 7px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-inline-start: 7px;
|
||||
margin-inline-start: 5px;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-btn {
|
||||
|
@ -4408,24 +4415,56 @@ a.status-card {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.column-settings__outer {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
padding: 15px;
|
||||
}
|
||||
.column-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.column-settings__section {
|
||||
color: $darker-text-color;
|
||||
cursor: default;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
&__section {
|
||||
// FIXME: Legacy
|
||||
color: $darker-text-color;
|
||||
cursor: default;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.column-settings__row--with-margin {
|
||||
margin-bottom: 15px;
|
||||
.column-header__links {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
color: $primary-text-color;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-form__toggle {
|
||||
&__toggle > div {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-settings__hashtags {
|
||||
margin-top: 15px;
|
||||
|
||||
.column-settings__row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
@ -4549,16 +4588,13 @@ a.status-card {
|
|||
}
|
||||
|
||||
.setting-toggle {
|
||||
display: block;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.setting-toggle__label {
|
||||
color: $darker-text-color;
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
margin-inline-start: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.limited-account-hint {
|
||||
|
@ -6949,29 +6985,33 @@ a.status-card {
|
|||
background: $ui-base-color;
|
||||
|
||||
&__column {
|
||||
padding: 10px 15px;
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.column-settings__row .radio-button {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 6px 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
gap: 10px;
|
||||
color: $secondary-text-color;
|
||||
|
||||
input[type='radio'],
|
||||
input[type='checkbox'] {
|
||||
|
@ -6979,21 +7019,29 @@ a.status-card {
|
|||
}
|
||||
|
||||
&__input {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
position: relative;
|
||||
border: 1px solid $ui-primary-color;
|
||||
border: 2px solid $secondary-text-color;
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
margin-inline-end: 10px;
|
||||
top: -1px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
|
||||
&.checked {
|
||||
border-color: lighten($ui-highlight-color, 4%);
|
||||
background: lighten($ui-highlight-color, 4%);
|
||||
border-color: $secondary-text-color;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9588,3 +9636,110 @@ noscript {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filtered-notifications-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $ui-base-color;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
padding: 15px;
|
||||
gap: 15px;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $secondary-text-color;
|
||||
|
||||
.filtered-notifications-banner__badge {
|
||||
background: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1 1 auto;
|
||||
font-style: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
background: $darker-text-color;
|
||||
color: $ui-base-color;
|
||||
border-radius: 100px;
|
||||
padding: 2px 8px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-request {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
&__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1 1 auto;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
|
||||
.account__avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1 1 auto;
|
||||
color: $darker-text-color;
|
||||
font-style: 14px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&__display-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 24px;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
.filtered-notifications-banner__badge {
|
||||
background-color: $highlight-text-color;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.icon-button {
|
||||
border-radius: 4px;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1313,6 +1313,12 @@ code {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
.recommended {
|
||||
position: absolute;
|
||||
margin: 0 4px;
|
||||
|
|
|
@ -48,6 +48,6 @@ class NotificationRequest < ApplicationRecord
|
|||
private
|
||||
|
||||
def prepare_notifications_count
|
||||
self.notifications_count = Notification.where(account: account, from_account: from_account).limit(MAX_MEANINGFUL_COUNT).count
|
||||
self.notifications_count = Notification.where(account: account, from_account: from_account, filtered: true).limit(MAX_MEANINGFUL_COUNT).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,7 +27,7 @@ Rails.application.routes.draw do
|
|||
/public/remote
|
||||
/conversations
|
||||
/lists/(*any)
|
||||
/notifications
|
||||
/notifications/(*any)
|
||||
/favourites
|
||||
/bookmarks
|
||||
/pinned
|
||||
|
|
|
@ -151,7 +151,7 @@ namespace :api, format: false do
|
|||
end
|
||||
|
||||
namespace :notifications do
|
||||
resources :requests, only: :index do
|
||||
resources :requests, only: [:index, :show] do
|
||||
member do
|
||||
post :accept
|
||||
post :dismiss
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe NotificationPolicy do
|
|||
let(:sender) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
Fabricate.times(2, :notification, account: subject.account, activity: Fabricate(:status, account: sender))
|
||||
Fabricate.times(2, :notification, account: subject.account, activity: Fabricate(:status, account: sender), filtered: true)
|
||||
Fabricate(:notification_request, account: subject.account, from_account: sender)
|
||||
subject.summarize!
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe NotificationRequest do
|
|||
|
||||
context 'when there are remaining notifications' do
|
||||
before do
|
||||
Fabricate(:notification, account: subject.account, activity: Fabricate(:status, account: subject.from_account))
|
||||
Fabricate(:notification, account: subject.account, activity: Fabricate(:status, account: subject.from_account), filtered: true)
|
||||
subject.reconsider_existence!
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue