summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2020-09-18 17:26:45 +0200
committerGitHub <noreply@github.com>2020-09-18 17:26:45 +0200
commit974b1b79ce58e6799e5e5bb576e630ca783150de (patch)
tree93dfcb52fc58d714b3a9bd454f7589fe98c1d1ae
parent75e4bd9413143ee208d00814c728fc2bf0c58cf2 (diff)
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
-rw-r--r--app/controllers/api/v1/accounts_controller.rb5
-rw-r--r--app/controllers/api/v1/follow_requests_controller.rb2
-rw-r--r--app/javascript/mastodon/actions/accounts.js4
-rw-r--r--app/javascript/mastodon/actions/notifications.js2
-rw-r--r--app/javascript/mastodon/features/account/components/header.js12
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/header.js5
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js12
-rw-r--r--app/javascript/mastodon/features/notifications/components/filter_bar.js8
-rw-r--r--app/javascript/mastodon/features/notifications/components/notification.js35
-rw-r--r--app/javascript/styles/mastodon/components.scss4
-rw-r--r--app/lib/activitypub/activity.rb4
-rw-r--r--app/lib/activitypub/activity/follow.rb4
-rw-r--r--app/lib/activitypub/activity/like.rb2
-rw-r--r--app/models/concerns/account_interactions.rb26
-rw-r--r--app/models/follow.rb3
-rw-r--r--app/models/follow_request.rb3
-rw-r--r--app/models/notification.rb44
-rw-r--r--app/serializers/rest/notification_serializer.rb2
-rw-r--r--app/serializers/rest/relationship_serializer.rb12
-rw-r--r--app/services/favourite_service.rb2
-rw-r--r--app/services/follow_service.rb15
-rw-r--r--app/services/import_service.rb6
-rw-r--r--app/services/notify_service.rb8
-rw-r--r--app/services/process_mentions_service.rb2
-rw-r--r--app/services/reblog_service.rb2
-rw-r--r--app/workers/feed_insert_worker.rb15
-rw-r--r--app/workers/local_notification_worker.rb4
-rw-r--r--app/workers/poll_expiration_notify_worker.rb4
-rw-r--r--app/workers/refollow_worker.rb3
-rw-r--r--app/workers/unfollow_follow_worker.rb5
-rw-r--r--db/migrate/20200917192924_add_notify_to_follows.rb19
-rw-r--r--db/migrate/20200917193034_add_type_to_notifications.rb5
-rw-r--r--db/migrate/20200917222316_add_index_notifications_on_type.rb7
-rw-r--r--db/post_migrate/20200917193528_migrate_notifications_type.rb22
-rw-r--r--db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb15
-rw-r--r--db/schema.rb8
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb84
-rw-r--r--spec/models/concerns/account_interactions_spec.rb2
-rw-r--r--spec/models/follow_request_spec.rb2
-rw-r--r--spec/services/import_service_spec.rb1
-rw-r--r--spec/services/notify_service_spec.rb6
-rw-r--r--spec/workers/refollow_worker_spec.rb4
42 files changed, 324 insertions, 106 deletions
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 61dcb87c23d..aef51a6479a 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -30,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController
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
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index 0420b7bef92..b34c76f29e9 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
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
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index d28f7dad88b..723c04e554d 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -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));
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index a26844f8482..099e42f6c54 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -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);
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 02217b62c52..2b97af4e675 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -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';
@@ -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' },
@@ -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,
@@ -144,6 +148,7 @@ class Header extends ImmutablePureComponent {
let info = [];
let actionBtn = '';
+ let bellBtn = '';
let lockedIcon = '';
let menu = [];
@@ -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 = '';
}
@@ -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>
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index abb15edcc7e..6b52defe4a0 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -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);
}
@@ -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}
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index 8728b48068a..e12019547ec 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -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 }));
}
},
@@ -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));
},
diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.js b/app/javascript/mastodon/features/notifications/components/filter_bar.js
index 2fd28d83267..368eb0b7e6c 100644
--- a/app/javascript/mastodon/features/notifications/components/filter_bar.js
+++ b/app/javascript/mastodon/features/notifications/components/filter_bar.js
@@ -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
@@ -88,6 +89,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')}
title={intl.formatMessage(tooltips.follows)}
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js
index 74065e5e268..62a97f187c6 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.js
+++ b/app/javascript/mastodon/features/notifications/components/notification.js
@@ -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) => {
@@ -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');
@@ -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);
}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index cfcd937fabf..7defa0d162c 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6502,6 +6502,10 @@ noscript {
padding: 2px;
}
+ & > .icon-button {
+ margin-right: 8px;
+ }
+
.button {
margin: 0 8px;
}
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 94aee7939fc..224451f4173 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -118,13 +118,13 @@ class ActivityPub::Activity
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
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
index ec92f4255f6..0beec68ab38 100644
--- a/app/lib/activitypub/activity/follow.rb
+++ b/app/lib/activitypub/activity/follow.rb
@@ -22,10 +22,10 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
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
diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb
index 674d5fe4723..c065f01f860 100644
--- a/app/lib/activitypub/activity/like.rb
+++ b/app/lib/activitypub/activity/like.rb
@@ -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)
favourite = original_status.favourites.create!(account: @account)
- NotifyService.new.call(original_status.account, favourite)
+ NotifyService.new.call(original_status.account, :favourite, favourite)
end
end
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index be7211f2cad..427ebdae29c 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -8,6 +8,7 @@ module AccountInteractions
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
@@ -36,6 +37,7 @@ module Account