Skip to content
This repository has been archived by the owner on Dec 21, 2023. It is now read-only.

Commit

Permalink
Add option to be notified when a followed user posts (mastodon#13546)
Browse files Browse the repository at this point in the history
* Add bell button

Fix mastodon#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
  • Loading branch information
Gargron authored Sep 18, 2020
1 parent 75e4bd9 commit 974b1b7
Show file tree
Hide file tree
Showing 42 changed files with 324 additions and 106 deletions.
5 changes: 2 additions & 3 deletions app/controllers/api/v1/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ def create
end

def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)

options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
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 } }

render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/follow_requests_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def index

def authorize
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
end

Expand Down
4 changes: 2 additions & 2 deletions app/javascript/mastodon/actions/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
const locked = getState().getIn(['accounts', id, 'locked'], false);

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));
}).catch(error => {
dispatch(followAccountFail(error, locked));
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/actions/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {

let filtered = false;

if (notification.type === 'mention') {
if (['mention', 'status'].includes(notification.type)) {
const dropRegex = filters[0];
const regex = filters[1];
const searchIndex = searchTextFromRawStatus(notification.status);
Expand Down
12 changes: 11 additions & 1 deletion app/javascript/mastodon/features/account/components/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
import Avatar from 'mastodon/components/avatar';
import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
Expand Down Expand Up @@ -35,6 +36,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide 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' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
Expand Down Expand Up @@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent {
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onNotifyToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
Expand Down Expand Up @@ -144,6 +148,7 @@ class Header extends ImmutablePureComponent {

let info = [];
let actionBtn = '';
let bellBtn = '';
let lockedIcon = '';
let menu = [];

Expand Down Expand Up @@ -173,6 +178,10 @@ class Header extends ImmutablePureComponent {
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'])) {
actionBtn = '';
}
Expand Down Expand Up @@ -287,6 +296,7 @@ class Header extends ImmutablePureComponent {
{!suspended && (
<div className='account__header__tabs__buttons'>
{actionBtn}
{bellBtn}

<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onReblogToggle(this.props.account);
}

handleNotifyToggle = () => {
this.props.onNotifyToggle(this.props.account);
}

handleMute = () => {
this.props.onMute(this.props.account);
}
Expand Down Expand Up @@ -106,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
onMention={this.handleMention}
onDirect={this.handleDirect}
onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({

onReblogToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), false));
dispatch(followAccount(account.get('id'), { reblogs: false }));
} else {
dispatch(followAccount(account.get('id'), true));
dispatch(followAccount(account.get('id'), { reblogs: true }));
}
},

Expand All @@ -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) {
dispatch(initReport(account));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const tooltips = defineMessages({
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
});

export default @injectIntl
Expand Down Expand Up @@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
>
<Icon id='tasks' fixedWidth />
</button>
<button
className={selectedFilter === 'status' ? 'active' : ''}
onClick={this.onClick('status')}
title={intl.formatMessage(tooltips.statuses)}
>
<Icon id='home' fixedWidth />
</button>
<button
className={selectedFilter === 'follow' ? 'active' : ''}
onClick={this.onClick('follow')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const messages = defineMessages({
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
});

const notificationForScreenReader = (intl, message, timestamp) => {
Expand Down Expand Up @@ -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) {
const { intl } = this.props;
const ownPoll = me === account.get('id');
Expand Down Expand Up @@ -292,6 +325,8 @@ class Notification extends ImmutablePureComponent {
return this.renderFavourite(notification, link);
case 'reblog':
return this.renderReblog(notification, link);
case 'status':
return this.renderStatus(notification, link);
case 'poll':
return this.renderPoll(notification, account);
}
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/styles/mastodon/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6502,6 +6502,10 @@ noscript {
padding: 2px;
}

& > .icon-button {
margin-right: 8px;
}

.button {
margin: 0 8px;
}
Expand Down
4 changes: 2 additions & 2 deletions app/lib/activitypub/activity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ def reblog_by_following_group_account?(status)
end

def notify_about_reblog(status)
NotifyService.new.call(status.reblog.account, status)
NotifyService.new.call(status.reblog.account, :reblog, status)
end

def notify_about_mentions(status)
status.active_mentions.includes(:account).each do |mention|
next unless mention.account.local? && audience_includes?(mention.account)
NotifyService.new.call(mention.account, mention)
NotifyService.new.call(mention.account, :mention, mention)
end
end

Expand Down
4 changes: 2 additions & 2 deletions app/lib/activitypub/activity/follow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ def perform
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])

if target_account.locked? || @account.silenced?
NotifyService.new.call(target_account, follow_request)
NotifyService.new.call(target_account, :follow_request, follow_request)
else
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

Expand Down
2 changes: 1 addition & 1 deletion app/lib/activitypub/activity/like.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ def perform
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)
NotifyService.new.call(original_status.account, favourite)
NotifyService.new.call(original_status.account, :favourite, favourite)
end
end
26 changes: 16 additions & 10 deletions app/models/concerns/account_interactions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def following_map(target_account_ids, account_id)
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
mapping[follow.target_account_id] = {
reblogs: follow.show_reblogs?,
notify: follow.notify?,
}
end
end
Expand Down Expand Up @@ -36,6 +37,7 @@ def requested_map(target_account_ids, account_id)
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] = {
reblogs: follow_request.show_reblogs?,
notify: follow_request.notify?,
}
end
end
Expand Down Expand Up @@ -95,25 +97,29 @@ def follow_mapping(query, field)
has_many :announcement_mutes, dependent: :destroy
end

def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?

rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
.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)

rel
end

def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?

rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
.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)

rel
Expand Down
3 changes: 2 additions & 1 deletion app/models/follow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null
# uri :string
# notify :boolean default(FALSE), not null
#

class Follow < ApplicationRecord
Expand All @@ -34,7 +35,7 @@ def local?
end

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!
end

Expand Down
3 changes: 2 additions & 1 deletion app/models/follow_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null
# uri :string
# notify :boolean default(FALSE), not null
#

class FollowRequest < ApplicationRecord
Expand All @@ -28,7 +29,7 @@ class FollowRequest < ApplicationRecord
validates_with FollowLimitValidator, on: :create

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?
destroy!
end
Expand Down
Loading

0 comments on commit 974b1b7

Please sign in to comment.