Commit 4b4c1b2d authored by Sander Snel's avatar Sander Snel

Merge remote-tracking branch 'upstream/master'

parents 1557fd40 41950394
......@@ -183,6 +183,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
......
......@@ -178,7 +178,8 @@ STREAMING_CLUSTER_NUM=1
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_SEARCH_FILTER=%{uid}=%{email}
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
......
......@@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params
return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end
end
......@@ -19,6 +19,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
data = {
alerts: {
follow: alerts_enabled,
follow_request: false,
favourite: alerts_enabled,
reblog: alerts_enabled,
mention: alerts_enabled,
......@@ -58,6 +59,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def data_params
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end
end
......@@ -110,7 +110,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
return allTypes.filterNot(item => item === filter).toJS();
};
......
......@@ -57,6 +57,17 @@ export default class ColumnSettings extends React.PureComponent {
</div>
</div>
<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>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
<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>
<div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
......
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import Permalink from 'mastodon/components/permalink';
import IconButton from 'mastodon/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
});
export default @injectIntl
class FollowRequest extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onAuthorize: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { intl, hidden, account, onAuthorize, onReject } = this.props;
if (!account) {
return <div />;
}
if (hidden) {
return (
<Fragment>
{account.get('display_name')}
{account.get('username')}
</Fragment>
);
}
return (
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</Permalink>
<div className='account__relationship'>
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
<IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
</div>
</div>
</div>
);
}
}
......@@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'mastodon/initial_state';
import StatusContainer from 'mastodon/containers/status_container';
import AccountContainer from 'mastodon/containers/account_container';
import FollowRequestContainer from '../containers/follow_request_container';
import Icon from 'mastodon/components/icon';
import Permalink from 'mastodon/components/permalink';
......@@ -127,7 +128,29 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);
}
renderFollowRequest (notification, account, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user' fixedWidth />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} />
</span>
</div>
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
</div>
</HotKeys>
);
......@@ -261,6 +284,8 @@ class Notification extends ImmutablePureComponent {
switch(notification.get('type')) {
case 'follow':
return this.renderFollow(notification, account, link);
case 'follow_request':
return this.renderFollowRequest(notification, account, link);
case 'mention':
return this.renderMention(notification);
case 'favourite':
......
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import FollowRequest from '../components/follow_request';
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { id }) => ({
onAuthorize () {
dispatch(authorizeFollowRequest(id));
},
onReject () {
dispatch(rejectFollowRequest(id));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest);
......@@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { connect } from 'react-redux';
import { NavLink, withRouter } from 'react-router-dom';
import IconWithBadge from 'mastodon/components/icon_with_badge';
import { me } from 'mastodon/initial_state';
import { List as ImmutableList } from 'immutable';
import { FormattedMessage } from 'react-intl';
const mapStateToProps = state => ({
locked: state.getIn(['accounts', me, 'locked']),
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
});
......@@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
locked: PropTypes.bool,
count: PropTypes.number.isRequired,
};
componentDidMount () {
const { dispatch, locked } = this.props;
const { dispatch } = this.props;
if (locked) {
dispatch(fetchFollowRequests());
}
dispatch(fetchFollowRequests());
}
render () {
const { locked, count } = this.props;
const { count } = this.props;
if (!locked || count === 0) {
if (count === 0) {
return null;
}
......
......@@ -13,6 +13,8 @@ import {
import {
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
} from '../actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
......@@ -89,8 +91,8 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
});
};
const filterNotifications = (state, accountIds) => {
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')));
const filterNotifications = (state, accountIds, type) => {
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
return state.update('items', helper).update('pendingItems', helper);
};
......@@ -129,6 +131,11 @@ export default function notifications(state = initialState, action) {
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
case DOMAIN_BLOCK_SUCCESS:
return filterNotifications(state, action.accounts);
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
case FOLLOW_REQUEST_REJECT_SUCCESS:
return filterNotifications(state, [action.id], 'follow_request');
case ACCOUNT_MUTE_SUCCESS:
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
case TIMELINE_DELETE:
......
......@@ -6,6 +6,7 @@ const initialState = Immutable.Map({
subscription: null,
alerts: new Immutable.Map({
follow: false,
follow_request: false,
favourite: false,
reblog: false,
mention: false,
......
......@@ -30,6 +30,7 @@ const initialState = ImmutableMap({
notifications: ImmutableMap({
alerts: ImmutableMap({
follow: true,
follow_request: false,
favourite: true,
reblog: true,
mention: true,
......@@ -44,6 +45,7 @@ const initialState = ImmutableMap({
shows: ImmutableMap({
follow: true,
follow_request: false,
favourite: true,
reblog: true,
mention: true,
......@@ -52,6 +54,7 @@ const initialState = ImmutableMap({
sounds: ImmutableMap({
follow: true,
follow_request: false,
favourite: true,
reblog: true,
mention: true,
......
import {
NOTIFICATIONS_UPDATE,
} from '../actions/notifications';
import {
FOLLOWERS_FETCH_SUCCESS,
FOLLOWERS_EXPAND_SUCCESS,
......@@ -53,6 +56,12 @@ const appendToList = (state, type, id, accounts, next) => {
});
};
const normalizeFollowRequest = (state, notification) => {
return state.updateIn(['follow_requests', 'items'], list => {
return list.filterNot(item => item === notification.account.id).unshift(notification.account.id);
});
};
export default function userLists(state = initialState, action) {
switch(action.type) {
case FOLLOWERS_FETCH_SUCCESS:
......@@ -67,6 +76,8 @@ export default function userLists(state = initialState, action) {
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
case FAVOURITES_FETCH_SUCCESS:
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS:
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
......
......@@ -16,6 +16,7 @@ filenames.forEach(filename => {
filtered[locale] = {
'notification.favourite': full['notification.favourite'] || '',
'notification.follow': full['notification.follow'] || '',
'notification.follow_request': full['notification.follow_request'] || '',
'notification.mention': full['notification.mention'] || '',
'notification.reblog': full['notification.reblog'] || '',
'notification.poll': full['notification.poll'] || '',
......
......@@ -6,7 +6,7 @@ module LdapAuthenticable
class_methods do
def authenticate_with_ldap(params = {})
ldap = Net::LDAP.new(ldap_options)
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: params[:email])
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: params[:email])
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
ldap_get_user(user_info.first)
......@@ -25,7 +25,7 @@ module LdapAuthenticable
resource = joins(:account).find_by(accounts: { username: safe_username })
if resource.blank?
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
resource = new(email: attributes[Devise.ldap_mail.to_sym].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
resource.save!
end
......
......@@ -42,7 +42,7 @@ class Notification < ApplicationRecord
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
scope :browserable, ->(exclude_types = [], account_id = nil) {
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types + [:follow_request])
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
if account_id.nil?
where(activity_type: types)
else
......@@ -50,7 +50,7 @@ class Notification < ApplicationRecord
end
}
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, poll: [status: STATUS_INCLUDES]
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
def type
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
......@@ -69,10 +69,6 @@ class Notification < ApplicationRecord
end
end
def browserable?
type != :follow_request
end
class << self
def cache_ids
select(:id, :updated_at, :activity_type, :activity_id)
......
......@@ -36,7 +36,7 @@ class Poll < ApplicationRecord
scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) }
before_validation :prepare_options
before_validation :prepare_options, if: :local?
before_validation :prepare_votes_count
after_initialize :prepare_cached_tallies
......
......@@ -9,7 +9,7 @@ class NotifyService < BaseService
return if recipient.user.nil? || blocked?
create_notification!
push_notification! if @notification.browserable?
push_notification!
push_to_conversation! if direct_message?
send_email! if email_enabled?
rescue ActiveRecord::RecordInvalid
......
......@@ -19,7 +19,7 @@
.dashboard__counters__num= number_with_delimiter @blocks_count
.dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
%div
%div
= link_to admin_reports_path(by_target_domain: @instance.domain) do
.dashboard__counters__num= number_with_delimiter @reports_count
.dashboard__counters__label= t 'admin.instances.total_reported'
%div
......
......@@ -53,6 +53,8 @@ module Devise
@@ldap_base = nil
mattr_accessor :ldap_uid
@@ldap_uid = nil
mattr_accessor :ldap_mail
@@ldap_mail = nil
mattr_accessor :ldap_bind_dn
@@ldap_bind_dn = nil
mattr_accessor :ldap_password
......@@ -369,8 +371,9 @@ Devise.setup do |config|
config.ldap_bind_dn = ENV.fetch('LDAP_BIND_DN')
config.ldap_password = ENV.fetch('LDAP_PASSWORD')
config.ldap_uid = ENV.fetch('LDAP_UID', 'cn')
config.ldap_mail = ENV.fetch('LDAP_MAIL', 'mail')
config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true'
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '%{uid}=%{email}')
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '(|(%{uid}=%{email})(%{mail}=%{email}))')
config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true'
config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ')
config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_')
......
......@@ -34,32 +34,6 @@ RSpec.describe Notification, type: :model do
end
end
describe '#browserable?' do
let(:notification) { Fabricate(:notification) }
subject { notification.browserable? }
context 'type is :follow_request' do
before do
allow(notification).to receive(:type).and_return(:follow_request)
end
it 'returns false' do
is_expected.to be false
end
end
context 'type is not :follow_request' do
before do
allow(notification).to receive(:type).and_return(:else)
end
it 'returns true' do
is_expected.to be true
end
end
end
describe '#type' do
it 'returns :reblog for a Status' do
notification = Notification.new(activity: Status.new)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment