diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2016-12-22 23:03:57 +0100 |
---|---|---|
committer | Eugen Rochko <eugen@zeonfederated.com> | 2016-12-22 23:03:57 +0100 |
commit | b891a81008d2cf595cb37432a8e1f36606db16d6 (patch) | |
tree | e3b083966fc14dda46a2ec75586fdf566c2585aa | |
parent | 2d2154ba75279186b064c887452b7d6ee70b8ba2 (diff) |
Follow call on locked account creates follow request instead
Reflect "requested" relationship in API and UI Reflect inability of private posts to be reblogged in the UI Disable Webfinger for locked accounts
24 files changed, 145 insertions, 47 deletions
diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx index d8f00f5d8..e9a7228e4 100644 --- a/app/assets/javascripts/components/components/icon_button.jsx +++ b/app/assets/javascripts/components/components/icon_button.jsx @@ -5,17 +5,19 @@ const IconButton = React.createClass({ propTypes: { title: React.PropTypes.string.isRequired, icon: React.PropTypes.string.isRequired, - onClick: React.PropTypes.func.isRequired, + onClick: React.PropTypes.func, size: React.PropTypes.number, active: React.PropTypes.bool, style: React.PropTypes.object, - activeStyle: React.PropTypes.object + activeStyle: React.PropTypes.object, + disabled: React.PropTypes.bool }, getDefaultProps () { return { size: 18, - active: false + active: false, + disabled: false }; }, @@ -23,8 +25,10 @@ const IconButton = React.createClass({ handleClick (e) { e.preventDefault(); - this.props.onClick(); - e.stopPropagation(); + + if (!this.props.disabled) { + this.props.onClick(); + } }, render () { @@ -37,7 +41,6 @@ const IconButton = React.createClass({ width: `${this.props.size * 1.28571429}px`, height: `${this.props.size}px`, lineHeight: `${this.props.size}px`, - cursor: 'pointer', ...this.props.style }; @@ -46,7 +49,7 @@ const IconButton = React.createClass({ } return ( - <button aria-label={this.props.title} title={this.props.title} className={`icon-button ${this.props.active ? 'active' : ''}`} onClick={this.handleClick} style={style}> + <button aria-label={this.props.title} title={this.props.title} className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''}`} onClick={this.handleClick} style={style}> <i className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> </button> ); diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index 9c6d13bdf..80b51456e 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -76,7 +76,7 @@ const StatusActionBar = React.createClass({ return ( <div style={{ marginTop: '10px', overflow: 'hidden' }}> <div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div> - <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> + <div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> <div style={{ width: '18px', height: '18px', float: 'left' }}> diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index b890e15c1..fe400e50b 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -8,6 +8,7 @@ import IconButton from '../../../components/icon_button'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' } }); const Header = React.createClass({ @@ -36,11 +37,19 @@ const Header = React.createClass({ } if (me !== account.get('id')) { - actionBtn = ( - <div style={{ position: 'absolute', top: '10px', left: '20px' }}> - <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> - </div> - ); + if (account.getIn(['relationship', 'requested'])) { + actionBtn = ( + <div style={{ position: 'absolute', top: '10px', left: '20px' }}> + <IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> + </div> + ); + } else { + actionBtn = ( + <div style={{ position: 'absolute', top: '10px', left: '20px' }}> + <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> + </div> + ); + } } const content = { __html: emojify(account.get('note')) }; diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx index 1f46b956e..00b1dd415 100644 --- a/app/assets/javascripts/components/features/status/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx @@ -60,7 +60,7 @@ const ActionBar = React.createClass({ return ( <div style={{ background: '#2f3441', display: 'flex', flexDirection: 'row', borderTop: '1px solid #363c4b', borderBottom: '1px solid #363c4b', padding: '10px 0' }}> <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div> - <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> + <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon='retweet' onClick={this.handleReblogClick} /></div> <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> <div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} /></div> </div> diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 210e722cc..1689193a8 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -44,13 +44,14 @@ color: #616b86; border: none; background: transparent; + cursor: pointer; &:hover { color: #717b98; } &.disabled { - color: #535b72; + color: #454b5e; cursor: default; } diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 81270edf6..cf9b4fba6 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -14,6 +14,12 @@ code { margin-bottom: 15px; } + .hint { + display: block; + color: rgba(255, 255, 255, 0.8); + font-size: 12px; + } + .input.file, .input.select { padding: 15px 0; margin-bottom: 0; @@ -59,6 +65,10 @@ code { top: 1px; margin: 0; } + + .hint { + padding-left: 25px; + } } input[type=text], input[type=email], input[type=password], textarea { diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index de53a9602..05ff806c5 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -84,10 +84,12 @@ class Api::V1::AccountsController < ApiController def relationships ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i] + @accounts = Account.where(id: ids).select('id') @following = Account.following_map(ids, current_user.account_id) @followed_by = Account.followed_by_map(ids, current_user.account_id) @blocking = Account.blocking_map(ids, current_user.account_id) + @requested = Account.requested_map(ids, current_user.account_id) end def search @@ -109,5 +111,6 @@ class Api::V1::AccountsController < ApiController @following = Account.following_map([@account.id], current_user.account_id) @followed_by = Account.followed_by_map([@account.id], current_user.account_id) @blocking = Account.blocking_map([@account.id], current_user.account_id) + @requested = Account.requested_map([@account.id], current_user.account_id) end end diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 438d51a84..3f60bb0c4 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -43,8 +43,10 @@ class StreamEntriesController < ApplicationController end def set_stream_entry - @stream_entry = @account.stream_entries.where(hidden: false).find(params[:id]) + @stream_entry = @account.stream_entries.find(params[:id]) @type = @stream_entry.activity_type.downcase + + raise ActiveRecord::RecordNotFound if @stream_entry.hidden? && (@stream_entry.activity_type != 'Status' || (@stream_entry.activity_type == 'Status' && !@stream_entry.activity.permitted?(current_account))) end def check_account_suspension diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb index 9e0277860..c3c38063c 100644 --- a/app/controllers/xrd_controller.rb +++ b/app/controllers/xrd_controller.rb @@ -13,7 +13,7 @@ class XrdController < ApplicationController end def webfinger - @account = Account.find_local!(username_from_resource) + @account = Account.where(locked: false).find_local!(username_from_resource) @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}" @magic_key = pem_to_magic_key(@account.keypair.public_key) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index e08f9a0da..c07e4b05f 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -39,6 +39,16 @@ class FeedManager redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}") end + def merge_into_timeline(from_account, into_account) + timeline_key = key(:home, into_account.id) + + from_account.statuses.limit(MAX_ITEMS).each do |status| + redis.zadd(timeline_key, status.id, status.id) + end + + trim(:home, into_account.id) + end + def inline_render(target_account, template, object) rabl_scope = Class.new do include RoutingHelper diff --git a/app/models/account.rb b/app/models/account.rb index aa904588b..273c09833 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -34,6 +34,8 @@ class Account < ApplicationRecord has_many :notifications, inverse_of: :account, dependent: :destroy # Follow relations + has_many :follow_requests, dependent: :destroy + has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', dependent: :destroy has_many :passive_relationships, class_name: 'Follow', foreign_key: 'target_account_id', dependent: :destroy @@ -179,6 +181,10 @@ class Account < ApplicationRecord def blocking_map(target_account_ids, account_id) Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h end + + def requested_map(target_account_ids, account_id) + FollowRequest.where(target_account_id: target_account_ids).where(account_id: account_id).map { |r| [r.target_account_id, true] }.to_h + end end before_create do diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb new file mode 100644 index 000000000..132316fb4 --- /dev/null +++ b/app/models/follow_request.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class FollowRequest < ApplicationRecord + 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 authorize! + account.follow!(target_account) + FeedManager.instance.merge_into_timeline(target_account, account) + destroy! + end + + def reject! + destroy! + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 1e6298a0e..033ae0529 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -170,7 +170,7 @@ class Status < ApplicationRecord text.strip! self.reblog = reblog.reblog if reblog? && reblog.reblog? self.in_reply_to_account_id = thread.account_id if reply? - self.visibility = :public if visibility.nil? + self.visibility = (account.locked? ? :private : :public) if visibility.nil? end private diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 02baa6553..a73ec344d 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -10,6 +10,20 @@ class FollowService < BaseService raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? raise Mastodon::NotPermitted if target_account.blocking?(source_account) + if target_account.locked? + request_follow(source_account, target_account) + else + direct_follow(source_account, target_account) + end + end + + private + + def request_follow(source_account, target_account) + FollowRequest.create!(account: source_account, target_account: target_account) + end + + def direct_follow(source_account, target_account) follow = source_account.follow!(target_account) if target_account.local? @@ -19,25 +33,12 @@ class FollowService < BaseService NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) end - merge_into_timeline(target_account, source_account) - + FeedManager.instance.merge_into_timeline(target_account, source_account) Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id) follow end - private - - def merge_into_timeline(from_account, into_account) - timeline_key = FeedManager.instance.key(:home, into_account.id) - - from_account.statuses.find_each do |status| - redis.zadd(timeline_key, status.id, status.id) - end - - FeedManager.instance.trim(:home, into_account.id) - end - def redis Redis.current end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 1a78b8f69..23b35ffd2 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -6,7 +6,7 @@ class ReblogService < BaseService # @param [Status] reblogged_status Status to be reblogged # @return [Status] def call(account, reblogged_status) - raise ActiveRecord::RecordInvalid if reblogged_status.private_visibility? + raise Mastodon::NotPermitted if reblogged_status.private_visibility? reblog = account.statuses.create!(reblog: reblogged_status, text: '') diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl index 84043e9cd..22b37586e 100644 --- a/app/views/api/v1/accounts/relationship.rabl +++ b/app/views/api/v1/accounts/relationship.rabl @@ -4,3 +4,4 @@ attribute :id node(:following) { |account| @following[account.id] || false } node(:followed_by) { |account| @followed_by[account.id] || false } node(:blocking) { |account| @blocking[account.id] || false } +node(:requested) { |account| @requested[account.id] || false } diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl index 22cb87f6c..151a5080d 100644 --- a/app/views/api/v1/accounts/show.rabl +++ b/app/views/api/v1/accounts/show.rabl @@ -1,11 +1,11 @@ object @account -attributes :id, :username, :acct, :display_name +attributes :id, :username, :acct, :display_name, :locked node(:note) { |account| Formatter.instance.simplified_format(account) } node(:url) { |account| TagManager.instance.url_for(account) } -node(:avatar) { |account| full_asset_url(account.avatar.url( :original)) } -node(:header) { |account| full_asset_url(account.header.url( :original)) } +node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) } +node(:header) { |account| full_asset_url(account.header.url(:original)) } node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : (account.try(:followers_count) || account.followers.count) } node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : (account.try(:following_count) || account.following.count) } node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : (account.try(:statuses_count) || account.statuses.count) } diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index a8ea9bbc4..6bb458aa2 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -4,11 +4,13 @@ = simple_form_for @account, url: settings_profile_path, html: { method: :put } do |f| = render 'shared/error_messages', object: @account - = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name') - = f.input :note, placeholder: t('simple_form.labels.defaults.note') - = f.input :avatar, wrapper: :with_label - = f.input :header, wrapper: :with_label - = f.input :locked, as: :boolean, wrapper: :with_label + .fields-group + = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name') + = f.input :note, placeholder: t('simple_form.labels.defaults.note') + = f.input :avatar, wrapper: :with_label + = f.input :header, wrapper: :with_label + + = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index e0c22e0c2..065999d0b 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -5,8 +5,7 @@ SimpleForm.setup do |config| # wrapper, change the order or even add your own to the # stack. The options given below are used to wrap the # whole input. - config.wrappers :default, class: :input, - hint_class: :field_with_hint, error_class: :field_with_errors do |b| + config.wrappers :default, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| ## Extensions enabled by default # Any of these extensions can be disabled for a # given input by passing: `f.input EXTENSION_NAME => false`. @@ -51,12 +50,11 @@ SimpleForm.setup do |config| # b.use :full_error, wrap_with: { tag: :span, class: :error } end - config.wrappers :with_label, class: :input, - hint_class: :field_with_hint, error_class: :field_with_errors do |b| + config.wrappers :with_label, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| b.use :html5 + b.use :label_input b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :error, wrap_with: { tag: :span, class: :error } - b.use :label_input end # The default wrapper to be used by the FormBuilder. diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 516d79b08..deecff3fd 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -15,6 +15,7 @@ en: note: Bio password: Password username: Username + locked: Make account private interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow @@ -23,6 +24,9 @@ en: follow: Send e-mail when someone follows you mention: Send e-mail when someone mentions you reblog: Send e-mail when someone reblogs your status + hints: + defaults: + locked: Requires you to approve followers, defaults post privacy to followers-only and disables federation 'no': 'No' required: mark: "*" diff --git a/db/migrate/20161222204147_create_follow_requests.rb b/db/migrate/20161222204147_create_follow_requests.rb new file mode 100644 index 000000000..fbe5edf3d --- /dev/null +++ b/db/migrate/20161222204147_create_follow_requests.rb @@ -0,0 +1,12 @@ +class CreateFollowRequests < ActiveRecord::Migration[5.0] + def change + create_table :follow_requests do |t| + t.integer :account_id, null: false + t.integer :target_account_id, null: false + + t.timestamps null: false + end + + add_index :follow_requests, [:account_id, :target_account_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 47e1b098d..180d3b14d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161222201034) do +ActiveRecord::Schema.define(version: 20161222204147) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -69,6 +69,14 @@ ActiveRecord::Schema.define(version: 20161222201034) do t.index ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree end + create_table "follow_requests", force: :cascade do |t| + t.integer "account_id", null: false + t.integer "target_account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true, using: :btree + end + create_table "follows", force: :cascade do |t| t.integer "account_id", null: false t.integer "target_account_id", null: false diff --git a/spec/fabricators/follow_request_fabricator.rb b/spec/fabricators/follow_request_fabricator.rb new file mode 100644 index 000000000..9c3733cef --- /dev/null +++ b/spec/fabricators/follow_request_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:follow_request) do + +end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb new file mode 100644 index 000000000..f2ec642d8 --- /dev/null +++ b/spec/models/follow_request_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' + +RSpec.describe FollowRequest, type: :model do + describe '#authorize!' + describe '#reject!' +end |