Add option to be notified when a followed user posts (#13546)
* Add bell button Fix #4890 * Remove duplicate type from post-deployment migration * Fix legacy class type mappings * Improve query performance with better index * Fix validation * Remove redundant index from notifications
This commit is contained in:
parent
75e4bd9413
commit
974b1b79ce
42 changed files with 330 additions and 112 deletions
|
@ -30,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
|
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
|
||||||
|
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
|
||||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
|
||||||
|
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
||||||
|
|
||||||
def authorize
|
def authorize
|
||||||
AuthorizeFollowService.new.call(account, current_account)
|
AuthorizeFollowService.new.call(account, current_account)
|
||||||
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
|
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
|
||||||
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -109,14 +109,14 @@ export function fetchAccountFail(id, error) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function followAccount(id, reblogs = true) {
|
export function followAccount(id, options = { reblogs: true }) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||||
|
|
||||||
dispatch(followAccountRequest(id, locked));
|
dispatch(followAccountRequest(id, locked));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(followAccountFail(error, locked));
|
dispatch(followAccountFail(error, locked));
|
||||||
|
|
|
@ -59,7 +59,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (notification.type === 'mention') {
|
if (['mention', 'status'].includes(notification.type)) {
|
||||||
const dropRegex = filters[0];
|
const dropRegex = filters[0];
|
||||||
const regex = filters[1];
|
const regex = filters[1];
|
||||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
const searchIndex = searchTextFromRawStatus(notification.status);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
|
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import Avatar from 'mastodon/components/avatar';
|
import Avatar from 'mastodon/components/avatar';
|
||||||
import { counterRenderer } from 'mastodon/components/common_counter';
|
import { counterRenderer } from 'mastodon/components/common_counter';
|
||||||
import ShortNumber from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
|
@ -35,6 +36,8 @@ const messages = defineMessages({
|
||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||||
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
||||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||||
|
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
||||||
|
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
||||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
|
@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent {
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onReport: PropTypes.func.isRequired,
|
|
||||||
onReblogToggle: PropTypes.func.isRequired,
|
onReblogToggle: PropTypes.func.isRequired,
|
||||||
|
onNotifyToggle: PropTypes.func.isRequired,
|
||||||
|
onReport: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
onBlockDomain: PropTypes.func.isRequired,
|
onBlockDomain: PropTypes.func.isRequired,
|
||||||
onUnblockDomain: PropTypes.func.isRequired,
|
onUnblockDomain: PropTypes.func.isRequired,
|
||||||
|
@ -144,6 +148,7 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
let info = [];
|
let info = [];
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
|
let bellBtn = '';
|
||||||
let lockedIcon = '';
|
let lockedIcon = '';
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
@ -173,6 +178,10 @@ class Header extends ImmutablePureComponent {
|
||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||||
|
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
||||||
actionBtn = '';
|
actionBtn = '';
|
||||||
}
|
}
|
||||||
|
@ -287,6 +296,7 @@ class Header extends ImmutablePureComponent {
|
||||||
{!suspended && (
|
{!suspended && (
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{actionBtn}
|
{actionBtn}
|
||||||
|
{bellBtn}
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -55,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
|
||||||
this.props.onReblogToggle(this.props.account);
|
this.props.onReblogToggle(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNotifyToggle = () => {
|
||||||
|
this.props.onNotifyToggle(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
handleMute = () => {
|
handleMute = () => {
|
||||||
this.props.onMute(this.props.account);
|
this.props.onMute(this.props.account);
|
||||||
}
|
}
|
||||||
|
@ -106,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
onDirect={this.handleDirect}
|
onDirect={this.handleDirect}
|
||||||
onReblogToggle={this.handleReblogToggle}
|
onReblogToggle={this.handleReblogToggle}
|
||||||
|
onNotifyToggle={this.handleNotifyToggle}
|
||||||
onReport={this.handleReport}
|
onReport={this.handleReport}
|
||||||
onMute={this.handleMute}
|
onMute={this.handleMute}
|
||||||
onBlockDomain={this.handleBlockDomain}
|
onBlockDomain={this.handleBlockDomain}
|
||||||
|
|
|
@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onReblogToggle (account) {
|
onReblogToggle (account) {
|
||||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||||
dispatch(followAccount(account.get('id'), false));
|
dispatch(followAccount(account.get('id'), { reblogs: false }));
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id'), true));
|
dispatch(followAccount(account.get('id'), { reblogs: true }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -90,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onNotifyToggle (account) {
|
||||||
|
if (account.getIn(['relationship', 'notifying'])) {
|
||||||
|
dispatch(followAccount(account.get('id'), { notify: false }));
|
||||||
|
} else {
|
||||||
|
dispatch(followAccount(account.get('id'), { notify: true }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onReport (account) {
|
onReport (account) {
|
||||||
dispatch(initReport(account));
|
dispatch(initReport(account));
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,7 @@ const tooltips = defineMessages({
|
||||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||||
|
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
|
||||||
>
|
>
|
||||||
<Icon id='tasks' fixedWidth />
|
<Icon id='tasks' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={selectedFilter === 'status' ? 'active' : ''}
|
||||||
|
onClick={this.onClick('status')}
|
||||||
|
title={intl.formatMessage(tooltips.statuses)}
|
||||||
|
>
|
||||||
|
<Icon id='home' fixedWidth />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={selectedFilter === 'follow' ? 'active' : ''}
|
className={selectedFilter === 'follow' ? 'active' : ''}
|
||||||
onClick={this.onClick('follow')}
|
onClick={this.onClick('follow')}
|
||||||
|
|
|
@ -17,6 +17,7 @@ const messages = defineMessages({
|
||||||
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
||||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||||
|
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||||
|
@ -237,6 +238,38 @@ class Notification extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderStatus (notification, link) {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className='notification notification-status focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='home' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderPoll (notification, account) {
|
renderPoll (notification, account) {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
const ownPoll = me === account.get('id');
|
const ownPoll = me === account.get('id');
|
||||||
|
@ -292,6 +325,8 @@ class Notification extends ImmutablePureComponent {
|
||||||
return this.renderFavourite(notification, link);
|
return this.renderFavourite(notification, link);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
|
case 'status':
|
||||||
|
return this.renderStatus(notification, link);
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return this.renderPoll(notification, account);
|
return this.renderPoll(notification, account);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6502,6 +6502,10 @@ noscript {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .icon-button {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,13 +118,13 @@ class ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_about_reblog(status)
|
def notify_about_reblog(status)
|
||||||
NotifyService.new.call(status.reblog.account, status)
|
NotifyService.new.call(status.reblog.account, :reblog, status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_about_mentions(status)
|
def notify_about_mentions(status)
|
||||||
status.active_mentions.includes(:account).each do |mention|
|
status.active_mentions.includes(:account).each do |mention|
|
||||||
next unless mention.account.local? && audience_includes?(mention.account)
|
next unless mention.account.local? && audience_includes?(mention.account)
|
||||||
NotifyService.new.call(mention.account, mention)
|
NotifyService.new.call(mention.account, :mention, mention)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,10 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
||||||
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
|
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
|
||||||
|
|
||||||
if target_account.locked? || @account.silenced?
|
if target_account.locked? || @account.silenced?
|
||||||
NotifyService.new.call(target_account, follow_request)
|
NotifyService.new.call(target_account, :follow_request, follow_request)
|
||||||
else
|
else
|
||||||
AuthorizeFollowService.new.call(@account, target_account)
|
AuthorizeFollowService.new.call(@account, target_account)
|
||||||
NotifyService.new.call(target_account, ::Follow.find_by(account: @account, target_account: target_account))
|
NotifyService.new.call(target_account, :follow, ::Follow.find_by(account: @account, target_account: target_account))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,6 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
||||||
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
|
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
|
||||||
|
|
||||||
favourite = original_status.favourites.create!(account: @account)
|
favourite = original_status.favourites.create!(account: @account)
|
||||||
NotifyService.new.call(original_status.account, favourite)
|
NotifyService.new.call(original_status.account, :favourite, favourite)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ module AccountInteractions
|
||||||
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
||||||
mapping[follow.target_account_id] = {
|
mapping[follow.target_account_id] = {
|
||||||
reblogs: follow.show_reblogs?,
|
reblogs: follow.show_reblogs?,
|
||||||
|
notify: follow.notify?,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -36,6 +37,7 @@ module AccountInteractions
|
||||||
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
||||||
mapping[follow_request.target_account_id] = {
|
mapping[follow_request.target_account_id] = {
|
||||||
reblogs: follow_request.show_reblogs?,
|
reblogs: follow_request.show_reblogs?,
|
||||||
|
notify: follow_request.notify?,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -95,25 +97,29 @@ module AccountInteractions
|
||||||
has_many :announcement_mutes, dependent: :destroy
|
has_many :announcement_mutes, dependent: :destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
|
||||||
reblogs = true if reblogs.nil?
|
rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
|
||||||
|
|
||||||
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
|
||||||
.find_or_create_by!(target_account: other_account)
|
.find_or_create_by!(target_account: other_account)
|
||||||
|
|
||||||
rel.update!(show_reblogs: reblogs)
|
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||||
|
rel.notify = notify unless notify.nil?
|
||||||
|
|
||||||
|
rel.save! if rel.changed?
|
||||||
|
|
||||||
remove_potential_friendship(other_account)
|
remove_potential_friendship(other_account)
|
||||||
|
|
||||||
rel
|
rel
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
|
||||||
reblogs = true if reblogs.nil?
|
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
|
||||||
|
|
||||||
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
|
||||||
.find_or_create_by!(target_account: other_account)
|
.find_or_create_by!(target_account: other_account)
|
||||||
|
|
||||||
rel.update!(show_reblogs: reblogs)
|
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||||
|
rel.notify = notify unless notify.nil?
|
||||||
|
|
||||||
|
rel.save! if rel.changed?
|
||||||
|
|
||||||
remove_potential_friendship(other_account)
|
remove_potential_friendship(other_account)
|
||||||
|
|
||||||
rel
|
rel
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# target_account_id :bigint(8) not null
|
# target_account_id :bigint(8) not null
|
||||||
# show_reblogs :boolean default(TRUE), not null
|
# show_reblogs :boolean default(TRUE), not null
|
||||||
# uri :string
|
# uri :string
|
||||||
|
# notify :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Follow < ApplicationRecord
|
class Follow < ApplicationRecord
|
||||||
|
@ -34,7 +35,7 @@ class Follow < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def revoke_request!
|
def revoke_request!
|
||||||
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri)
|
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, uri: uri)
|
||||||
destroy!
|
destroy!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# target_account_id :bigint(8) not null
|
# target_account_id :bigint(8) not null
|
||||||
# show_reblogs :boolean default(TRUE), not null
|
# show_reblogs :boolean default(TRUE), not null
|
||||||
# uri :string
|
# uri :string
|
||||||
|
# notify :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class FollowRequest < ApplicationRecord
|
class FollowRequest < ApplicationRecord
|
||||||
|
@ -28,7 +29,7 @@ class FollowRequest < ApplicationRecord
|
||||||
validates_with FollowLimitValidator, on: :create
|
validates_with FollowLimitValidator, on: :create
|
||||||
|
|
||||||
def authorize!
|
def authorize!
|
||||||
account.follow!(target_account, reblogs: show_reblogs, uri: uri)
|
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
|
||||||
MergeWorker.perform_async(target_account.id, account.id) if account.local?
|
MergeWorker.perform_async(target_account.id, account.id) if account.local?
|
||||||
destroy!
|
destroy!
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,21 +10,34 @@
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :bigint(8) not null
|
# account_id :bigint(8) not null
|
||||||
# from_account_id :bigint(8) not null
|
# from_account_id :bigint(8) not null
|
||||||
|
# type :string
|
||||||
#
|
#
|
||||||
|
|
||||||
class Notification < ApplicationRecord
|
class Notification < ApplicationRecord
|
||||||
|
self.inheritance_column = nil
|
||||||
|
|
||||||
include Paginable
|
include Paginable
|
||||||
include Cacheable
|
include Cacheable
|
||||||
|
|
||||||
TYPE_CLASS_MAP = {
|
LEGACY_TYPE_CLASS_MAP = {
|
||||||
mention: 'Mention',
|
'Mention' => :mention,
|
||||||
reblog: 'Status',
|
'Status' => :reblog,
|
||||||
follow: 'Follow',
|
'Follow' => :follow,
|
||||||
follow_request: 'FollowRequest',
|
'FollowRequest' => :follow_request,
|
||||||
favourite: 'Favourite',
|
'Favourite' => :favourite,
|
||||||
poll: 'Poll',
|
'Poll' => :poll,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
TYPES = %i(
|
||||||
|
mention
|
||||||
|
status
|
||||||
|
reblog
|
||||||
|
follow
|
||||||
|
follow_request
|
||||||
|
favourite
|
||||||
|
poll
|
||||||
|
).freeze
|
||||||
|
|
||||||
STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
|
STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
|
||||||
|
|
||||||
belongs_to :account, optional: true
|
belongs_to :account, optional: true
|
||||||
|
@ -38,29 +51,30 @@ class Notification < ApplicationRecord
|
||||||
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
||||||
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
|
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
|
||||||
|
|
||||||
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
|
validates :type, inclusion: { in: TYPES }
|
||||||
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
|
||||||
|
|
||||||
scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) }
|
scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) }
|
||||||
|
|
||||||
scope :browserable, ->(exclude_types = [], account_id = nil) {
|
scope :browserable, ->(exclude_types = [], account_id = nil) {
|
||||||
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
|
types = TYPES - exclude_types.map(&:to_sym)
|
||||||
|
|
||||||
if account_id.nil?
|
if account_id.nil?
|
||||||
where(activity_type: types)
|
where(type: types)
|
||||||
else
|
else
|
||||||
where(activity_type: types, from_account_id: account_id)
|
where(type: types, from_account_id: account_id)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
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]
|
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
|
def type
|
||||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
@type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
def target_status
|
def target_status
|
||||||
case type
|
case type
|
||||||
|
when :status
|
||||||
|
status
|
||||||
when :reblog
|
when :reblog
|
||||||
status&.reblog
|
status&.reblog
|
||||||
when :favourite
|
when :favourite
|
||||||
|
@ -89,10 +103,6 @@ class Notification < ApplicationRecord
|
||||||
item.target_status.account = accounts[item.target_status.account_id] if item.target_status
|
item.target_status.account = accounts[item.target_status.account_id] if item.target_status
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity_types_from_types(types)
|
|
||||||
types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
after_initialize :set_from_account
|
after_initialize :set_from_account
|
||||||
|
|
|
@ -11,6 +11,6 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_type?
|
def status_type?
|
||||||
[:favourite, :reblog, :mention, :poll].include?(object.type)
|
[:favourite, :reblog, :status, :mention, :poll].include?(object.type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::RelationshipSerializer < ActiveModel::Serializer
|
class REST::RelationshipSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :following, :showing_reblogs, :followed_by, :blocking, :blocked_by,
|
attributes :id, :following, :showing_reblogs, :notifying, :followed_by,
|
||||||
:muting, :muting_notifications, :requested, :domain_blocking,
|
:blocking, :blocked_by, :muting, :muting_notifications, :requested,
|
||||||
:endorsed, :note
|
:domain_blocking, :endorsed, :note
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
@ -19,6 +19,12 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notifying
|
||||||
|
(instance_options[:relationships].following[object.id] || {})[:notify] ||
|
||||||
|
(instance_options[:relationships].requested[object.id] || {})[:notify] ||
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def followed_by
|
def followed_by
|
||||||
instance_options[:relationships].followed_by[object.id] || false
|
instance_options[:relationships].followed_by[object.id] || false
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ class FavouriteService < BaseService
|
||||||
status = favourite.status
|
status = favourite.status
|
||||||
|
|
||||||
if status.account.local?
|
if status.account.local?
|
||||||
NotifyService.new.call(status.account, favourite)
|
NotifyService.new.call(status.account, :favourite, favourite)
|
||||||
elsif status.account.activitypub?
|
elsif status.account.activitypub?
|
||||||
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,12 +9,13 @@ class FollowService < BaseService
|
||||||
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
||||||
# @param [Hash] options
|
# @param [Hash] options
|
||||||
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
|
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
|
||||||
|
# @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
|
||||||
# @option [Boolean] :bypass_locked
|
# @option [Boolean] :bypass_locked
|
||||||
# @option [Boolean] :with_rate_limit
|
# @option [Boolean] :with_rate_limit
|
||||||
def call(source_account, target_account, options = {})
|
def call(source_account, target_account, options = {})
|
||||||
@source_account = source_account
|
@source_account = source_account
|
||||||
@target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
@target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
||||||
@options = { reblogs: true, bypass_locked: false, with_rate_limit: false }.merge(options)
|
@options = { bypass_locked: false, with_rate_limit: false }.merge(options)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if following_not_possible?
|
raise ActiveRecord::RecordNotFound if following_not_possible?
|
||||||
raise Mastodon::NotPermittedError if following_not_allowed?
|
raise Mastodon::NotPermittedError if following_not_allowed?
|
||||||
|
@ -45,18 +46,18 @@ class FollowService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_follow_options!
|
def change_follow_options!
|
||||||
@source_account.follow!(@target_account, reblogs: @options[:reblogs])
|
@source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_follow_request_options!
|
def change_follow_request_options!
|
||||||
@source_account.request_follow!(@target_account, reblogs: @options[:reblogs])
|
@source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_follow!
|
def request_follow!
|
||||||
follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
|
follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit])
|
||||||
|
|
||||||
if @target_account.local?
|
if @target_account.local?
|
||||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name)
|
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, :follow_request)
|
||||||
elsif @target_account.activitypub?
|
elsif @target_account.activitypub?
|
||||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
|
||||||
end
|
end
|
||||||
|
@ -65,9 +66,9 @@ class FollowService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def direct_follow!
|
def direct_follow!
|
||||||
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
|
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit])
|
||||||
|
|
||||||
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name)
|
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow)
|
||||||
MergeWorker.perform_async(@target_account.id, @source_account.id)
|
MergeWorker.perform_async(@target_account.id, @source_account.id)
|
||||||
|
|
||||||
follow
|
follow
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ImportService < BaseService
|
||||||
|
|
||||||
def import_follows!
|
def import_follows!
|
||||||
parse_import_data!(['Account address'])
|
parse_import_data!(['Account address'])
|
||||||
import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: 'Show boosts')
|
import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: { header: 'Show boosts', default: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_blocks!
|
def import_blocks!
|
||||||
|
@ -35,7 +35,7 @@ class ImportService < BaseService
|
||||||
|
|
||||||
def import_mutes!
|
def import_mutes!
|
||||||
parse_import_data!(['Account address'])
|
parse_import_data!(['Account address'])
|
||||||
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: 'Hide notifications')
|
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_domain_blocks!
|
def import_domain_blocks!
|
||||||
|
@ -65,7 +65,7 @@ class ImportService < BaseService
|
||||||
|
|
||||||
def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
|
def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
|
||||||
local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
|
local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
|
||||||
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? }
|
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }]] }.reject { |(id, _)| id.blank? }
|
||||||
|
|
||||||
if @import.overwrite?
|
if @import.overwrite?
|
||||||
presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
|
presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class NotifyService < BaseService
|
class NotifyService < BaseService
|
||||||
def call(recipient, activity)
|
def call(recipient, type, activity)
|
||||||
@recipient = recipient
|
@recipient = recipient
|
||||||
@activity = activity
|
@activity = activity
|
||||||
@notification = Notification.new(account: @recipient, activity: @activity)
|
@notification = Notification.new(account: @recipient, type: type, activity: @activity)
|
||||||
|
|
||||||
return if recipient.user.nil? || blocked?
|
return if recipient.user.nil? || blocked?
|
||||||
|
|
||||||
|
@ -22,6 +22,10 @@ class NotifyService < BaseService
|
||||||
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
|
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def blocked_status?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def blocked_favourite?
|
def blocked_favourite?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,7 +58,7 @@ class ProcessMentionsService < BaseService
|
||||||
mentioned_account = mention.account
|
mentioned_account = mention.account
|
||||||
|
|
||||||
if mentioned_account.local?
|
if mentioned_account.local?
|
||||||
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
|
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name, :mention)
|
||||||
elsif mentioned_account.activitypub?
|
elsif mentioned_account.activitypub?
|
||||||
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,7 +45,7 @@ class ReblogService < BaseService
|
||||||
reblogged_status = reblog.reblog
|
reblogged_status = reblog.reblog
|
||||||
|
|
||||||
if reblogged_status.account.local?
|
if reblogged_status.account.local?
|
||||||
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name)
|
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, :reblog)
|
||||||
elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account)
|
elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account)
|
||||||
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,10 @@ class FeedInsertWorker
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_and_insert
|
def check_and_insert
|
||||||
perform_push unless feed_filtered?
|
return if feed_filtered?
|
||||||
|
|
||||||
|
perform_push
|
||||||
|
perform_notify if notify?
|
||||||
end
|
end
|
||||||
|
|
||||||
def feed_filtered?
|
def feed_filtered?
|
||||||
|
@ -35,6 +38,12 @@ class FeedInsertWorker
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notify?
|
||||||
|
return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id)
|
||||||
|
|
||||||
|
Follow.find_by(account: @follower, target_account: @status.account)&.notify?
|
||||||
|
end
|
||||||
|
|
||||||
def perform_push
|
def perform_push
|
||||||
case @type
|
case @type
|
||||||
when :home
|
when :home
|
||||||
|
@ -43,4 +52,8 @@ class FeedInsertWorker
|
||||||
FeedManager.instance.push_to_list(@list, @status)
|
FeedManager.instance.push_to_list(@list, @status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def perform_notify
|
||||||
|
NotifyService.new.call(@follower, :status, @status)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class LocalNotificationWorker
|
class LocalNotificationWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(receiver_account_id, activity_id = nil, activity_class_name = nil)
|
def perform(receiver_account_id, activity_id = nil, activity_class_name = nil, type = nil)
|
||||||
if activity_id.nil? && activity_class_name.nil?
|
if activity_id.nil? && activity_class_name.nil?
|
||||||
activity = Mention.find(receiver_account_id)
|
activity = Mention.find(receiver_account_id)
|
||||||
receiver = activity.account
|
receiver = activity.account
|
||||||
|
@ -12,7 +12,7 @@ class LocalNotificationWorker
|
||||||
activity = activity_class_name.constantize.find(activity_id)
|
activity = activity_class_name.constantize.find(activity_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
NotifyService.new.call(receiver, activity)
|
NotifyService.new.call(receiver, type || activity_class_name.underscore, activity)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,12 +11,12 @@ class PollExpirationNotifyWorker
|
||||||
# Notify poll owner and remote voters
|
# Notify poll owner and remote voters
|
||||||
if poll.local?
|
if poll.local?
|
||||||
ActivityPub::DistributePollUpdateWorker.perform_async(poll.status.id)
|
ActivityPub::DistributePollUpdateWorker.perform_async(poll.status.id)
|
||||||
NotifyService.new.call(poll.account, poll)
|
NotifyService.new.call(poll.account, :poll, poll)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Notify local voters
|
# Notify local voters
|
||||||
poll.votes.includes(:account).map(&:account).select(&:local?).each do |account|
|
poll.votes.includes(:account).map(&:account).select(&:local?).each do |account|
|
||||||
NotifyService.new.call(account, poll)
|
NotifyService.new.call(account, :poll, poll)
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
|
|
|
@ -11,6 +11,7 @@ class RefollowWorker
|
||||||
|
|
||||||
target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow|
|
target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow|
|
||||||
reblogs = follow.show_reblogs?
|
reblogs = follow.show_reblogs?
|
||||||
|
notify = follow.notify?
|
||||||
|
|
||||||
# Locally unfollow remote account
|
# Locally unfollow remote account
|
||||||
follower = follow.account
|
follower = follow.account
|
||||||
|
@ -18,7 +19,7 @@ class RefollowWorker
|
||||||
|
|
||||||
# Schedule re-follow
|
# Schedule re-follow
|
||||||
begin
|
begin
|
||||||
FollowService.new.call(follower, target_account, reblogs: reblogs)
|
FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify)
|
||||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
|
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,8 +12,9 @@ class UnfollowFollowWorker
|
||||||
|
|
||||||
follow = follower_account.active_relationships.find_by(target_account: old_target_account)
|
follow = follower_account.active_relationships.find_by(target_account: old_target_account)
|
||||||
reblogs = follow&.show_reblogs?
|
reblogs = follow&.show_reblogs?
|
||||||
|
notify = follow&.notify?
|
||||||
|
|
||||||
FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, bypass_locked: bypass_locked)
|
FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked)
|
||||||
UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
|
UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
|
||||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
true
|
true
|
||||||
|
|
19
db/migrate/20200917192924_add_notify_to_follows.rb
Normal file
19
db/migrate/20200917192924_add_notify_to_follows.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class AddNotifyToFollows < ActiveRecord::Migration[5.1]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :follows, :notify, :boolean, default: false, allow_null: false
|
||||||
|
add_column_with_default :follow_requests, :notify, :boolean, default: false, allow_null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :follows, :notify
|
||||||
|
remove_column :follow_requests, :notify
|
||||||
|
end
|
||||||
|
end
|
5
db/migrate/20200917193034_add_type_to_notifications.rb
Normal file
5
db/migrate/20200917193034_add_type_to_notifications.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class AddTypeToNotifications < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :notifications, :type, :string
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
class AddIndexNotificationsOnType < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_index :notifications, [:account_id, :id, :type], order: { id: :desc }, algorithm: :concurrently
|
||||||
|
end
|
||||||
|
end
|
22
db/post_migrate/20200917193528_migrate_notifications_type.rb
Normal file
22
db/post_migrate/20200917193528_migrate_notifications_type.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MigrateNotificationsType < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
TYPES_TO_MIGRATE = {
|
||||||
|
'Mention' => :mention,
|
||||||
|
'Status' => :reblog,
|
||||||
|
'Follow' => :follow,
|
||||||
|
'FollowRequest' => :follow_request,
|
||||||
|
'Favourite' => :favourite,
|
||||||
|
'Poll' => :poll,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def up
|
||||||
|
TYPES_TO_MIGRATE.each_pair do |activity_type, type|
|
||||||
|
Notification.where(activity_type: activity_type, type: nil).in_batches.update_all(type: type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down; end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveIndexNotificationsOnAccountActivity < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
remove_index :notifications, name: :account_activity
|
||||||
|
remove_index :notifications, name: :index_notifications_on_account_id_and_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_index :notifications, [:account_id, :activity_id, :activity_type], unique: true, name: 'account_activity', algorithm: :concurrently
|
||||||
|
add_index :notifications, [:account_id, :id], order: { id: :desc }, algorithm: :concurrently
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2020_09_08_193330) do
|
ActiveRecord::Schema.define(version: 2020_09_17_222734) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -411,6 +411,7 @@ ActiveRecord::Schema.define(version: 2020_09_08_193330) do
|
||||||
t.bigint "target_account_id", null: false
|
t.bigint "target_account_id", null: false
|
||||||
t.boolean "show_reblogs", default: true, null: false
|
t.boolean "show_reblogs", default: true, null: false
|
||||||
t.string "uri"
|
t.string "uri"
|
||||||
|
t.boolean "notify", default: false, null: false
|
||||||
t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true
|
t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -421,6 +422,7 @@ ActiveRecord::Schema.define(version: 2020_09_08_193330) do
|
||||||
t.bigint "target_account_id", null: false
|
t.bigint "target_account_id", null: false
|
||||||
t.boolean "show_reblogs", default: true, null: false
|
t.boolean "show_reblogs", default: true, null: false
|
||||||
t.string "uri"
|
t.string "uri"
|
||||||
|
t.boolean "notify", default: false, null: false
|
||||||
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
|
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
|
||||||
t.index ["target_account_id"], name: "index_follows_on_target_account_id"
|
t.index ["target_account_id"], name: "index_follows_on_target_account_id"
|
||||||
end
|
end
|
||||||
|
@ -545,8 +547,8 @@ ActiveRecord::Schema.define(version: 2020_09_08_193330) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.bigint "from_account_id", null: false
|
t.bigint "from_account_id", null: false
|
||||||
t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true
|
t.string "type"
|
||||||
t.index ["account_id", "id"], name: "index_notifications_on_account_id_and_id", order: { id: :desc }
|
t.index ["account_id", "id", "type"], name: "index_notifications_on_account_id_and_id_and_type", order: { id: :desc }
|
||||||
t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type"
|
t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type"
|
||||||
t.index ["from_account_id"], name: "index_notifications_on_from_account_id"
|
t.index ["from_account_id"], name: "index_notifications_on_from_account_id"
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,6 +71,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
|
||||||
let(:scopes) { 'write:follows' }
|
let(:scopes) { 'write:follows' }
|
||||||
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account }
|
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account }
|
||||||
|
|
||||||
|
context do
|
||||||
before do
|
before do
|
||||||
post :follow, params: { id: other_account.id }
|
post :follow, params: { id: other_account.id }
|
||||||
end
|
end
|
||||||
|
@ -118,6 +119,35 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'modifying follow options' do
|
||||||
|
let(:locked) { false }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.follow!(other_account, reblogs: false, notify: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes reblogs option' do
|
||||||
|
post :follow, params: { id: other_account.id, reblogs: true }
|
||||||
|
|
||||||
|
json = body_as_json
|
||||||
|
|
||||||
|
expect(json[:following]).to be true
|
||||||
|
expect(json[:showing_reblogs]).to be true
|
||||||
|
expect(json[:notifying]).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes notify option' do
|
||||||
|
post :follow, params: { id: other_account.id, notify: true }
|
||||||
|
|
||||||
|
json = body_as_json
|
||||||
|
|
||||||
|
expect(json[:following]).to be true
|
||||||
|
expect(json[:showing_reblogs]).to be false
|
||||||
|
expect(json[:notifying]).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'POST #unfollow' do
|
describe 'POST #unfollow' do
|
||||||
let(:scopes) { 'write:follows' }
|
let(:scopes) { 'write:follows' }
|
||||||
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||||
|
|
|
@ -14,7 +14,7 @@ describe AccountInteractions do
|
||||||
context 'account with Follow' do
|
context 'account with Follow' do
|
||||||
it 'returns { target_account_id => true }' do
|
it 'returns { target_account_id => true }' do
|
||||||
Fabricate(:follow, account: account, target_account: target_account)
|
Fabricate(:follow, account: account, target_account: target_account)
|
||||||
is_expected.to eq(target_account_id => { reblogs: true })
|
is_expected.to eq(target_account_id => { reblogs: true, notify: false })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe FollowRequest, type: :model do
|
||||||
let(:target_account) { Fabricate(:account) }
|
let(:target_account) { Fabricate(:account) }
|
||||||
|
|
||||||
it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do
|
it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do
|
||||||
expect(account).to receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri)
|
expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri)
|
||||||
expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id)
|
expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id)
|
||||||
expect(follow_request).to receive(:destroy!)
|
expect(follow_request).to receive(:destroy!)
|
||||||
follow_request.authorize!
|
follow_request.authorize!
|
||||||
|
|
|
@ -95,6 +95,7 @@ RSpec.describe ImportService, type: :service do
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||||
it 'follows the listed accounts, including boosts' do
|
it 'follows the listed accounts, including boosts' do
|
||||||
subject.call(import)
|
subject.call(import)
|
||||||
|
|
||||||
expect(account.following.count).to eq 1
|
expect(account.following.count).to eq 1
|
||||||
expect(account.follow_requests.count).to eq 1
|
expect(account.follow_requests.count).to eq 1
|
||||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||||
|
|
|
@ -2,13 +2,14 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe NotifyService, type: :service do
|
RSpec.describe NotifyService, type: :service do
|
||||||
subject do
|
subject do
|
||||||
-> { described_class.new.call(recipient, activity) }
|
-> { described_class.new.call(recipient, type, activity) }
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:recipient) { user.account }
|
let(:recipient) { user.account }
|
||||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||||
let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
|
let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
|
||||||
|
let(:type) { :follow }
|
||||||
|
|
||||||
it { is_expected.to change(Notification, :count).by(1) }
|
it { is_expected.to change(Notification, :count).by(1) }
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ RSpec.describe NotifyService, type: :service do
|
||||||
|
|
||||||
context 'for direct messages' do
|
context 'for direct messages' do
|
||||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) }
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) }
|
||||||
|
let(:type) { :mention }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled)
|
user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled)
|
||||||
|
@ -93,6 +95,7 @@ RSpec.describe NotifyService, type: :service do
|
||||||
describe 'reblogs' do
|
describe 'reblogs' do
|
||||||
let(:status) { Fabricate(:status, account: Fabricate(:account)) }
|
let(:status) { Fabricate(:status, account: Fabricate(:account)) }
|
||||||
let(:activity) { Fabricate(:status, account: sender, reblog: status) }
|
let(:activity) { Fabricate(:status, account: sender, reblog: status) }
|
||||||
|
let(:type) { :reblog }
|
||||||
|
|
||||||
it 'shows reblogs by default' do
|
it 'shows reblogs by default' do
|
||||||
recipient.follow!(sender)
|
recipient.follow!(sender)
|
||||||
|
@ -114,6 +117,7 @@ RSpec.describe NotifyService, type: :service do
|
||||||
let(:asshole) { Fabricate(:account, username: 'asshole') }
|
let(:asshole) { Fabricate(:account, username: 'asshole') }
|
||||||
let(:reply_to) { Fabricate(:status, account: asshole) }
|
let(:reply_to) { Fabricate(:status, account: asshole) }
|
||||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) }
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) }
|
||||||
|
let(:type) { :mention }
|
||||||
|
|
||||||
it 'does not notify when conversation is muted' do
|
it 'does not notify when conversation is muted' do
|
||||||
recipient.mute_conversation!(activity.status.conversation)
|
recipient.mute_conversation!(activity.status.conversation)
|
||||||
|
|
|
@ -23,8 +23,8 @@ describe RefollowWorker do
|
||||||
result = subject.perform(account.id)
|
result = subject.perform(account.id)
|
||||||
|
|
||||||
expect(result).to be_nil
|
expect(result).to be_nil
|
||||||
expect(service).to have_received(:call).with(alice, account, reblogs: true)
|
expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false)
|
||||||
expect(service).to have_received(:call).with(bob, account, reblogs: false)
|
expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue