Przeglądaj źródła

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
master
Eugen Rochko 7 lat temu
rodzic
commit
b891a81008
24 zmienionych plików z 145 dodań i 47 usunięć
  1. +10
    -7
      app/assets/javascripts/components/components/icon_button.jsx
  2. +1
    -1
      app/assets/javascripts/components/components/status_action_bar.jsx
  3. +14
    -5
      app/assets/javascripts/components/features/account/components/header.jsx
  4. +1
    -1
      app/assets/javascripts/components/features/status/components/action_bar.jsx
  5. +2
    -1
      app/assets/stylesheets/components.scss
  6. +10
    -0
      app/assets/stylesheets/forms.scss
  7. +3
    -0
      app/controllers/api/v1/accounts_controller.rb
  8. +3
    -1
      app/controllers/stream_entries_controller.rb
  9. +1
    -1
      app/controllers/xrd_controller.rb
  10. +10
    -0
      app/lib/feed_manager.rb
  11. +6
    -0
      app/models/account.rb
  12. +19
    -0
      app/models/follow_request.rb
  13. +1
    -1
      app/models/status.rb
  14. +15
    -14
      app/services/follow_service.rb
  15. +1
    -1
      app/services/reblog_service.rb
  16. +1
    -0
      app/views/api/v1/accounts/relationship.rabl
  17. +3
    -3
      app/views/api/v1/accounts/show.rabl
  18. +7
    -5
      app/views/settings/profiles/show.html.haml
  19. +3
    -5
      config/initializers/simple_form.rb
  20. +4
    -0
      config/locales/simple_form.en.yml
  21. +12
    -0
      db/migrate/20161222204147_create_follow_requests.rb
  22. +9
    -1
      db/schema.rb
  23. +3
    -0
      spec/fabricators/follow_request_fabricator.rb
  24. +6
    -0
      spec/models/follow_request_spec.rb

+ 10
- 7
app/assets/javascripts/components/components/icon_button.jsx Wyświetl plik

@@ -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>
);


+ 1
- 1
app/assets/javascripts/components/components/status_action_bar.jsx Wyświetl plik

@@ -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' }}>


+ 14
- 5
app/assets/javascripts/components/features/account/components/header.jsx Wyświetl plik

@@ -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')) };


+ 1
- 1
app/assets/javascripts/components/features/status/components/action_bar.jsx Wyświetl plik

@@ -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>


+ 2
- 1
app/assets/stylesheets/components.scss Wyświetl plik

@@ -44,13 +44,14 @@
color: #616b86;
border: none;
background: transparent;
cursor: pointer;

&:hover {
color: #717b98;
}

&.disabled {
color: #535b72;
color: #454b5e;
cursor: default;
}



+ 10
- 0
app/assets/stylesheets/forms.scss Wyświetl plik

@@ -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 {


+ 3
- 0
app/controllers/api/v1/accounts_controller.rb Wyświetl plik

@@ -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

+ 3
- 1
app/controllers/stream_entries_controller.rb Wyświetl plik

@@ -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


+ 1
- 1
app/controllers/xrd_controller.rb Wyświetl plik

@@ -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)



+ 10
- 0
app/lib/feed_manager.rb Wyświetl plik

@@ -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


+ 6
- 0
app/models/account.rb Wyświetl plik

@@ -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


+ 19
- 0
app/models/follow_request.rb Wyświetl plik

@@ -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

+ 1
- 1
app/models/status.rb Wyświetl plik

@@ -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


+ 15
- 14
app/services/follow_service.rb Wyświetl plik

@@ -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


+ 1
- 1
app/services/reblog_service.rb Wyświetl plik

@@ -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: '')



+ 1
- 0
app/views/api/v1/accounts/relationship.rabl Wyświetl plik

@@ -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 }

+ 3
- 3
app/views/api/v1/accounts/show.rabl Wyświetl plik

@@ -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) }

+ 7
- 5
app/views/settings/profiles/show.html.haml Wyświetl plik

@@ -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


+ 3
- 5
config/initializers/simple_form.rb Wyświetl plik

@@ -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.


+ 4
- 0
config/locales/simple_form.en.yml Wyświetl plik

@@ -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: "*"


+ 12
- 0
db/migrate/20161222204147_create_follow_requests.rb Wyświetl plik

@@ -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

+ 9
- 1
db/schema.rb Wyświetl plik

@@ -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


+ 3
- 0
spec/fabricators/follow_request_fabricator.rb Wyświetl plik

@@ -0,0 +1,3 @@
Fabricator(:follow_request) do

end

+ 6
- 0
spec/models/follow_request_spec.rb Wyświetl plik

@@ -0,0 +1,6 @@
require 'rails_helper'

RSpec.describe FollowRequest, type: :model do
describe '#authorize!'
describe '#reject!'
end

Ładowanie…
Anuluj
Zapisz