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']),
- unlisted: getState().getIn(['compose', 'unlisted'])
+ visibility: getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public'
}).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data }));
def show
respond_to do |format|
format.html do
- @statuses = @account.statuses.order('id desc').paginate_by_max_id(20, params[:max_id], params[:since_id])
+ @statuses = @account.statuses.permitted_for(@account, current_account).order('id desc').paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
end
format.atom do
- @entries = @account.stream_entries.order('id desc').with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+ @entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
end
end
end
respond_to :json
- def show
- end
+ def show; end
def verify_credentials
@account = current_user.account
end
def statuses
- @statuses = @account.statuses.paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id])
+ @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
set_maps(@statuses)
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], unlisted: params[:unlisted])
+ @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], visibility: params[:visibility])
render action: :show
end
def set_status
@status = Status.find(params[:id])
+ raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
end
end
return gone if @stream_entry.activity.nil?
if @stream_entry.activity_type == 'Status'
- @ancestors = @stream_entry.activity.ancestors
- @descendants = @stream_entry.activity.descendants
+ @ancestors = @stream_entry.activity.ancestors(current_account)
+ @descendants = @stream_entry.activity.descendants(current_account)
end
end
end
def set_stream_entry
- @stream_entry = @account.stream_entries.find(params[:id])
+ @stream_entry = @account.stream_entries.where(hidden: false).find(params[:id])
@type = @stream_entry.activity_type.downcase
end
# frozen_string_literal: true
class Block < ApplicationRecord
+ include Streamable
+
belongs_to :account
belongs_to :target_account, class_name: 'Account'
validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id }
+
+ def verb
+ destroyed? ? :unblock : :block
+ end
+
+ def target
+ target_account
+ end
+
+ def object_type
+ :person
+ end
+
+ def hidden?
+ true
+ end
+
+ def title
+ destroyed? ? "#{account.acct} is no longer blocking #{target_account.acct}" : "#{account.acct} blocked #{target_account.acct}"
+ end
end
super
end
+ def hidden?
+ false
+ end
+
after_create do
- account.stream_entries.create!(activity: self) if account.local?
+ account.stream_entries.create!(activity: self, hidden: hidden?) if account.local?
end
end
end
include Streamable
include Cacheable
- enum visibility: [:public, :unlisted], _suffix: :visibility
+ enum visibility: [:public, :unlisted, :private], _suffix: :visibility
belongs_to :account, inverse_of: :statuses
content
end
- def reblogs_count
- attributes['reblogs_count'] || reblogs.count
+ def hidden?
+ private_visibility?
end
- def favourites_count
- attributes['favourites_count'] || favourites.count
+ def permitted?(other_account = nil)
+ private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : true
end
def ancestors(account = nil)
ids = (Status.find_by_sql(['WITH RECURSIVE search_tree(id, in_reply_to_id, path) AS (SELECT id, in_reply_to_id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id FROM search_tree JOIN statuses ON statuses.id = search_tree.in_reply_to_id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path DESC', id]) - [self]).pluck(:id)
statuses = Status.where(id: ids).with_includes.group_by(&:id)
results = ids.map { |id| statuses[id].first }
- results = results.reject { |status| account.blocking?(status.account) } unless account.nil?
+ results = results.reject { |status| filter_from_context?(status, account) }
results
end
ids = (Status.find_by_sql(['WITH RECURSIVE search_tree(id, path) AS (SELECT id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, path || statuses.id FROM search_tree JOIN statuses ON statuses.in_reply_to_id = search_tree.id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path', id]) - [self]).pluck(:id)
statuses = Status.where(id: ids).with_includes.group_by(&:id)
results = ids.map { |id| statuses[id].first }
- results = results.reject { |status| account.blocking?(status.account) } unless account.nil?
+ results = results.reject { |status| filter_from_context?(status, account) }
results
end
select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
end
+ def permitted_for(target_account, account)
+ if account&.id == target_account.id || account&.following?(target_account)
+ self
+ else
+ where.not(visibility: :private)
+ end
+ end
+
def reload_stale_associations!(cached_items)
account_ids = []
before_validation do
text.strip!
self.in_reply_to_account_id = thread.account_id if reply?
+ self.visibility = :public if visibility.nil?
+ end
+
+ private
+
+ def filter_from_context?(status, account)
+ account&.blocking?(status.account) || !status.permitted?(account)
end
end
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id'
belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id'
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id'
+ belongs_to :block, foreign_type: 'Block', foreign_key: 'activity_id'
validates :account, :activity, presence: true
end
def targeted?
- [:follow, :share, :favorite].include? verb
+ [:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb
end
def target
end
def activity
- send(activity_type.downcase.to_sym)
+ !new_record? ? send(activity_type.downcase) : super
end
private
# @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], visibility: options[:unlisted] ? :unlisted : :public)
+ status = account.statuses.create!(text: text, thread: in_reply_to, sensitive: options[:sensitive], visibility: options[:visibility])
attach_media(status, options[:media_ids])
process_mentions_service.call(status)
process_hashtags_service.call(status)
# @param [Status] reblogged_status Status to be reblogged
# @return [Status]
def call(account, reblogged_status)
+ raise ActiveRecord::RecordInvalid if reblogged_status.private_visibility?
+
reblog = account.statuses.create!(reblog: reblogged_status, text: '')
DistributionWorker.perform_async(reblog.id)
-attributes :id, :created_at, :in_reply_to_id, :sensitive
+attributes :id, :created_at, :in_reply_to_id, :sensitive, :visibility
node(:uri) { |status| TagManager.instance.uri_for(status) }
node(:content) { |status| Formatter.instance.format(status) }
node(:url) { |status| TagManager.instance.url_for(status) }
-node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs_count }
-node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count }
+node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs.count }
+node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites.count }
child :account do
extends 'api/v1/accounts/show'
--- /dev/null
+class AddHiddenToStreamEntries < ActiveRecord::Migration[5.0]
+ def change
+ add_column :stream_entries, :hidden, :boolean, null: false, default: false
+ end
+end
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161205214545) do
+ActiveRecord::Schema.define(version: 20161221152630) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.integer "account_id"
t.integer "activity_id"
t.string "activity_type"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "hidden", default: false, null: false
t.index ["account_id"], name: "index_stream_entries_on_account_id", using: :btree
t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type", using: :btree
end
<html lang="en">
<head>
<meta charset="utf-8">
- <title>The page you were looking for doesn't exist (404)</title>
+ <title>The page you were looking for doesn't exist</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
+ <link href="https://fonts.googleapis.com/css?family=Roboto:400" rel="stylesheet">
<style>
- body {
- background-color: #EFEFEF;
- color: #2E2F30;
- text-align: center;
- font-family: arial, sans-serif;
- margin: 0;
- }
+ body {
+ font-family: 'Roboto', sans-serif;
+ background: #282c37;
+ color: #9baec8;
+ text-align: center;
+ margin: 0;
+ padding: 20px;
+ }
- div.dialog {
- width: 95%;
- max-width: 33em;
- margin: 4em auto 0;
- }
+ .dialog img {
+ display: block;
+ margin: 20px auto;
+ margin-top: 50px;
+ max-width: 600px;
+ width: 100%;
+ height: auto;
+ }
- div.dialog > div {
- border: 1px solid #CCC;
- border-right-color: #999;
- border-left-color: #999;
- border-bottom-color: #BBB;
- border-top: #B00100 solid 4px;
- border-top-left-radius: 9px;
- border-top-right-radius: 9px;
- background-color: white;
- padding: 7px 12% 0;
- box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
- }
-
- h1 {
- font-size: 100%;
- color: #730E15;
- line-height: 1.5em;
- }
-
- div.dialog > p {
- margin: 0 0 1em;
- padding: 1em;
- background-color: #F7F7F7;
- border: 1px solid #CCC;
- border-right-color: #999;
- border-left-color: #999;
- border-bottom-color: #999;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- border-top-color: #DADADA;
- color: #666;
- box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
- }
+ .dialog h1 {
+ font: 20px/28px 'Roboto', sans-serif;
+ font-weight: 400;
+ }
</style>
</head>
<body>
- <!-- This file lives in public/404.html -->
<div class="dialog">
+ <img src="/oops.png" alt="Mastodon" />
+
<div>
- <h1>The page you were looking for doesn't exist.</h1>
- <p>You may have mistyped the address or the page may have moved.</p>
+ <h1>The page you were looking for doesn't exist</h1>
</div>
- <p>If you are the application owner check the logs for more information.</p>
</div>
</body>
</html>
+++ /dev/null
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="utf-8">
- <title>The change you wanted was rejected (422)</title>
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <style>
- body {
- background-color: #EFEFEF;
- color: #2E2F30;
- text-align: center;
- font-family: arial, sans-serif;
- margin: 0;
- }
-
- div.dialog {
- width: 95%;
- max-width: 33em;
- margin: 4em auto 0;
- }
-
- div.dialog > div {
- border: 1px solid #CCC;
- border-right-color: #999;
- border-left-color: #999;
- border-bottom-color: #BBB;
- border-top: #B00100 solid 4px;
- border-top-left-radius: 9px;
- border-top-right-radius: 9px;
- background-color: white;
- padding: 7px 12% 0;
- box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
- }
-
- h1 {
- font-size: 100%;
- color: #730E15;
- line-height: 1.5em;
- }
-
- div.dialog > p {
- margin: 0 0 1em;
- padding: 1em;
- background-color: #F7F7F7;
- border: 1px solid #CCC;
- border-right-color: #999;
- border-left-color: #999;
- border-bottom-color: #999;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- border-top-color: #DADADA;
- color: #666;
- box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
- }
- </style>
-</head>
-
-<body>
- <!-- This file lives in public/422.html -->
- <div class="dialog">
- <div>
- <h1>The change you wanted was rejected.</h1>
- <p>Maybe you tried to change something you didn't have access to.</p>
- </div>
- <p>If you are the application owner check the logs for more information.</p>
- </div>
-</body>
-</html>
end
it 'updates the reblogs count' do
- expect(status.reblogs_count).to eq 1
+ expect(status.reblogs.count).to eq 1
end
it 'updates the reblogged attribute' do
end
it 'updates the reblogs count' do
- expect(status.reblogs_count).to eq 0
+ expect(status.reblogs.count).to eq 0
end
it 'updates the reblogged attribute' do
end
it 'updates the favourites count' do
- expect(status.favourites_count).to eq 1
+ expect(status.favourites.count).to eq 1
end
it 'updates the favourited attribute' do
end
it 'updates the favourites count' do
- expect(status.favourites_count).to eq 0
+ expect(status.favourites.count).to eq 0
end
it 'updates the favourited attribute' do