forked from treehouse/mastodon
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 accountssignup-info-prompt
parent
2d2154ba75
commit
b891a81008
|
@ -5,17 +5,19 @@ const IconButton = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string.isRequired,
|
title: React.PropTypes.string.isRequired,
|
||||||
icon: React.PropTypes.string.isRequired,
|
icon: React.PropTypes.string.isRequired,
|
||||||
onClick: React.PropTypes.func.isRequired,
|
onClick: React.PropTypes.func,
|
||||||
size: React.PropTypes.number,
|
size: React.PropTypes.number,
|
||||||
active: React.PropTypes.bool,
|
active: React.PropTypes.bool,
|
||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
activeStyle: React.PropTypes.object
|
activeStyle: React.PropTypes.object,
|
||||||
|
disabled: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
return {
|
return {
|
||||||
size: 18,
|
size: 18,
|
||||||
active: false
|
active: false,
|
||||||
|
disabled: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -23,8 +25,10 @@ const IconButton = React.createClass({
|
||||||
|
|
||||||
handleClick (e) {
|
handleClick (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!this.props.disabled) {
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
e.stopPropagation();
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -37,7 +41,6 @@ const IconButton = React.createClass({
|
||||||
width: `${this.props.size * 1.28571429}px`,
|
width: `${this.props.size * 1.28571429}px`,
|
||||||
height: `${this.props.size}px`,
|
height: `${this.props.size}px`,
|
||||||
lineHeight: `${this.props.size}px`,
|
lineHeight: `${this.props.size}px`,
|
||||||
cursor: 'pointer',
|
|
||||||
...this.props.style
|
...this.props.style
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,7 +49,7 @@ const IconButton = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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' />
|
<i className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -76,7 +76,7 @@ const StatusActionBar = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
<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 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={{ 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' }}>
|
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import IconButton from '../../../components/icon_button';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const Header = React.createClass({
|
const Header = React.createClass({
|
||||||
|
@ -36,12 +37,20 @@ const Header = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
|
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 = (
|
actionBtn = (
|
||||||
<div style={{ position: 'absolute', top: '10px', left: '20px' }}>
|
<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} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const content = { __html: emojify(account.get('note')) };
|
const content = { __html: emojify(account.get('note')) };
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||||
|
|
|
@ -60,7 +60,7 @@ const ActionBar = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div style={{ background: '#2f3441', display: 'flex', flexDirection: 'row', borderTop: '1px solid #363c4b', borderBottom: '1px solid #363c4b', padding: '10px 0' }}>
|
<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 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' }}><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 style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,13 +44,14 @@
|
||||||
color: #616b86;
|
color: #616b86;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #717b98;
|
color: #717b98;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: #535b72;
|
color: #454b5e;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,12 @@ code {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
display: block;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.input.file, .input.select {
|
.input.file, .input.select {
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -59,6 +65,10 @@ code {
|
||||||
top: 1px;
|
top: 1px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
padding-left: 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text], input[type=email], input[type=password], textarea {
|
input[type=text], input[type=email], input[type=password], textarea {
|
||||||
|
|
|
@ -84,10 +84,12 @@ class Api::V1::AccountsController < ApiController
|
||||||
|
|
||||||
def relationships
|
def relationships
|
||||||
ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i]
|
ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i]
|
||||||
|
|
||||||
@accounts = Account.where(id: ids).select('id')
|
@accounts = Account.where(id: ids).select('id')
|
||||||
@following = Account.following_map(ids, current_user.account_id)
|
@following = Account.following_map(ids, current_user.account_id)
|
||||||
@followed_by = Account.followed_by_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)
|
@blocking = Account.blocking_map(ids, current_user.account_id)
|
||||||
|
@requested = Account.requested_map(ids, current_user.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
|
@ -109,5 +111,6 @@ class Api::V1::AccountsController < ApiController
|
||||||
@following = Account.following_map([@account.id], current_user.account_id)
|
@following = Account.following_map([@account.id], current_user.account_id)
|
||||||
@followed_by = Account.followed_by_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)
|
@blocking = Account.blocking_map([@account.id], current_user.account_id)
|
||||||
|
@requested = Account.requested_map([@account.id], current_user.account_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,8 +43,10 @@ class StreamEntriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_stream_entry
|
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
|
@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
|
end
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
|
|
|
@ -13,7 +13,7 @@ class XrdController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def webfinger
|
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}"
|
@canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}"
|
||||||
@magic_key = pem_to_magic_key(@account.keypair.public_key)
|
@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}")
|
redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
|
||||||
end
|
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)
|
def inline_render(target_account, template, object)
|
||||||
rabl_scope = Class.new do
|
rabl_scope = Class.new do
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
|
@ -34,6 +34,8 @@ class Account < ApplicationRecord
|
||||||
has_many :notifications, inverse_of: :account, dependent: :destroy
|
has_many :notifications, inverse_of: :account, dependent: :destroy
|
||||||
|
|
||||||
# Follow relations
|
# Follow relations
|
||||||
|
has_many :follow_requests, dependent: :destroy
|
||||||
|
|
||||||
has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', 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
|
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)
|
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
|
Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
before_create do
|
before_create do
|
||||||
|
|
|
@ -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!
|
text.strip!
|
||||||
self.reblog = reblog.reblog if reblog? && reblog.reblog?
|
self.reblog = reblog.reblog if reblog? && reblog.reblog?
|
||||||
self.in_reply_to_account_id = thread.account_id if reply?
|
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
|
end
|
||||||
|
|
||||||
private
|
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 ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
||||||
raise Mastodon::NotPermitted if target_account.blocking?(source_account)
|
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)
|
follow = source_account.follow!(target_account)
|
||||||
|
|
||||||
if target_account.local?
|
if target_account.local?
|
||||||
|
@ -19,25 +33,12 @@ class FollowService < BaseService
|
||||||
NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
|
NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
|
||||||
end
|
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)
|
Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id)
|
||||||
|
|
||||||
follow
|
follow
|
||||||
end
|
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
|
def redis
|
||||||
Redis.current
|
Redis.current
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ class ReblogService < BaseService
|
||||||
# @param [Status] reblogged_status Status to be reblogged
|
# @param [Status] reblogged_status Status to be reblogged
|
||||||
# @return [Status]
|
# @return [Status]
|
||||||
def call(account, reblogged_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: '')
|
reblog = account.statuses.create!(reblog: reblogged_status, text: '')
|
||||||
|
|
||||||
|
|
|
@ -4,3 +4,4 @@ attribute :id
|
||||||
node(:following) { |account| @following[account.id] || false }
|
node(:following) { |account| @following[account.id] || false }
|
||||||
node(:followed_by) { |account| @followed_by[account.id] || false }
|
node(:followed_by) { |account| @followed_by[account.id] || false }
|
||||||
node(:blocking) { |account| @blocking[account.id] || false }
|
node(:blocking) { |account| @blocking[account.id] || false }
|
||||||
|
node(:requested) { |account| @requested[account.id] || false }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
object @account
|
object @account
|
||||||
|
|
||||||
attributes :id, :username, :acct, :display_name
|
attributes :id, :username, :acct, :display_name, :locked
|
||||||
|
|
||||||
node(:note) { |account| Formatter.instance.simplified_format(account) }
|
node(:note) { |account| Formatter.instance.simplified_format(account) }
|
||||||
node(:url) { |account| TagManager.instance.url_for(account) }
|
node(:url) { |account| TagManager.instance.url_for(account) }
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
= simple_form_for @account, url: settings_profile_path, html: { method: :put } do |f|
|
= simple_form_for @account, url: settings_profile_path, html: { method: :put } do |f|
|
||||||
= render 'shared/error_messages', object: @account
|
= render 'shared/error_messages', object: @account
|
||||||
|
|
||||||
|
.fields-group
|
||||||
= f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name')
|
= f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name')
|
||||||
= f.input :note, placeholder: t('simple_form.labels.defaults.note')
|
= f.input :note, placeholder: t('simple_form.labels.defaults.note')
|
||||||
= f.input :avatar, wrapper: :with_label
|
= f.input :avatar, wrapper: :with_label
|
||||||
= f.input :header, wrapper: :with_label
|
= f.input :header, wrapper: :with_label
|
||||||
= f.input :locked, as: :boolean, wrapper: :with_label
|
|
||||||
|
= f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked')
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|
|
@ -5,8 +5,7 @@ SimpleForm.setup do |config|
|
||||||
# wrapper, change the order or even add your own to the
|
# wrapper, change the order or even add your own to the
|
||||||
# stack. The options given below are used to wrap the
|
# stack. The options given below are used to wrap the
|
||||||
# whole input.
|
# whole input.
|
||||||
config.wrappers :default, class: :input,
|
config.wrappers :default, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b|
|
||||||
hint_class: :field_with_hint, error_class: :field_with_errors do |b|
|
|
||||||
## Extensions enabled by default
|
## Extensions enabled by default
|
||||||
# Any of these extensions can be disabled for a
|
# Any of these extensions can be disabled for a
|
||||||
# given input by passing: `f.input EXTENSION_NAME => false`.
|
# 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 }
|
# b.use :full_error, wrap_with: { tag: :span, class: :error }
|
||||||
end
|
end
|
||||||
|
|
||||||
config.wrappers :with_label, class: :input,
|
config.wrappers :with_label, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b|
|
||||||
hint_class: :field_with_hint, error_class: :field_with_errors do |b|
|
|
||||||
b.use :html5
|
b.use :html5
|
||||||
|
b.use :label_input
|
||||||
b.use :hint, wrap_with: { tag: :span, class: :hint }
|
b.use :hint, wrap_with: { tag: :span, class: :hint }
|
||||||
b.use :error, wrap_with: { tag: :span, class: :error }
|
b.use :error, wrap_with: { tag: :span, class: :error }
|
||||||
b.use :label_input
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# The default wrapper to be used by the FormBuilder.
|
# The default wrapper to be used by the FormBuilder.
|
||||||
|
|
|
@ -15,6 +15,7 @@ en:
|
||||||
note: Bio
|
note: Bio
|
||||||
password: Password
|
password: Password
|
||||||
username: Username
|
username: Username
|
||||||
|
locked: Make account private
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: Block notifications from non-followers
|
must_be_follower: Block notifications from non-followers
|
||||||
must_be_following: Block notifications from people you don't follow
|
must_be_following: Block notifications from people you don't follow
|
||||||
|
@ -23,6 +24,9 @@ en:
|
||||||
follow: Send e-mail when someone follows you
|
follow: Send e-mail when someone follows you
|
||||||
mention: Send e-mail when someone mentions you
|
mention: Send e-mail when someone mentions you
|
||||||
reblog: Send e-mail when someone reblogs your status
|
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'
|
'no': 'No'
|
||||||
required:
|
required:
|
||||||
mark: "*"
|
mark: "*"
|
||||||
|
|
|
@ -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
|
10
db/schema.rb
10
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
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
|
t.index ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree
|
||||||
end
|
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|
|
create_table "follows", force: :cascade do |t|
|
||||||
t.integer "account_id", null: false
|
t.integer "account_id", null: false
|
||||||
t.integer "target_account_id", null: false
|
t.integer "target_account_id", null: false
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Fabricator(:follow_request) do
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe FollowRequest, type: :model do
|
||||||
|
describe '#authorize!'
|
||||||
|
describe '#reject!'
|
||||||
|
end
|
Loading…
Reference in New Issue