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/actions/compose.jsx11
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx15
-rw-r--r--app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx8
-rw-r--r--app/assets/javascripts/components/locales/de.jsx2
-rw-r--r--app/assets/javascripts/components/locales/en.jsx1
-rw-r--r--app/assets/javascripts/components/locales/es.jsx2
-rw-r--r--app/assets/javascripts/components/locales/fr.jsx3
-rw-r--r--app/assets/javascripts/components/locales/hu.jsx1
-rw-r--r--app/assets/javascripts/components/locales/pt.jsx2
-rw-r--r--app/assets/javascripts/components/reducers/compose.jsx6
-rw-r--r--app/controllers/api/oembed_controller.rb21
-rw-r--r--app/controllers/api/v1/statuses_controller.rb4
-rw-r--r--app/controllers/concerns/obfuscate_filename.rb (renamed from app/models/concerns/obfuscate_filename.rb)3
-rw-r--r--app/controllers/settings/profiles_controller.rb2
-rw-r--r--app/helpers/api/oembed_helper.rb2
-rw-r--r--app/helpers/atom_builder_helper.rb38
-rw-r--r--app/lib/tag_manager.rb31
-rw-r--r--app/models/status.rb4
-rw-r--r--app/services/fan_out_on_write_service.rb2
-rw-r--r--app/services/post_status_service.rb2
-rw-r--r--app/services/process_feed_service.rb54
-rw-r--r--app/services/process_interaction_service.rb19
-rw-r--r--app/services/update_remote_profile_service.rb13
-rw-r--r--app/views/api/oembed/show.json.rabl14
-rw-r--r--app/views/settings/profiles/show.html.haml1
-rw-r--r--app/views/stream_entries/show.html.haml5
26 files changed, 200 insertions, 66 deletions
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index b97cb7b12..c2a7909f6 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -23,6 +23,7 @@ export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT';
 export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
 
 export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
+export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE';
 
 export function changeCompose(text) {
   return {
@@ -65,7 +66,8 @@ export function submitCompose() {
       status: getState().getIn(['compose', 'text'], ''),
       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
       media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
-      sensitive: getState().getIn(['compose', 'sensitive'])
+      sensitive: getState().getIn(['compose', 'sensitive']),
+      unlisted: getState().getIn(['compose', 'unlisted'])
     }).then(function (response) {
       dispatch(submitComposeSuccess(response.data));
       dispatch(updateTimeline('home', response.data));
@@ -207,3 +209,10 @@ export function changeComposeSensitivity(checked) {
     checked
   };
 };
+
+export function changeComposeVisibility(checked) {
+  return {
+    type: COMPOSE_VISIBILITY_CHANGE,
+    checked
+  };
+};
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index b16731c05..4688f39d3 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -70,6 +70,7 @@ const ComposeForm = React.createClass({
     suggestion_token: React.PropTypes.string,
     suggestions: React.PropTypes.array,
     sensitive: React.PropTypes.bool,
+    unlisted: React.PropTypes.bool,
     is_submitting: React.PropTypes.bool,
     is_uploading: React.PropTypes.bool,
     in_reply_to: ImmutablePropTypes.map,
@@ -79,7 +80,8 @@ const ComposeForm = React.createClass({
     onClearSuggestions: React.PropTypes.func.isRequired,
     onFetchSuggestions: React.PropTypes.func.isRequired,
     onSuggestionSelected: React.PropTypes.func.isRequired,
-    onChangeSensitivity: React.PropTypes.func.isRequired
+    onChangeSensitivity: React.PropTypes.func.isRequired,
+    onChangeVisibility: React.PropTypes.func.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -147,6 +149,10 @@ const ComposeForm = React.createClass({
     this.props.onChangeSensitivity(e.target.checked);
   },
 
+  handleChangeVisibility (e) {
+    this.props.onChangeVisibility(e.target.checked);
+  },
+
   render () {
     const { intl } = this.props;
     let replyArea  = '';
@@ -187,7 +193,12 @@ const ComposeForm = React.createClass({
           <UploadButtonContainer style={{ paddingTop: '4px' }} />
         </div>
 
-        <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', marginTop: '10px', borderTop: '1px solid #616b86', paddingTop: '10px' }}>
+        <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', marginTop: '10px', borderTop: '1px solid #282c37', paddingTop: '10px' }}>
+          <Toggle checked={this.props.unlisted} onChange={this.handleChangeVisibility} />
+          <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.unlisted' defaultMessage='Unlisted mode' /></span>
+        </label>
+
+        <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle' }}>
           <Toggle checked={this.props.sensitive} onChange={this.handleChangeSensitivity} />
           <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark content as sensitive' /></span>
         </label>
diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
index 9897f6505..8aa719476 100644
--- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
@@ -7,7 +7,8 @@ import {
   clearComposeSuggestions,
   fetchComposeSuggestions,
   selectComposeSuggestion,
-  changeComposeSensitivity
+  changeComposeSensitivity,
+  changeComposeVisibility
 } from '../../../actions/compose';
 import { makeGetStatus } from '../../../selectors';
 
@@ -20,6 +21,7 @@ const makeMapStateToProps = () => {
       suggestion_token: state.getIn(['compose', 'suggestion_token']),
       suggestions: state.getIn(['compose', 'suggestions']).toJS(),
       sensitive: state.getIn(['compose', 'sensitive']),
+      unlisted: state.getIn(['compose', 'unlisted']),
       is_submitting: state.getIn(['compose', 'is_submitting']),
       is_uploading: state.getIn(['compose', 'is_uploading']),
       in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to']))
@@ -57,6 +59,10 @@ const mapDispatchToProps = function (dispatch) {
 
     onChangeSensitivity (checked) {
       dispatch(changeComposeSensitivity(checked));
+    },
+
+    onChangeVisibility (checked) {
+      dispatch(changeComposeVisibility(checked));
     }
   }
 };
diff --git a/app/assets/javascripts/components/locales/de.jsx b/app/assets/javascripts/components/locales/de.jsx
index 4e2a70edb..17b74e15d 100644
--- a/app/assets/javascripts/components/locales/de.jsx
+++ b/app/assets/javascripts/components/locales/de.jsx
@@ -34,6 +34,8 @@ const en = {
   "tabs_bar.notifications": "Mitteilungen",
   "compose_form.placeholder": "Worüber möchstest du schreiben?",
   "compose_form.publish": "Veröffentlichen",
+  "compose_form.sensitive": "Medien als sensitiv markieren",
+  "compose_form.unlisted": "Öffentlich nicht auflisten",
   "navigation_bar.settings": "Einstellungen",
   "navigation_bar.public_timeline": "Öffentlich",
   "navigation_bar.logout": "Abmelden",
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index 41a44e3dc..b17e623cb 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -38,6 +38,7 @@ const en = {
   "compose_form.placeholder": "What is on your mind?",
   "compose_form.publish": "Toot",
   "compose_form.sensitive": "Mark content as sensitive",
+  "compose_form.unlisted": "Unlisted mode",
   "navigation_bar.settings": "Settings",
   "navigation_bar.public_timeline": "Public timeline",
   "navigation_bar.logout": "Logout",
diff --git a/app/assets/javascripts/components/locales/es.jsx b/app/assets/javascripts/components/locales/es.jsx
index d4434bba7..d6e61fd9e 100644
--- a/app/assets/javascripts/components/locales/es.jsx
+++ b/app/assets/javascripts/components/locales/es.jsx
@@ -35,6 +35,8 @@ const es = {
   "tabs_bar.notifications": "Notificaciones",
   "compose_form.placeholder": "¿En qué estás pensando?",
   "compose_form.publish": "Publicar",
+  "compose_form.sensitive": null,
+  "compose_form.unlisted": "No listado",
   "navigation_bar.settings": "Ajustes",
   "navigation_bar.public_timeline": "Público",
   "navigation_bar.logout": "Cerrar sesión",
diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx
index c4458a145..968c3f8c3 100644
--- a/app/assets/javascripts/components/locales/fr.jsx
+++ b/app/assets/javascripts/components/locales/fr.jsx
@@ -36,7 +36,8 @@ const fr = {
   "tabs_bar.notifications": "Notifications",
   "compose_form.placeholder": "Qu’avez-vous en tête ?",
   "compose_form.publish": "Pouet",
-  "compose_form.sensitive": "Marquer le contenu comme délicat", 
+  "compose_form.sensitive": "Marquer le contenu comme délicat",
+  "compose_form.unlisted": "Ne pas apparaître dans le fil public",
   "navigation_bar.settings": "Paramètres",
   "navigation_bar.public_timeline": "Public",
   "navigation_bar.logout": "Déconnexion",
diff --git a/app/assets/javascripts/components/locales/hu.jsx b/app/assets/javascripts/components/locales/hu.jsx
index 4a446965c..606fc830f 100644
--- a/app/assets/javascripts/components/locales/hu.jsx
+++ b/app/assets/javascripts/components/locales/hu.jsx
@@ -37,6 +37,7 @@ const hu = {
   "compose_form.placeholder": "Mire gondolsz?",
   "compose_form.publish": "Tülk!",
   "compose_form.sensitive": "Tartalom érzékenynek jelölése",
+  "compose_form.unlisted": "Listázatlan mód",
   "navigation_bar.settings": "Beállítások",
   "navigation_bar.public_timeline": "Nyilvános időfolyam",
   "navigation_bar.logout": "Kijelentkezés",
diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx
index e67bd80ac..0fba15f2c 100644
--- a/app/assets/javascripts/components/locales/pt.jsx
+++ b/app/assets/javascripts/components/locales/pt.jsx
@@ -33,6 +33,8 @@ const pt = {
   "tabs_bar.public": "Público",
   "compose_form.placeholder": "Que estás pensando?",
   "compose_form.publish": "Publicar",
+  "compose_form.sensitive": null,
+  "compose_form.unlisted": null,
   "navigation_bar.settings": "Configurações",
   "navigation_bar.public_timeline": "Timeline Pública",
   "navigation_bar.logout": "Logout",
diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx
index 4abc3e6aa..9d1d53083 100644
--- a/app/assets/javascripts/components/reducers/compose.jsx
+++ b/app/assets/javascripts/components/reducers/compose.jsx
@@ -16,7 +16,8 @@ import {
   COMPOSE_SUGGESTIONS_CLEAR,
   COMPOSE_SUGGESTIONS_READY,
   COMPOSE_SUGGESTION_SELECT,
-  COMPOSE_SENSITIVITY_CHANGE
+  COMPOSE_SENSITIVITY_CHANGE,
+  COMPOSE_VISIBILITY_CHANGE
 } from '../actions/compose';
 import { TIMELINE_DELETE } from '../actions/timelines';
 import { ACCOUNT_SET_SELF } from '../actions/accounts';
@@ -25,6 +26,7 @@ import Immutable from 'immutable';
 const initialState = Immutable.Map({
   mounted: false,
   sensitive: false,
+  unlisted: false,
   text: '',
   in_reply_to: null,
   is_submitting: false,
@@ -91,6 +93,8 @@ export default function compose(state = initialState, action) {
       return state.set('mounted', false);
     case COMPOSE_SENSITIVITY_CHANGE:
       return state.set('sensitive', action.checked);
+    case COMPOSE_VISIBILITY_CHANGE:
+      return state.set('unlisted', action.checked);
     case COMPOSE_CHANGE:
       return state.set('text', action.text);
     case COMPOSE_REPLY:
diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb
new file mode 100644
index 000000000..4a591dc22
--- /dev/null
+++ b/app/controllers/api/oembed_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Api::OembedController < ApiController
+  respond_to :json
+
+  def show
+    @stream_entry = stream_entry_from_url(params[:url])
+    @width        = [300, params[:maxwidth].to_i].min
+    @height       = [200, params[:maxheight].to_i].min
+  end
+
+  private
+
+  def stream_entry_from_url(url)
+    params = Rails.application.routes.recognize_path(url)
+
+    raise ActiveRecord::NotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
+
+    StreamEntry.find(params[:id])
+  end
+end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index a0b15cfbc..8b7690850 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -52,7 +52,7 @@ class Api::V1::StatusesController < ApiController
   end
 
   def create
-    @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], sensitive: params[:sensitive])
+    @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], sensitive: params[:sensitive], unlisted: params[:unlisted])
     render action: :show
   end
 
@@ -73,7 +73,7 @@ class Api::V1::StatusesController < ApiController
     @reblogged_map = { @status.id => false }
 
     RemovalWorker.perform_async(reblog.id)
-    
+
     render action: :show
   end
 
diff --git a/app/models/concerns/obfuscate_filename.rb b/app/controllers/concerns/obfuscate_filename.rb
index dc25cdbc2..9f12cb7e9 100644
--- a/app/models/concerns/obfuscate_filename.rb
+++ b/app/controllers/concerns/obfuscate_filename.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
 module ObfuscateFilename
   extend ActiveSupport::Concern
 
@@ -11,6 +12,6 @@ module ObfuscateFilename
     file = params.dig(*path)
     return if file.nil?
 
-    file.original_filename = "media" + File.extname(file.original_filename)
+    file.original_filename = 'media' + File.extname(file.original_filename)
   end
 end
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 21fbba2af..0276f5fed 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -24,7 +24,7 @@ class Settings::ProfilesController < ApplicationController
   private
 
   def account_params
-    params.require(:account).permit(:display_name, :note, :avatar, :header, :silenced)
+    params.require(:account).permit(:display_name, :note, :avatar, :header)
   end
 
   def set_account
diff --git a/app/helpers/api/oembed_helper.rb b/app/helpers/api/oembed_helper.rb
new file mode 100644
index 000000000..05d5ca216
--- /dev/null
+++ b/app/helpers/api/oembed_helper.rb
@@ -0,0 +1,2 @@
+module Api::OembedHelper
+end
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index 13faaa261..40bbe0491 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -38,7 +38,7 @@ module AtomBuilderHelper
   end
 
   def verb(xml, verb)
-    xml['activity'].send('verb', "http://activitystrea.ms/schema/1.0/#{verb}")
+    xml['activity'].send('verb', TagManager::VERBS[verb])
   end
 
   def content(xml, content)
@@ -62,7 +62,7 @@ module AtomBuilderHelper
   end
 
   def object_type(xml, type)
-    xml['activity'].send('object-type', "http://activitystrea.ms/schema/1.0/#{type}")
+    xml['activity'].send('object-type', TagManager::TYPES[type])
   end
 
   def uri(xml, uri)
@@ -108,7 +108,7 @@ module AtomBuilderHelper
   end
 
   def link_mention(xml, account)
-    xml.link(rel: 'mentioned', href: TagManager.instance.uri_for(account))
+    xml.link(:rel => 'mentioned', :href => TagManager.instance.uri_for(account), 'ostatus:object-type' => TagManager::TYPES[:person])
   end
 
   def link_enclosure(xml, media)
@@ -139,6 +139,11 @@ module AtomBuilderHelper
     end
   end
 
+  def link_visibility(xml, item)
+    return unless item.respond_to?(:visibility) && item.public_visibility?
+    xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection])
+  end
+
   def include_author(xml, account)
     object_type      xml, :person
     uri              xml, TagManager.instance.uri_for(account)
@@ -189,6 +194,8 @@ module AtomBuilderHelper
             include_author xml, stream_entry.target.account
           end
 
+          link_visibility xml, stream_entry.target
+
           stream_entry.target.mentions.each do |mention|
             link_mention xml, mention.account
           end
@@ -204,25 +211,34 @@ module AtomBuilderHelper
       end
     end
 
+    link_visibility xml, stream_entry.activity
+
     stream_entry.mentions.each do |mentioned|
       link_mention xml, mentioned
     end
 
-    if stream_entry.activity.is_a?(Status)
-      stream_entry.activity.media_attachments.each do |media|
-        link_enclosure xml, media
-      end
+    return unless stream_entry.activity.is_a?(Status)
 
-      stream_entry.activity.tags.each do |tag|
-        category xml, tag
-      end
+    stream_entry.activity.media_attachments.each do |media|
+      link_enclosure xml, media
+    end
+
+    stream_entry.activity.tags.each do |tag|
+      category xml, tag
     end
   end
 
   private
 
   def root_tag(xml, tag, &block)
-    xml.send(tag, { :xmlns => 'http://www.w3.org/2005/Atom', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', 'xmlns:media' => 'http://purl.org/syndication/atommedia' }, &block)
+    xml.send(tag, {
+               'xmlns'          => TagManager::XMLNS,
+               'xmlns:thr'      => TagManager::THR_XMLNS,
+               'xmlns:activity' => TagManager::AS_XMLNS,
+               'xmlns:poco'     => TagManager::POCO_XMLNS,
+               'xmlns:media'    => TagManager::MEDIA_XMLNS,
+               'xmlns:ostatus'  => TagManager::OS_XMLNS,
+             }, &block)
   end
 
   def single_link_avatar(xml, account, size, px)
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index fcc4b3259..bd4cbbbb0 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -6,6 +6,37 @@ class TagManager
   include Singleton
   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:     'delete',
+    follow:     'http://activitystrea.ms/schema/1.0/follow',
+    unfollow:   'http://ostatus.org/schema/1.0/unfollow',
+  }.freeze
+
+  TYPES = {
+    activity:   'http://activitystrea.ms/schema/1.0/activity',
+    note:       'http://activitystrea.ms/schema/1.0/note',
+    comment:    'http://activitystrea.ms/schema/1.0/comment',
+    person:     'http://activitystrea.ms/schema/1.0/person',
+    collection: 'http://activitystrea.ms/schema/1.0/collection',
+    group:      'http://activitystrea.ms/schema/1.0/group',
+  }.freeze
+
+  COLLECTIONS = {
+    public: 'http://activityschema.org/collection/public',
+  }.freeze
+
+  XMLNS       = 'http://www.w3.org/2005/Atom'
+  MEDIA_XMLNS = 'http://purl.org/syndication/atommedia'
+  AS_XMLNS    = 'http://activitystrea.ms/spec/1.0/'
+  THR_XMLNS   = 'http://purl.org/syndication/thread/1.0'
+  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'
+
   def unique_tag(date, id, type)
     "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index 8f65a3ecc..87d8249b1 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -5,6 +5,8 @@ class Status < ApplicationRecord
   include Streamable
   include Cacheable
 
+  enum visibility: [:public, :unlisted], _suffix: :visibility
+
   belongs_to :account, inverse_of: :statuses
 
   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies
@@ -100,6 +102,7 @@ class Status < ApplicationRecord
 
     def as_public_timeline(account = nil)
       query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
+              .where(visibility: :public)
               .where('accounts.silenced = FALSE')
               .where('statuses.in_reply_to_id IS NULL')
               .where('statuses.reblog_of_id IS NULL')
@@ -110,6 +113,7 @@ class Status < ApplicationRecord
     def as_tag_timeline(tag, account = nil)
       query = tag.statuses
                  .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
+                 .where(visibility: :public)
                  .where('accounts.silenced = FALSE')
                  .where('statuses.in_reply_to_id IS NULL')
                  .where('statuses.reblog_of_id IS NULL')
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 40d8a0fee..ea9588bec 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -8,7 +8,7 @@ class FanOutOnWriteService < BaseService
     deliver_to_followers(status)
     deliver_to_mentioned(status)
 
-    return if status.account.silenced?
+    return if status.account.silenced? || !status.public_visibility?
 
     deliver_to_hashtags(status)
     deliver_to_public(status)
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 979a157e9..9e0ced129 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -10,7 +10,7 @@ class PostStatusService < BaseService
   # @option [Enumerable] :media_ids Optional array of media IDs to attach
   # @return [Status]
   def call(account, text, in_reply_to = nil, options = {})
-    status = account.statuses.create!(text: text, thread: in_reply_to, sensitive: options[:sensitive])
+    status = account.statuses.create!(text: text, thread: in_reply_to, sensitive: options[:sensitive], visibility: options[:unlisted] ? :unlisted : :public)
     attach_media(status, options[:media_ids])
     process_mentions_service.call(status)
     process_hashtags_service.call(status)
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index a7a4cb2b0..3aa0ceff8 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
 class ProcessFeedService < BaseService
-  ACTIVITY_NS = 'http://activitystrea.ms/spec/1.0/'
-  THREAD_NS   = 'http://purl.org/syndication/thread/1.0'
-
   def call(body, account)
     xml = Nokogiri::XML(body)
     xml.encoding = 'utf-8'
@@ -15,12 +12,12 @@ class ProcessFeedService < BaseService
   private
 
   def update_author(xml, account)
-    return if xml.at_xpath('/xmlns:feed').nil?
-    UpdateRemoteProfileService.new.call(xml.at_xpath('/xmlns:feed'), account, true)
+    return if xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS).nil?
+    UpdateRemoteProfileService.new.call(xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS), account, true)
   end
 
   def process_entries(xml, account)
-    xml.xpath('//xmlns:entry').reverse_each.map { |entry| ProcessEntry.new.call(entry, account) }.compact
+    xml.xpath('//xmlns:entry', xmlns: TagManager::XMLNS).reverse_each.map { |entry| ProcessEntry.new.call(entry, account) }.compact
   end
 
   class ProcessEntry
@@ -48,7 +45,7 @@ class ProcessFeedService < BaseService
       status = status_from_xml(@xml)
 
       if verb == :share
-        original_status = status_from_xml(@xml.at_xpath('.//activity:object', activity: ACTIVITY_NS))
+        original_status = status_from_xml(@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS))
         status.reblog   = original_status
 
         if original_status.nil?
@@ -138,9 +135,15 @@ class ProcessFeedService < BaseService
 
     def mentions_from_xml(parent, xml)
       processed_account_ids = []
+      public_visibility     = false
 
-      xml.xpath('./xmlns:link[@rel="mentioned"]').each do |link|
-        next if link['href'] == 'http://activityschema.org/collection/public'
+      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
 
         url = Addressable::URI.parse(link['href'])
 
@@ -160,15 +163,18 @@ 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)
-      tags = xml.xpath('./xmlns:category').map { |category| category['term'] }.select { |t| !t.blank? }
+      tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select { |t| !t.blank? }
       ProcessHashtagsService.new.call(parent, tags)
     end
 
     def media_from_xml(parent, xml)
-      xml.xpath('./xmlns:link[@rel="enclosure"]').each do |link|
+      xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link|
         next unless link['href']
 
         media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
@@ -183,52 +189,52 @@ class ProcessFeedService < BaseService
     end
 
     def id(xml = @xml)
-      xml.at_xpath('./xmlns:id').content
+      xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content
     end
 
     def verb(xml = @xml)
-      raw = xml.at_xpath('./activity:verb', activity: ACTIVITY_NS).content
-      raw.gsub('http://activitystrea.ms/schema/1.0/', '').gsub('http://ostatus.org/schema/1.0/', '').to_sym
+      raw = xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content
+      TagManager::VERBS.key(raw)
     rescue
       :post
     end
 
     def type(xml = @xml)
-      raw = xml.at_xpath('./activity:object-type', activity: ACTIVITY_NS).content
-      raw.gsub('http://activitystrea.ms/schema/1.0/', '').gsub('http://ostatus.org/schema/1.0/', '').to_sym
+      raw = xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content
+      TagManager::TYPES.key(raw)
     rescue
       :activity
     end
 
     def url(xml = @xml)
-      link = xml.at_xpath('./xmlns:link[@rel="alternate"]')
+      link = xml.at_xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS)
       link.nil? ? nil : link['href']
     end
 
     def content(xml = @xml)
-      xml.at_xpath('./xmlns:content').content
+      xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content
     end
 
     def published(xml = @xml)
-      xml.at_xpath('./xmlns:published').content
+      xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content
     end
 
     def thread?(xml = @xml)
-      !xml.at_xpath('./thr:in-reply-to', thr: THREAD_NS).nil?
+      !xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil?
     end
 
     def thread(xml = @xml)
-      thr = xml.at_xpath('./thr:in-reply-to', thr: THREAD_NS)
+      thr = xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS)
       [thr['ref'], thr['href']]
     end
 
     def account?(xml = @xml)
-      !xml.at_xpath('./xmlns:author').nil?
+      !xml.at_xpath('./xmlns:author', xmlns: TagManager::XMLNS).nil?
     end
 
     def acct(xml = @xml)
-      username = xml.at_xpath('./xmlns:author/xmlns:name').content
-      url      = xml.at_xpath('./xmlns:author/xmlns:uri').content
+      username = xml.at_xpath('./xmlns:author/xmlns:name', xmlns: TagManager::XMLNS).content
+      url      = xml.at_xpath('./xmlns:author/xmlns:uri', xmlns: TagManager::XMLNS).content
       domain   = Addressable::URI.parse(url).host
 
       "#{username}@#{domain}"
diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb
index 6b2f6e2d2..129b2a2be 100644
--- a/app/services/process_interaction_service.rb
+++ b/app/services/process_interaction_service.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class ProcessInteractionService < BaseService
-  ACTIVITY_NS = 'http://activitystrea.ms/spec/1.0/'
-
   # Record locally the remote interaction with our user
   # @param [String] envelope Salmon envelope
   # @param [Account] target_account Account the Salmon was addressed to
@@ -14,8 +12,8 @@ class ProcessInteractionService < BaseService
 
     return unless contains_author?(xml)
 
-    username = xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:name').content
-    url      = xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:uri').content
+    username = xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:name', xmlns: TagManager::XMLNS).content
+    url      = xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:uri', xmlns: TagManager::XMLNS).content
     domain   = Addressable::URI.parse(url).host
     account  = Account.find_by(username: username, domain: domain)
 
@@ -26,7 +24,7 @@ class ProcessInteractionService < BaseService
     end
 
     if salmon.verify(envelope, account.keypair)
-      update_remote_profile_service.call(xml.at_xpath('/xmlns:entry'), account, true)
+      update_remote_profile_service.call(xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS), account, true)
 
       case verb(xml)
       when :follow
@@ -50,16 +48,17 @@ class ProcessInteractionService < BaseService
   private
 
   def contains_author?(xml)
-    !(xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:name').nil? || xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:uri').nil?)
+    !(xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:name', xmlns: TagManager::XMLNS).nil? || xml.at_xpath('/xmlns:entry/xmlns:author/xmlns:uri', xmlns: TagManager::XMLNS).nil?)
   end
 
   def mentions_account?(xml, account)
-    xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]').each { |mention_link| return true if mention_link.attribute('href').value == TagManager.instance.url_for(account) }
+    xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each { |mention_link| return true if mention_link.attribute('href').value == TagManager.instance.url_for(account) }
     false
   end
 
   def verb(xml)
-    xml.at_xpath('//activity:verb', activity: ACTIVITY_NS).content.gsub('http://activitystrea.ms/schema/1.0/', '').gsub('http://ostatus.org/schema/1.0/', '').to_sym
+    raw = xml.at_xpath('//activity:verb', activity: TagManager::AS_XMLNS).content
+    TagManager::VERBS.key(raw)
   rescue
     :post
   end
@@ -74,7 +73,7 @@ class ProcessInteractionService < BaseService
   end
 
   def delete_post!(xml, account)
-    status = Status.find(xml.at_xpath('//xmlns:id').content)
+    status = Status.find(xml.at_xpath('//xmlns:id', xmlns: TagManager::XMLNS).content)
 
     return if status.nil?
 
@@ -96,7 +95,7 @@ class ProcessInteractionService < BaseService
   end
 
   def activity_id(xml)
-    xml.at_xpath('//activity:object', activity: ACTIVITY_NS).at_xpath('./xmlns:id').content
+    xml.at_xpath('//activity:object', activity: TagManager::AS_XMLNS).at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content
   end
 
   def salmon
diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb
index 56b25816f..66d25dfeb 100644
--- a/app/services/update_remote_profile_service.rb
+++ b/app/services/update_remote_profile_service.rb
@@ -1,19 +1,16 @@
 # frozen_string_literal: true
 
 class UpdateRemoteProfileService < BaseService
-  POCO_NS = 'http://portablecontacts.net/spec/1.0'
-  DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
-
   def call(xml, account, resubscribe = false)
     return if xml.nil?
 
-    author_xml = xml.at_xpath('./xmlns:author') || xml.at_xpath('./dfrn:owner', dfrn: DFRN_NS)
-    hub_link   = xml.at_xpath('./xmlns:link[@rel="hub"]')
+    author_xml = xml.at_xpath('./xmlns:author', xmlns: TagManager::XMLNS) || xml.at_xpath('./dfrn:owner', dfrn: TagManager::DFRN_XMLNS)
+    hub_link   = xml.at_xpath('./xmlns:link[@rel="hub"]', xmlns: TagManager::XMLNS)
 
     unless author_xml.nil?
-      account.display_name      = author_xml.at_xpath('./poco:displayName', poco: POCO_NS).content unless author_xml.at_xpath('./poco:displayName', poco: POCO_NS).nil?
-      account.note              = author_xml.at_xpath('./poco:note', poco: POCO_NS).content unless author_xml.at_xpath('./poco:note').nil?
-      account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]')['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]').nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]')['href'].blank?
+      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.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?
     end
 
     old_hub_url     = account.hub_url
diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl
new file mode 100644
index 000000000..e035bc13c
--- /dev/null
+++ b/app/views/api/oembed/show.json.rabl
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+object @stream_entry
+
+node(:type) { 'rich' }
+node(:version) { '1.0' }
+node(:title, &:title)
+node(:author_name) { |entry| entry.account.display_name.blank? ? entry.account.username : entry.account.display_name }
+node(:author_url) { |entry| account_url(entry.account) }
+node(:provider_name) { Rails.configuration.x.local_domain }
+node(:provider_url) { root_url }
+node(:cache_age) { 86_400 }
+node(:html, &:content)
+node(:width) { @width }
+node(:height) { @height }
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index e5f8a46c4..c2f1adb12 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -8,7 +8,6 @@
   = f.input :note, placeholder: t('simple_form.labels.defaults.note')
   = f.input :avatar, wrapper: :with_label
   = f.input :header, wrapper: :with_label
-  = f.input :silenced, as: :boolean, wrapper: :with_label
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index 2c6de32d9..e628dd99d 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -1,8 +1,13 @@
 - content_for :header_tags do
   %link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/
+  %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/
+
   %meta{ name: 'og:site_name', content: 'Mastodon' }/
   %meta{ name: 'og:type', content: 'article' }/
+  %meta{ name: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
   %meta{ name: 'og:article:author', content: @account.username }/
+  %meta{ name: 'og:description', content: @stream_entry.activity.content }/
+  %meta{ name: 'og:image', content: @stream_entry.activity.is_a?(Status) && @stream_entry.activity.media_attachments.size > 0 ? full_asset_url(@stream_entry.activity.media_attachments.first.file.url(:preview)) : full_asset_url(@account.avatar.url(:large)) }/
 
 .activity-stream.activity-stream-headless
   = render partial: @type, locals: { @type.to_sym => @stream_entry.activity, include_threads: true }