about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/components/components/column_collapsable.jsx5
-rw-r--r--app/assets/javascripts/components/components/loading_indicator.jsx3
-rw-r--r--app/assets/javascripts/components/components/media_gallery.jsx6
-rw-r--r--app/assets/javascripts/components/components/video_player.jsx6
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx2
-rw-r--r--app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx7
-rw-r--r--app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx8
-rw-r--r--app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx3
-rw-r--r--app/assets/javascripts/components/features/notifications/components/column_settings.jsx12
-rw-r--r--app/assets/javascripts/components/features/notifications/components/notification.jsx20
-rw-r--r--app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx5
-rw-r--r--app/assets/javascripts/components/features/status/components/card.jsx43
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx4
-rw-r--r--app/assets/javascripts/components/features/ui/components/column_link.jsx1
-rw-r--r--app/assets/javascripts/components/features/ui/containers/modal_container.jsx9
-rw-r--r--app/assets/stylesheets/application.scss29
-rw-r--r--app/assets/stylesheets/components.scss135
-rw-r--r--app/assets/stylesheets/stream_entries.scss8
-rw-r--r--app/controllers/api/v1/follow_requests_controller.rb4
-rw-r--r--app/controllers/concerns/obfuscate_filename.rb1
-rw-r--r--app/helpers/atom_builder_helper.rb8
-rw-r--r--app/lib/feed_manager.rb1
-rw-r--r--app/lib/tag_manager.rb22
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/favourite.rb4
-rw-r--r--app/models/follow_request.rb36
-rw-r--r--app/models/status.rb10
-rw-r--r--app/models/stream_entry.rb2
-rw-r--r--app/services/authorize_follow_service.rb11
-rw-r--r--app/services/block_service.rb4
-rw-r--r--app/services/concerns/stream_entry_renderer.rb8
-rw-r--r--app/services/favourite_service.rb4
-rw-r--r--app/services/fetch_remote_account_service.rb4
-rw-r--r--app/services/follow_service.rb17
-rw-r--r--app/services/process_feed_service.rb18
-rw-r--r--app/services/process_interaction_service.rb22
-rw-r--r--app/services/process_mentions_service.rb6
-rw-r--r--app/services/reblog_service.rb10
-rw-r--r--app/services/reject_follow_service.rb11
-rw-r--r--app/services/remove_status_service.rb4
-rw-r--r--app/services/send_interaction_service.rb19
-rw-r--r--app/services/unblock_service.rb4
-rw-r--r--app/services/unfavourite_service.rb4
-rw-r--r--app/services/unfollow_service.rb4
-rw-r--r--app/services/update_remote_profile_service.rb1
-rw-r--r--app/views/about/index.html.haml1
-rw-r--r--app/views/admin/settings/index.html.haml6
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/tags/show.html.haml14
-rw-r--r--app/workers/after_remote_follow_request_worker.rb17
-rw-r--r--app/workers/after_remote_follow_worker.rb17
-rw-r--r--app/workers/notification_worker.rb4
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb9
-rw-r--r--app/workers/push_notification_worker.rb11
54 files changed, 432 insertions, 198 deletions
diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx
index 90c561bce..676759055 100644
--- a/app/assets/javascripts/components/components/column_collapsable.jsx
+++ b/app/assets/javascripts/components/components/column_collapsable.jsx
@@ -40,10 +40,11 @@ const ColumnCollapsable = React.createClass({
   render () {
     const { icon, fullHeight, children } = this.props;
     const { collapsed } = this.state;
-
+    const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
+    
     return (
       <div style={{ position: 'relative' }}>
-        <div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
+        <div style={{...iconStyle }} className={collapsedClassName} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
 
         <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
           {({ opacity, height }) =>
diff --git a/app/assets/javascripts/components/components/loading_indicator.jsx b/app/assets/javascripts/components/components/loading_indicator.jsx
index c8a263924..913a4bf99 100644
--- a/app/assets/javascripts/components/components/loading_indicator.jsx
+++ b/app/assets/javascripts/components/components/loading_indicator.jsx
@@ -4,12 +4,11 @@ const style = {
   textAlign: 'center',
   fontSize: '16px',
   fontWeight: '500',
-  color: '#616b86',
   paddingTop: '120px'
 };
 
 const LoadingIndicator = () => (
-  <div style={style}>
+  <div className='loading-indicator' style={style}>
     <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
   </div>
 );
diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx
index a13448d0b..b0e397e80 100644
--- a/app/assets/javascripts/components/components/media_gallery.jsx
+++ b/app/assets/javascripts/components/components/media_gallery.jsx
@@ -16,8 +16,6 @@ const outerStyle = {
 };
 
 const spoilerStyle = {
-  background: '#000',
-  color: '#fff',
   textAlign: 'center',
   height: '100%',
   cursor: 'pointer',
@@ -84,14 +82,14 @@ const MediaGallery = React.createClass({
     if (!this.state.visible) {
       if (sensitive) {
         children = (
-          <div style={spoilerStyle} onClick={this.handleOpen}>
+          <div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
             <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
             <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
           </div>
         );
       } else {
         children = (
-          <div style={spoilerStyle} onClick={this.handleOpen}>
+          <div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
             <span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
             <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
           </div>
diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx
index 3edc8f672..ccd67ddf0 100644
--- a/app/assets/javascripts/components/components/video_player.jsx
+++ b/app/assets/javascripts/components/components/video_player.jsx
@@ -28,8 +28,6 @@ const muteStyle = {
 
 const spoilerStyle = {
   marginTop: '8px',
-  background: '#000',
-  color: '#fff',
   textAlign: 'center',
   height: '100%',
   cursor: 'pointer',
@@ -122,7 +120,7 @@ const VideoPlayer = React.createClass({
     if (!this.state.visible) {
       if (sensitive) {
         return (
-          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleVisibility}>
+          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
             {spoilerButton}
             <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
             <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@@ -130,7 +128,7 @@ const VideoPlayer = React.createClass({
         );
       } else {
         return (
-          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleOpen}>
+          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleOpen}>
             {spoilerButton}
             <span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
             <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx
index b2d943c1c..a4f0ca768 100644
--- a/app/assets/javascripts/components/features/account/components/header.jsx
+++ b/app/assets/javascripts/components/features/account/components/header.jsx
@@ -35,7 +35,7 @@ const Header = React.createClass({
     }
 
     if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
-      info = <span style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', color: '#fff', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
+      info = <span className='account--follows-info' style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
     }
 
     if (me !== account.get('id')) {
diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
index 0d41d192f..1766655c2 100644
--- a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
+++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
@@ -16,11 +16,8 @@ const outerStyle = {
 };
 
 const panelStyle = {
-  background: '#2f3441',
   display: 'flex',
   flexDirection: 'row',
-  borderTop: '1px solid #363c4b',
-  borderBottom: '1px solid #363c4b',
   padding: '10px 0'
 };
 
@@ -40,10 +37,10 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
           <DisplayName account={account} />
         </Permalink>
 
-        <div style={{ color: '#616b86', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
+        <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
       </div>
 
-      <div style={panelStyle}>
+      <div className='account--panel' style={panelStyle}>
         <div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
         <div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
       </div>
diff --git a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
index 714be309b..3317210bf 100644
--- a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
+++ b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
@@ -10,7 +10,6 @@ const messages = defineMessages({
 });
 
 const outerStyle = {
-  background: '#373b4a',
   padding: '15px'
 };
 
@@ -18,7 +17,6 @@ const sectionStyle = {
   cursor: 'default',
   display: 'block',
   fontWeight: '500',
-  color: '#9baec8',
   marginBottom: '10px'
 };
 
@@ -42,8 +40,8 @@ const ColumnSettings = React.createClass({
 
     return (
       <ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}>
-        <div style={outerStyle}>
-          <span style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
+        <div className='column-settings--outer' style={outerStyle}>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reblogs' />} />
@@ -53,7 +51,7 @@ const ColumnSettings = React.createClass({
             <SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 
           <div style={rowStyle}>
             <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
index d20a4d170..0b7c737c6 100644
--- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
@@ -4,8 +4,7 @@ const iconStyle = {
   position: 'absolute',
   right: '48px',
   top: '0',
-  cursor: 'pointer',
-  background: '#2f3441'
+  cursor: 'pointer'
 };
 
 const ClearColumnButton = ({ onClick }) => (
diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
index b63c1881a..f1b8ef57f 100644
--- a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
@@ -5,7 +5,6 @@ import ColumnCollapsable from '../../../components/column_collapsable';
 import SettingToggle from './setting_toggle';
 
 const outerStyle = {
-  background: '#373b4a',
   padding: '15px'
 };
 
@@ -13,7 +12,6 @@ const sectionStyle = {
   cursor: 'default',
   display: 'block',
   fontWeight: '500',
-  color: '#9baec8',
   marginBottom: '10px'
 };
 
@@ -40,8 +38,8 @@ const ColumnSettings = React.createClass({
 
     return (
       <ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}>
-        <div style={outerStyle}>
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
+        <div className='column-settings--outer' style={outerStyle}>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
@@ -49,7 +47,7 @@ const ColumnSettings = React.createClass({
             <SettingToggle settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
@@ -57,7 +55,7 @@ const ColumnSettings = React.createClass({
             <SettingToggle settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
@@ -65,7 +63,7 @@ const ColumnSettings = React.createClass({
             <SettingToggle settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx
index 140ba9134..fa8466140 100644
--- a/app/assets/javascripts/components/features/notifications/components/notification.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx
@@ -7,16 +7,6 @@ import Permalink from '../../../components/permalink';
 import emojify from '../../../emoji';
 import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
 
-const messageStyle = {
-  marginLeft: '68px',
-  padding: '8px 0',
-  paddingBottom: '0',
-  cursor: 'default',
-  color: '#d9e1e8',
-  fontSize: '15px',
-  position: 'relative'
-};
-
 const linkStyle = {
   fontWeight: '500'
 };
@@ -32,9 +22,9 @@ const Notification = React.createClass({
   renderFollow (account, link) {
     return (
       <div className='notification'>
-        <div style={messageStyle}>
+        <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
-            <i className='fa fa-fw fa-user-plus' style={{ color: '#2b90d9' }} />
+            <i className='fa fa-fw fa-user-plus' />
           </div>
 
           <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
@@ -52,7 +42,7 @@ const Notification = React.createClass({
   renderFavourite (notification, link) {
     return (
       <div className='notification'>
-        <div style={messageStyle}>
+        <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
           </div>
@@ -68,9 +58,9 @@ const Notification = React.createClass({
   renderReblog (notification, link) {
     return (
       <div className='notification'>
-        <div style={messageStyle}>
+        <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
-            <i className='fa fa-fw fa-retweet' style={{ color: '#2b90d9' }} />
+            <i className='fa fa-fw fa-retweet' />
           </div>
 
           <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
diff --git a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx
index c2438f716..eae3c2be2 100644
--- a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx
@@ -11,14 +11,13 @@ const labelSpanStyle = {
   display: 'inline-block',
   verticalAlign: 'middle',
   marginBottom: '14px',
-  marginLeft: '8px',
-  color: '#9baec8'
+  marginLeft: '8px'
 };
 
 const SettingToggle = ({ settings, settingKey, label, onChange }) => (
   <label style={labelStyle}>
     <Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
-    <span style={labelSpanStyle}>{label}</span>
+    <span className='setting-toggle' style={labelSpanStyle}>{label}</span>
   </label>
 );
 
diff --git a/app/assets/javascripts/components/features/status/components/card.jsx b/app/assets/javascripts/components/features/status/components/card.jsx
index ccb06dfd5..1bb281c70 100644
--- a/app/assets/javascripts/components/features/status/components/card.jsx
+++ b/app/assets/javascripts/components/features/status/components/card.jsx
@@ -1,18 +1,6 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
-const outerStyle = {
-  display: 'flex',
-  cursor: 'pointer',
-  fontSize: '14px',
-  border: '1px solid #363c4b',
-  borderRadius: '4px',
-  color: '#616b86',
-  marginTop: '14px',
-  textDecoration: 'none',
-  overflow: 'hidden'
-};
-
 const contentStyle = {
   flex: '1 1 auto',
   padding: '8px',
@@ -20,25 +8,6 @@ const contentStyle = {
   overflow: 'hidden'
 };
 
-const titleStyle = {
-  display: 'block',
-  fontWeight: '500',
-  marginBottom: '5px',
-  color: '#d9e1e8',
-  overflow: 'hidden',
-  textOverflow: 'ellipsis',
-  whiteSpace: 'nowrap'
-};
-
-const descriptionStyle = {
-  color: '#d9e1e8'
-};
-
-const imageOuterStyle = {
-  flex: '0 0 100px',
-  background: '#373b4a'
-};
-
 const imageStyle = {
   display: 'block',
   width: '100%',
@@ -77,20 +46,20 @@ const Card = React.createClass({
 
     if (card.get('image')) {
       image = (
-        <div style={imageOuterStyle}>
+        <div className='status-card__image'>
           <img src={card.get('image')} alt={card.get('title')} style={imageStyle} />
         </div>
       );
     }
 
     return (
-      <a style={outerStyle} href={card.get('url')} className='status-card'>
+      <a href={card.get('url')} className='status-card'>
         {image}
 
-        <div style={contentStyle}>
-          <strong style={titleStyle} title={card.get('title')}>{card.get('title')}</strong>
-          <p style={descriptionStyle}>{card.get('description').substring(0, 50)}</p>
-          <span style={hostStyle}>{getHostname(card.get('url'))}</span>
+        <div className='status-card__content' style={contentStyle}>
+          <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
+          <p className='status-card__description'>{card.get('description').substring(0, 50)}</p>
+          <span className='status-card__host' style={hostStyle}>{getHostname(card.get('url'))}</span>
         </div>
       </a>
     );
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
index f2d6ae48a..8a7c0c5d5 100644
--- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx
+++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
@@ -52,7 +52,7 @@ const DetailedStatus = React.createClass({
     }
 
     return (
-      <div style={{ background: '#2f3441', padding: '14px 10px' }} className='detailed-status'>
+      <div style={{ padding: '14px 10px' }} className='detailed-status'>
         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
           <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
           <DisplayName account={status.get('account')} />
@@ -62,7 +62,7 @@ const DetailedStatus = React.createClass({
 
         {media}
 
-        <div style={{ marginTop: '15px', color: '#616b86', fontSize: '14px', lineHeight: '18px' }}>
+        <div className='detailed-status__meta'>
           <a className='detailed-status__datetime' style={{ color: 'inherit' }} href={status.get('url')} target='_blank' rel='noopener'><FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /></a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-retweet' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('reblogs_count')} /></span></Link> · <Link to={`/statuses/${status.get('id')}/favourites`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-star' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('favourites_count')} /></span></Link>
         </div>
       </div>
diff --git a/app/assets/javascripts/components/features/ui/components/column_link.jsx b/app/assets/javascripts/components/features/ui/components/column_link.jsx
index 901a29f5c..2bd1e1017 100644
--- a/app/assets/javascripts/components/features/ui/components/column_link.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column_link.jsx
@@ -4,7 +4,6 @@ const outerStyle = {
   display: 'block',
   padding: '15px',
   fontSize: '16px',
-  color: '#fff',
   textDecoration: 'none'
 };
 
diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
index 334e5c199..4c47fb8c5 100644
--- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -41,13 +41,12 @@ const imageStyle = {
 };
 
 const loadingStyle = {
-  background: '#373b4a',
   width: '400px',
   paddingBottom: '120px'
 };
 
 const preloader = () => (
-  <div style={loadingStyle}>
+  <div className='modal-container--preloader' style={loadingStyle}>
     <LoadingIndicator />
   </div>
 );
@@ -57,7 +56,6 @@ const leftNavStyle = {
   background: 'rgba(0, 0, 0, 0.5)',
   padding: '30px 15px',
   cursor: 'pointer',
-  color: '#fff',
   fontSize: '24px',
   top: '0',
   left: '-61px',
@@ -72,7 +70,6 @@ const rightNavStyle = {
   background: 'rgba(0, 0, 0, 0.5)',
   padding: '30px 15px',
   cursor: 'pointer',
-  color: '#fff',
   fontSize: '24px',
   top: '0',
   right: '-61px',
@@ -143,11 +140,11 @@ const Modal = React.createClass({
     leftNav = rightNav = '';
 
     if (hasLeft) {
-      leftNav = <div style={leftNavStyle} onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
+      leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
     }
 
     if (hasRight) {
-      rightNav = <div style={rightNavStyle} onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
+      rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
     }
 
     return (
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index c4c876e30..c590f7038 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -256,6 +256,35 @@ button:focus {
   }
 }
 
+.compact-header {
+  h1 {
+    font-size: 24px;
+    line-height: 28px;
+    color: $color3;
+    overflow: hidden;
+    font-weight: 500;
+    margin-bottom: 20px;
+
+    a {
+      color: inherit;
+      text-decoration: none;
+    }
+
+    small {
+      font-weight: 400;
+      color: $color2;
+    }
+
+    img {
+      display: inline-block;
+      margin-bottom: -5px;
+      margin-right: 15px;
+      width: 36px;
+      height: 36px;
+    }
+  }
+}
+
 @import 'forms';
 @import 'accounts';
 @import 'stream_entries';
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 6bb683f17..f0948b0f3 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -34,6 +34,7 @@
 
 .column-icon {
   color: $color3;
+  background: lighten($color1, 4%);
 
   &:hover {
     color: lighten($color3, 7%);
@@ -187,7 +188,7 @@
 a.status__content__spoiler-link {
   display: inline-block;
   border-radius: 2px;
-  color: lighten($color1, 6%);
+  color: lighten($color1, 8%);
   font-weight: 500;
   font-size: 11px;
   padding: 0px 6px;
@@ -200,7 +201,7 @@ a.status__content__spoiler-link {
   padding-left: 68px;
   position: relative;
   min-height: 48px;
-  border-bottom: 1px solid lighten($color1, 6%);
+  border-bottom: 1px solid lighten($color1, 8%);
   cursor: default;
 
   .status__relative-time {
@@ -226,6 +227,8 @@ a.status__content__spoiler-link {
 }
 
 .detailed-status {
+  background: lighten($color1, 4%);
+
   .status__content {
     font-size: 19px;
     line-height: 24px;
@@ -237,12 +240,19 @@ a.status__content__spoiler-link {
   }
 }
 
+.detailed-status__meta {
+  margin-top: 15px;
+  color: lighten($color1, 26%);
+  font-size: 14px;
+  line-height: 18px;
+}
+
 .detailed-status__action-bar {
   background: lighten($color1, 4%);
   display: flex;
   flex-direction: row;
-  border-top: 1px solid lighten($color1, 6%);
-  border-bottom: 1px solid lighten($color1, 6%);
+  border-top: 1px solid lighten($color1, 8%);
+  border-bottom: 1px solid lighten($color1, 8%);
   padding: 10px 0;
 }
 
@@ -257,7 +267,7 @@ a.status__content__spoiler-link {
 
 .account {
   padding: 10px;
-  border-bottom: 1px solid lighten($color1, 6%);
+  border-bottom: 1px solid lighten($color1, 8%);
 
   .account__display-name {
     flex: 1 1 auto;
@@ -298,6 +308,7 @@ a.status__content__spoiler-link {
   word-wrap: break-word;
   font-weight: 400;
   overflow: hidden;
+  color: $color3;
 
   p {
     margin-bottom: 20px;
@@ -325,8 +336,8 @@ a.status__content__spoiler-link {
 }
 
 .account__action-bar {
-  border-top: 1px solid lighten($color1, 6%);
-  border-bottom: 1px solid lighten($color1, 6%);
+  border-top: 1px solid lighten($color1, 8%);
+  border-bottom: 1px solid lighten($color1, 8%);
   line-height: 36px;
   overflow: hidden;
   flex: 0 0 auto;
@@ -337,7 +348,7 @@ a.status__content__spoiler-link {
   text-decoration: none;
   overflow: hidden;
   width: 80px;
-  border-left: 1px solid lighten($color1, 6%);
+  border-left: 1px solid lighten($color1, 8%);
   padding: 10px 5px;
 
   & > span {
@@ -412,8 +423,9 @@ a.status__content__spoiler-link {
     opacity: 0.5;
   }
 
-  .status__content__spoiler-link {
+  a.status__content__spoiler-link {
     background: lighten($color1, 26%);
+    color: lighten($color1, 4%);
 
     &:hover {
       background: lighten($color1, 29%);
@@ -422,6 +434,20 @@ a.status__content__spoiler-link {
   }
 }
 
+.notification__message {
+  margin-left: 68px;
+  padding: 8px 0;
+  padding-bottom: 0;
+  cursor: default;
+  color: $color3;
+  font-size: 15px;
+  position: relative;
+
+  .fa {
+    color: $color4;
+  }
+}
+
 .notification__display-name {
   color: inherit;
   text-decoration: none;
@@ -646,7 +672,7 @@ a.status__content__spoiler-link {
 
 .tabs-bar {
   display: flex;
-  background: lighten($color1, 6%);
+  background: lighten($color1, 8%);
   flex: 0 0 auto;
   overflow-y: auto;
 }
@@ -660,7 +686,7 @@ a.status__content__spoiler-link {
   text-align: center;
   font-size:12px;
   font-weight: 500;
-  border-bottom: 2px solid lighten($color1, 6%);
+  border-bottom: 2px solid lighten($color1, 8%);
 
   &.active {
     border-bottom: 2px solid $color4;
@@ -850,7 +876,8 @@ a.status__content__spoiler-link {
 }
 
 .column-link {
-  background: lighten($color1, 6%);
+  background: lighten($color1, 8%);
+  color: $color5;
 
   &:hover {
     background: lighten($color1, 11%);
@@ -883,6 +910,7 @@ a.status__content__spoiler-link {
 
 .autosuggest-textarea__textarea {
   height: 100px;
+  background: $color5;
 }
 
 .autosuggest-textarea__suggestions {
@@ -968,11 +996,40 @@ button.active i.fa-retweet {
 }
 
 .status-card {
+  display: flex;
+  cursor: pointer;
+  font-size: 14px;
+  border: 1px solid lighten($color1, 8%);
+  border-radius: 4px;
+  color: lighten($color1, 26%);
+  margin-top: 14px;
+  text-decoration: none;
+  overflow: hidden;
+
   &:hover {
-    background: lighten($color1, 6%);
+    background: lighten($color1, 8%);
   }
 }
 
+.status-card__title {
+  display: block;
+  font-weight: 500;
+  margin-bottom: 5px;
+  color: $color3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.status-card__description {
+  color: $color3;
+}
+
+.status-card__image {
+  flex: 0 0 100px;
+  background: lighten($color1, 8%);
+}
+
 .load-more {
   display: block;
   color: lighten($color1, 26%);
@@ -981,7 +1038,7 @@ button.active i.fa-retweet {
   text-decoration: none;
 
   &:hover {
-    background: lighten($color1, 6%);
+    background: lighten($color1, 8%);
   }
 }
 
@@ -1020,3 +1077,53 @@ button.active i.fa-retweet {
   font-size: 14px;
   margin: 0;
 }
+
+.loading-indicator {
+  color: $color2;
+}
+
+.collapsable-collapsed {
+  color: $color3;
+  background: lighten($color1, 4%);
+}
+
+.collapsable {
+  color: $color5;
+  background: lighten($color1, 8%);
+}
+
+.media-spoiler {
+  background: $color8;
+  color: $color5;
+}
+
+.modal-container--preloader {
+  background: lighten($color1, 8%);
+}
+
+.account--panel {
+  background: lighten($color1, 4%);
+  border-top: 1px solid lighten($color1, 8%);
+  border-bottom: 1px solid lighten($color1, 8%);
+}
+
+.column-settings--outer {
+  background: lighten($color1, 8%);
+}
+
+.column-settings--section {
+  color: $color3;
+}
+
+.modal-container--nav {
+  color: $color5;
+}
+
+.account--follows-info {
+  color: $color5;
+}
+
+.setting-toggle {
+  color: $color3;
+}
+
diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss
index 595275527..d427c1466 100644
--- a/app/assets/stylesheets/stream_entries.scss
+++ b/app/assets/stylesheets/stream_entries.scss
@@ -5,24 +5,24 @@
   .entry {
     background: lighten($color2, 8%);
 
-    &, .detailed-status.light {
+    .detailed-status.light, .status.light {
       border-bottom: 1px solid $color2;
     }
 
     &:last-child {
-      &, .detailed-status.light {
+      &, .detailed-status.light, .status.light {
         border-bottom: 0;
         border-radius: 0 0 4px 4px;
       }
     }
 
     &:first-child {
-      &, .detailed-status.light {
+      &, .detailed-status.light, .status.light {
         border-radius: 4px 4px 0 0;
       }
 
       &:last-child {
-        &, .detailed-status.light {
+        &, .detailed-status.light, .status.light {
           border-radius: 4px;
         }
       }
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index a30e97e71..740083735 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -18,12 +18,12 @@ class Api::V1::FollowRequestsController < ApiController
   end
 
   def authorize
-    FollowRequest.find_by!(account_id: params[:id], target_account: current_account).authorize!
+    AuthorizeFollowService.new.call(Account.find(params[:id]), current_account)
     render_empty
   end
 
   def reject
-    FollowRequest.find_by!(account_id: params[:id], target_account: current_account).reject!
+    RejectFollowService.new.call(Account.find(params[:id]), current_account)
     render_empty
   end
 end
diff --git a/app/controllers/concerns/obfuscate_filename.rb b/app/controllers/concerns/obfuscate_filename.rb
index 9f12cb7e9..dde7ce8c6 100644
--- a/app/controllers/concerns/obfuscate_filename.rb
+++ b/app/controllers/concerns/obfuscate_filename.rb
@@ -1,4 +1,5 @@
 # frozen_string_literal: true
+
 module ObfuscateFilename
   extend ActiveSupport::Concern
 
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index fb8f0976c..5d20f8c2d 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -143,6 +143,10 @@ module AtomBuilderHelper
     xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection])
   end
 
+  def privacy_scope(xml, level)
+    xml['mastodon'].scope(level)
+  end
+
   def include_author(xml, account)
     object_type      xml, :person
     uri              xml, TagManager.instance.uri_for(account)
@@ -152,6 +156,7 @@ module AtomBuilderHelper
     link_alternate   xml, TagManager.instance.url_for(account)
     link_avatar      xml, account
     portable_contact xml, account
+    privacy_scope    xml, account.locked? ? :private : :public
   end
 
   def rich_content(xml, activity)
@@ -216,6 +221,7 @@ module AtomBuilderHelper
           end
 
           category(xml, 'nsfw') if stream_entry.target.sensitive?
+          privacy_scope(xml, stream_entry.target.visibility)
         end
       end
     end
@@ -237,6 +243,7 @@ module AtomBuilderHelper
     end
 
     category(xml, 'nsfw') if stream_entry.activity.sensitive?
+    privacy_scope(xml, stream_entry.activity.visibility)
   end
 
   private
@@ -249,6 +256,7 @@ module AtomBuilderHelper
                'xmlns:poco'     => TagManager::POCO_XMLNS,
                'xmlns:media'    => TagManager::MEDIA_XMLNS,
                'xmlns:ostatus'  => TagManager::OS_XMLNS,
+               'xmlns:mastodon' => TagManager::MTDN_XMLNS,
              }, &block)
   end
 
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index f0928a945..623a1af03 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -107,7 +107,6 @@ class FeedManager
     should_filter ||= receiver.blocking?(status.account)                                    # or it's from someone I blocked
     should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked
     should_filter ||= (status.account.silenced? && !receiver.following?(status.account))    # of if the account is silenced and I'm not following them
-    should_filter ||= (status.private_visibility? && !receiver.following?(status.account))  # or if the mentioned account is not permitted to see the private status
 
     if status.reply? && !status.in_reply_to_account_id.nil?                                 # or it's a reply
       should_filter ||= receiver.blocking?(status.in_reply_to_account)                      # to a user I blocked
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index 2508eea97..9fef70fda 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -7,15 +7,18 @@ class TagManager
   include RoutingHelper
 
   VERBS = {
-    post:       'http://activitystrea.ms/schema/1.0/post',
-    share:      'http://activitystrea.ms/schema/1.0/share',
-    favorite:   'http://activitystrea.ms/schema/1.0/favorite',
-    unfavorite: 'http://activitystrea.ms/schema/1.0/unfavorite',
-    delete:     'http://activitystrea.ms/schema/1.0/delete',
-    follow:     'http://activitystrea.ms/schema/1.0/follow',
-    unfollow:   'http://ostatus.org/schema/1.0/unfollow',
-    block:      'http://mastodon.social/schema/1.0/block',
-    unblock:    'http://mastodon.social/schema/1.0/unblock',
+    post:           'http://activitystrea.ms/schema/1.0/post',
+    share:          'http://activitystrea.ms/schema/1.0/share',
+    favorite:       'http://activitystrea.ms/schema/1.0/favorite',
+    unfavorite:     'http://activitystrea.ms/schema/1.0/unfavorite',
+    delete:         'http://activitystrea.ms/schema/1.0/delete',
+    follow:         'http://activitystrea.ms/schema/1.0/follow',
+    request_friend: 'http://activitystrea.ms/schema/1.0/request-friend',
+    authorize:      'http://activitystrea.ms/schema/1.0/authorize',
+    reject:         'http://activitystrea.ms/schema/1.0/reject',
+    unfollow:       'http://ostatus.org/schema/1.0/unfollow',
+    block:          'http://mastodon.social/schema/1.0/block',
+    unblock:        'http://mastodon.social/schema/1.0/unblock',
   }.freeze
 
   TYPES = {
@@ -38,6 +41,7 @@ class TagManager
   POCO_XMLNS  = 'http://portablecontacts.net/spec/1.0'
   DFRN_XMLNS  = 'http://purl.org/macgirvin/dfrn/1.0'
   OS_XMLNS    = 'http://ostatus.org/schema/1.0'
+  MTDN_XMLNS  = 'http://mastodon.social/schema/1.0'
 
   def unique_tag(date, id, type)
     "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
diff --git a/app/models/account.rb b/app/models/account.rb
index c2a41c4c6..ed5c46197 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -95,6 +95,10 @@ class Account < ApplicationRecord
     follow_requests.where(target_account: other_account).exists?
   end
 
+  def followers_domains
+    followers.reorder('').select('DISTINCT accounts.domain').map(&:domain)
+  end
+
   def local?
     domain.nil?
   end
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index 3f3616dce..cd8e2098c 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -12,11 +12,11 @@ class Favourite < ApplicationRecord
   validates :status_id, uniqueness: { scope: :account_id }
 
   def verb
-    :favorite
+    destroyed? ? :unfavorite : :favorite
   end
 
   def title
-    "#{account.acct} favourited a status by #{status.account.acct}"
+    destroyed? ? "#{account.acct} no longer favourites a status by #{status.account.acct}" : "#{account.acct} favourited a status by #{status.account.acct}"
   end
 
   delegate :object_type, to: :target
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 936ad0691..989c2c2a2 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -2,6 +2,7 @@
 
 class FollowRequest < ApplicationRecord
   include Paginable
+  include Streamable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
@@ -12,12 +13,47 @@ class FollowRequest < ApplicationRecord
   validates :account_id, uniqueness: { scope: :target_account_id }
 
   def authorize!
+    @verb = :authorize
+
     account.follow!(target_account)
     MergeWorker.perform_async(target_account.id, account.id)
+
     destroy!
   end
 
   def reject!
+    @verb = :reject
     destroy!
   end
+
+  def verb
+    destroyed? ? (@verb || :delete) : :request_friend
+  end
+
+  def target
+    target_account
+  end
+
+  def object_type
+    :person
+  end
+
+  def hidden?
+    true
+  end
+
+  def title
+    if destroyed?
+      case @verb
+      when :authorize
+        "#{target_account.acct} authorized #{account.acct}'s request to follow"
+      when :reject
+        "#{target_account.acct} rejected #{account.acct}'s request to follow"
+      else
+        "#{account.acct} withdrew the request to follow #{target_account.acct}"
+      end
+    else
+      "#{account.acct} requested to follow #{target_account.acct}"
+    end
+  end
 end
diff --git a/app/models/status.rb b/app/models/status.rb
index d2be72308..93594ec8f 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -76,7 +76,11 @@ class Status < ApplicationRecord
   end
 
   def permitted?(other_account = nil)
-    private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : other_account.nil? || !account.blocking?(other_account)
+    if private_visibility?
+      (account.id == other_account&.id || other_account&.following?(account) || mentions.include?(other_account))
+    else
+      other_account.nil? || !account.blocking?(other_account)
+    end
   end
 
   def ancestors(account = nil)
@@ -153,6 +157,10 @@ class Status < ApplicationRecord
         where('1 = 1')
       elsif !account.nil? && target_account.blocking?(account)
         where('1 = 0')
+      elsif !account.nil?
+        joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id')
+          .where('mentions.account_id = ?', account.id)
+          .where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:private])
       else
         where.not(visibility: :private)
       end
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index fcc691bef..e0b85be15 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -30,7 +30,7 @@ class StreamEntry < ApplicationRecord
   end
 
   def targeted?
-    [:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb
+    [:follow, :request_friend, :authorize, :unfollow, :block, :unblock, :share, :favorite].include? verb
   end
 
   def target
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
new file mode 100644
index 000000000..1590d8433
--- /dev/null
+++ b/app/services/authorize_follow_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AuthorizeFollowService < BaseService
+  include StreamEntryRenderer
+
+  def call(source_account, target_account)
+    follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
+    follow_request.authorize!
+    NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
+  end
+end
diff --git a/app/services/block_service.rb b/app/services/block_service.rb
index e04b6cc39..095d2a8eb 100644
--- a/app/services/block_service.rb
+++ b/app/services/block_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class BlockService < BaseService
+  include StreamEntryRenderer
+
   def call(account, target_account)
     return if account.id == target_account.id
 
@@ -10,6 +12,6 @@ class BlockService < BaseService
     block = account.block!(target_account)
 
     BlockWorker.perform_async(account.id, target_account.id)
-    NotificationWorker.perform_async(block.stream_entry.id, target_account.id) unless target_account.local?
+    NotificationWorker.perform_async(stream_entry_to_xml(block.stream_entry), account.id, target_account.id) unless target_account.local?
   end
 end
diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb
new file mode 100644
index 000000000..a4255daea
--- /dev/null
+++ b/app/services/concerns/stream_entry_renderer.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module StreamEntryRenderer
+  def stream_entry_to_xml(stream_entry)
+    renderer = StreamEntriesController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
+    renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom])
+  end
+end
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index d5fbd29e9..ce1722b77 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class FavouriteService < BaseService
+  include StreamEntryRenderer
+
   # Favourite a status and notify remote user
   # @param [Account] account
   # @param [Status] status
@@ -15,7 +17,7 @@ class FavouriteService < BaseService
     if status.local?
       NotifyService.new.call(favourite.status.account, favourite)
     else
-      NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
+      NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
     end
 
     favourite
diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb
index 3c3694a65..baefa3a86 100644
--- a/app/services/fetch_remote_account_service.rb
+++ b/app/services/fetch_remote_account_service.rb
@@ -22,7 +22,9 @@ class FetchRemoteAccountService < BaseService
 
     Rails.logger.debug "Going to webfinger #{username}@#{domain}"
 
-    return FollowRemoteAccountService.new.call("#{username}@#{domain}")
+    account = FollowRemoteAccountService.new.call("#{username}@#{domain}")
+    UpdateRemoteProfileService.new.call(xml, account) unless account.nil?
+    account
   rescue TypeError
     Rails.logger.debug "Unparseable URL given: #{url}"
     nil
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 9f34cb6ac..ac0392d16 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class FollowService < BaseService
+  include StreamEntryRenderer
+
   # Follow a remote user, notify remote user about the follow
   # @param [Account] source_account From which to follow
   # @param [String] uri User URI to follow in the form of username@domain
@@ -20,10 +22,14 @@ class FollowService < BaseService
   private
 
   def request_follow(source_account, target_account)
-    return unless target_account.local?
-
     follow_request = FollowRequest.create!(account: source_account, target_account: target_account)
-    NotifyService.new.call(target_account, follow_request)
+
+    if target_account.local?
+      NotifyService.new.call(target_account, follow_request)
+    else
+      NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), source_account.id, target_account.id)
+      AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
+    end
 
     follow_request
   end
@@ -34,8 +40,9 @@ class FollowService < BaseService
     if target_account.local?
       NotifyService.new.call(target_account, follow)
     else
-      subscribe_service.call(target_account)
-      NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
+      subscribe_service.call(target_account) unless target_account.subscribed?
+      NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id)
+      AfterRemoteFollowWorker.perform_async(follow.id)
     end
 
     MergeWorker.perform_async(target_account.id, source_account.id)
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index c411e3e82..f0a62aa14 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -106,7 +106,8 @@ class ProcessFeedService < BaseService
         text: content(entry),
         spoiler_text: content_warning(entry),
         created_at: published(entry),
-        reply: thread?(entry)
+        reply: thread?(entry),
+        visibility: visibility_scope(entry)
       )
 
       if thread?(entry)
@@ -144,15 +145,9 @@ class ProcessFeedService < BaseService
 
     def mentions_from_xml(parent, xml)
       processed_account_ids = []
-      public_visibility     = false
 
       xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
-        if link['ostatus:object-type'] == TagManager::TYPES[:collection] && link['href'] == TagManager::COLLECTIONS[:public]
-          public_visibility = true
-          next
-        elsif link['ostatus:object-type'] == TagManager::TYPES[:group]
-          next
-        end
+        next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
 
         url = Addressable::URI.parse(link['href'])
 
@@ -172,9 +167,6 @@ class ProcessFeedService < BaseService
         # So we can skip duplicate mentions
         processed_account_ids << mentioned_account.id
       end
-
-      parent.visibility = public_visibility ? :public : :unlisted
-      parent.save!
     end
 
     def hashtags_from_xml(parent, xml)
@@ -230,6 +222,10 @@ class ProcessFeedService < BaseService
       xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
     end
 
+    def visibility_scope(xml = @xml)
+      xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public
+    end
+
     def published(xml = @xml)
       xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content
     end
diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb
index 5f91e3127..8420ca351 100644
--- a/app/services/process_interaction_service.rb
+++ b/app/services/process_interaction_service.rb
@@ -29,6 +29,12 @@ class ProcessInteractionService < BaseService
       case verb(xml)
       when :follow
         follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account)
+      when :request_friend
+        follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account)
+      when :authorize
+        authorize_follow_request!(account, target_account)
+      when :reject
+        reject_follow_request!(account, target_account)
       when :unfollow
         unfollow!(account, target_account)
       when :favorite
@@ -72,6 +78,22 @@ class ProcessInteractionService < BaseService
     NotifyService.new.call(target_account, follow)
   end
 
+  def follow_request!(account, target_account)
+    follow_request = FollowRequest.create!(account: account, target_account: target_account)
+    NotifyService.new.call(target_account, follow_request)
+  end
+
+  def authorize_follow_request!(account, target_account)
+    follow_request = FollowRequest.find_by(account: target_account, target_account: account)
+    follow_request&.authorize!
+    SubscribeService.new.call(account) unless account.subscribed?
+  end
+
+  def reject_follow_request!(account, target_account)
+    follow_request = FollowRequest.find_by(account: target_account, target_account: account)
+    follow_request&.reject!
+  end
+
   def unfollow!(account, target_account)
     account.unfollow!(target_account)
   end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index 72568e702..d3d3af8af 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class ProcessMentionsService < BaseService
+  include StreamEntryRenderer
+
   # Scan status for mentions and fetch remote mentioned users, create
   # local mention pointers, send Salmon notifications to mentioned
   # remote users
@@ -28,12 +30,10 @@ class ProcessMentionsService < BaseService
     status.mentions.each do |mention|
       mentioned_account = mention.account
 
-      next if status.private_visibility? && (!mentioned_account.following?(status.account) || !mentioned_account.local?)
-
       if mentioned_account.local?
         NotifyService.new.call(mentioned_account, mention)
       else
-        NotificationWorker.perform_async(status.stream_entry.id, mentioned_account.id)
+        NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
       end
     end
   end
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 4ea0dbf6c..7a52f041f 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class ReblogService < BaseService
+  include StreamEntryRenderer
+
   # Reblog a status and notify its remote author
   # @param [Account] account Account to reblog from
   # @param [Status] reblogged_status Status to be reblogged
@@ -18,15 +20,9 @@ class ReblogService < BaseService
     if reblogged_status.local?
       NotifyService.new.call(reblog.reblog.account, reblog)
     else
-      NotificationWorker.perform_async(reblog.stream_entry.id, reblog.reblog.account_id)
+      NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), account.id, reblog.reblog.account_id)
     end
 
     reblog
   end
-
-  private
-
-  def send_interaction_service
-    @send_interaction_service ||= SendInteractionService.new
-  end
 end
diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb
new file mode 100644
index 000000000..0c568b981
--- /dev/null
+++ b/app/services/reject_follow_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RejectFollowService < BaseService
+  include StreamEntryRenderer
+
+  def call(source_account, target_account)
+    follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
+    follow_request.reject!
+    NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
+  end
+end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 48e8dd3b8..b1a646b14 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class RemoveStatusService < BaseService
+  include StreamEntryRenderer
+
   def call(status)
     remove_from_self(status) if status.account.local?
     remove_from_followers(status)
@@ -43,7 +45,7 @@ class RemoveStatusService < BaseService
 
   def send_delete_salmon(account, status)
     return unless status.local?
-    NotificationWorker.perform_async(status.stream_entry.id, account.id)
+    NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, account.id)
   end
 
   def remove_reblogs(status)
diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb
index 05a1e77e3..99113eeca 100644
--- a/app/services/send_interaction_service.rb
+++ b/app/services/send_interaction_service.rb
@@ -2,27 +2,16 @@
 
 class SendInteractionService < BaseService
   # Send an Atom representation of an interaction to a remote Salmon endpoint
-  # @param [StreamEntry] stream_entry
+  # @param [String] Entry XML
+  # @param [Account] source_account
   # @param [Account] target_account
-  def call(stream_entry, target_account)
-    envelope = salmon.pack(entry_xml(stream_entry), stream_entry.account.keypair)
+  def call(xml, source_account, target_account)
+    envelope = salmon.pack(xml, source_account.keypair)
     salmon.post(target_account.salmon_url, envelope)
   end
 
   private
 
-  def entry_xml(stream_entry)
-    Nokogiri::XML::Builder.new do |xml|
-      entry(xml, true) do
-        author(xml) do
-          include_author xml, stream_entry.account
-        end
-
-        include_entry xml, stream_entry
-      end
-    end.to_xml
-  end
-
   def salmon
     @salmon ||= OStatus2::Salmon.new
   end
diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb
index f389364f9..84b1050c1 100644
--- a/app/services/unblock_service.rb
+++ b/app/services/unblock_service.rb
@@ -1,10 +1,12 @@
 # frozen_string_literal: true
 
 class UnblockService < BaseService
+  include StreamEntryRenderer
+
   def call(account, target_account)
     return unless account.blocking?(target_account)
 
     unblock = account.unblock!(target_account)
-    NotificationWorker.perform_async(unblock.stream_entry.id, target_account.id) unless target_account.local?
+    NotificationWorker.perform_async(stream_entry_to_xml(unblock.stream_entry), account.id, target_account.id) unless target_account.local?
   end
 end
diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb
index de6e84e7d..04293ee08 100644
--- a/app/services/unfavourite_service.rb
+++ b/app/services/unfavourite_service.rb
@@ -1,12 +1,14 @@
 # frozen_string_literal: true
 
 class UnfavouriteService < BaseService
+  include StreamEntryRenderer
+
   def call(account, status)
     favourite = Favourite.find_by!(account: account, status: status)
     favourite.destroy!
 
     unless status.local?
-      NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
+      NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
     end
 
     favourite
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index f469793c1..178da4da3 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -1,12 +1,14 @@
 # frozen_string_literal: true
 
 class UnfollowService < BaseService
+  include StreamEntryRenderer
+
   # Unfollow and notify the remote user
   # @param [Account] source_account Where to unfollow from
   # @param [Account] target_account Which to unfollow
   def call(source_account, target_account)
     follow = source_account.unfollow!(target_account)
-    NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) unless target_account.local?
+    NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id) unless target_account.local?
     UnmergeWorker.perform_async(target_account.id, source_account.id)
   end
 end
diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb
index ad9c56540..dc315db19 100644
--- a/app/services/update_remote_profile_service.rb
+++ b/app/services/update_remote_profile_service.rb
@@ -10,6 +10,7 @@ class UpdateRemoteProfileService < BaseService
     unless author_xml.nil?
       account.display_name = author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).nil?
       account.note         = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil?
+      account.locked       = author_xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content == 'private'
 
       unless account.suspended? || DomainBlock.find_by(domain: account.domain)&.reject_media?
         account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank?
diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml
index 88bfe3d61..022a3a9e4 100644
--- a/app/views/about/index.html.haml
+++ b/app/views/about/index.html.haml
@@ -32,6 +32,7 @@
       = link_to t('about.learn_more'), about_more_path
       = link_to t('about.terms'), terms_path
       = link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
+      = link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
 
     = link_to t('about.get_started'), new_user_registration_path, class: 'button webapp-btn'
     = link_to t('auth.login'), new_user_session_path, class: 'button webapp-btn'
diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml
index 5b482213b..1429dbd9e 100644
--- a/app/views/admin/settings/index.html.haml
+++ b/app/views/admin/settings/index.html.haml
@@ -17,6 +17,10 @@
       %td= best_in_place @settings['site_contact_email'], :value, url: admin_setting_path(@settings['site_contact_email']), place_holder: 'Enter a public e-mail address'
     %tr
       %td
+        %strong Site title
+      %td= best_in_place @settings['site_title'], :value, url: admin_setting_path(@settings['site_title'])
+    %tr
+      %td
         %strong Site description
         %br/
         Displayed as a paragraph on the frontpage and used as a meta tag.
@@ -33,4 +37,4 @@
         Displayed on extended information page
         %br/
         You can use HTML tags
-      %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
\ No newline at end of file
+      %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index e122e1c55..7eae6982b 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -13,7 +13,7 @@
 
     %title
       = "#{yield(:page_title)} - " if content_for?(:page_title)
-      Mastodon
+      = Setting.site_title
 
     = stylesheet_link_tag 'application', media: 'all'
     = csrf_meta_tags
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 412ec4fa5..32a50e158 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -1,10 +1,18 @@
+- content_for :page_title do
+  = "##{@tag.name}"
+
+.compact-header
+  %h1<
+    = link_to 'Mastodon', root_path
+    %small= "##{@tag.name}"
+
 - if @statuses.empty?
   .accounts-grid
     = render partial: 'accounts/nothing_here'
 - else
   .activity-stream.h-feed
-    = render partial: 'stream_entries/status', collection: @statuses, as: :status, cached: true
+    = render partial: 'stream_entries/status', collection: @statuses, as: :status
 
-.pagination
-  - if @statuses.size == 20
+- if @statuses.size == 20
+  .pagination
     = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb
new file mode 100644
index 000000000..ad94d2769
--- /dev/null
+++ b/app/workers/after_remote_follow_request_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AfterRemoteFollowRequestWorker
+  include Sidekiq::Worker
+
+  sidekiq_options retry: 5
+
+  def perform(follow_request_id)
+    follow_request  = FollowRequest.find(follow_request_id)
+    updated_account = FetchRemoteAccountService.new.call(follow_request.target_account.remote_url)
+
+    return if updated_account.locked?
+
+    follow_request.destroy
+    FollowService.new.call(follow_request.account, updated_account.acct)
+  end
+end
diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb
new file mode 100644
index 000000000..496aaf73e
--- /dev/null
+++ b/app/workers/after_remote_follow_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AfterRemoteFollowWorker
+  include Sidekiq::Worker
+
+  sidekiq_options retry: 5
+
+  def perform(follow_id)
+    follow          = Follow.find(follow_id)
+    updated_account = FetchRemoteAccountService.new.call(follow.target_account.remote_url)
+
+    return unless updated_account.locked?
+
+    follow.destroy
+    FollowService.new.call(follow.account, updated_account.acct)
+  end
+end
diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb
index e4c38d384..1a2faefd8 100644
--- a/app/workers/notification_worker.rb
+++ b/app/workers/notification_worker.rb
@@ -5,7 +5,7 @@ class NotificationWorker
 
   sidekiq_options retry: 5
 
-  def perform(stream_entry_id, target_account_id)
-    SendInteractionService.new.call(StreamEntry.find(stream_entry_id), Account.find(target_account_id))
+  def perform(xml, source_account_id, target_account_id)
+    SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))
   end
 end
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
index d5437bf6b..4576dc4a2 100644
--- a/app/workers/pubsubhubbub/distribution_worker.rb
+++ b/app/workers/pubsubhubbub/distribution_worker.rb
@@ -8,13 +8,18 @@ class Pubsubhubbub::DistributionWorker
   def perform(stream_entry_id)
     stream_entry = StreamEntry.find(stream_entry_id)
 
-    return if stream_entry.hidden?
+    # Most hidden stream entries should not be PuSHed,
+    # but statuses need to be distributed to trusted
+    # followers even when they are hidden
+    return if stream_entry.hidden? && stream_entry.activity_type != 'Status'
 
     account  = stream_entry.account
     renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
     payload  = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
+    domains  = account.followers_domains
 
-    Subscription.where(account: account).active.select('id').find_each do |subscription|
+    Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
+      next unless domains.include?(Addressable::URI.parse(subscription.callback_url).host)
       Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
     end
   rescue ActiveRecord::RecordNotFound
diff --git a/app/workers/push_notification_worker.rb b/app/workers/push_notification_worker.rb
deleted file mode 100644
index a61d0e349..000000000
--- a/app/workers/push_notification_worker.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class PushNotificationWorker
-  include Sidekiq::Worker
-
-  def perform(notification_id)
-    SendPushNotificationService.new.call(Notification.find(notification_id))
-  rescue ActiveRecord::RecordNotFound
-    true
-  end
-end