about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2016-12-22 23:03:57 +0100
committerEugen Rochko <eugen@zeonfederated.com>2016-12-22 23:03:57 +0100
commitb891a81008d2cf595cb37432a8e1f36606db16d6 (patch)
treee3b083966fc14dda46a2ec75586fdf566c2585aa /app
parent2d2154ba75279186b064c887452b7d6ee70b8ba2 (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
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/components/components/icon_button.jsx17
-rw-r--r--app/assets/javascripts/components/components/status_action_bar.jsx2
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx19
-rw-r--r--app/assets/javascripts/components/features/status/components/action_bar.jsx2
-rw-r--r--app/assets/stylesheets/components.scss3
-rw-r--r--app/assets/stylesheets/forms.scss10
-rw-r--r--app/controllers/api/v1/accounts_controller.rb3
-rw-r--r--app/controllers/stream_entries_controller.rb4
-rw-r--r--app/controllers/xrd_controller.rb2
-rw-r--r--app/lib/feed_manager.rb10
-rw-r--r--app/models/account.rb6
-rw-r--r--app/models/follow_request.rb19
-rw-r--r--app/models/status.rb2
-rw-r--r--app/services/follow_service.rb29
-rw-r--r--app/services/reblog_service.rb2
-rw-r--r--app/views/api/v1/accounts/relationship.rabl1
-rw-r--r--app/views/api/v1/accounts/show.rabl6
-rw-r--r--app/views/settings/profiles/show.html.haml12
18 files changed, 108 insertions, 41 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