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
This commit is contained in:
parent
2d2154ba75
commit
b891a81008
24 changed files with 145 additions and 47 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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' }}>
|
||||
|
|
|
@ -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')) };
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -44,13 +44,14 @@
|
|||
color: #616b86;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #717b98;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #535b72;
|
||||
color: #454b5e;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
app/models/follow_request.rb
Normal file
19
app/models/follow_request.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: '')
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue