Reflect "requested" relationship in API and UI Reflect inability of private posts to be reblogged in the UI Disable Webfinger for locked accountsmaster
@@ -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(); | ||||
this.props.onClick(); | |||||
e.stopPropagation(); | |||||
if (!this.props.disabled) { | |||||
this.props.onClick(); | |||||
} | |||||
}, | }, | ||||
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,11 +37,19 @@ const Header = React.createClass({ | |||||
} | } | ||||
if (me !== account.get('id')) { | 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')) }; | const content = { __html: emojify(account.get('note')) }; | ||||
@@ -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,11 +1,11 @@ | |||||
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) } | ||||
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(: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(: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) } | 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| | = 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 | ||||
= 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 | .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, | |||||
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 | ## 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, | |||||
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 :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,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 |