about summary refs log tree commit diff
path: root/app/javascript/mastodon/features
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 /app/javascript/mastodon/features
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
Diffstat (limited to 'app/javascript/mastodon/features')
-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
5 files changed, 69 insertions, 3 deletions
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 02217b62c..2b97af4e6 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 abb15edcc..6b52defe4 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 8728b4806..e12019547 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 2fd28d832..368eb0b7e 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 74065e5e2..62a97f187 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);
     }