Add API modifiers to limit returned toots from public/hashtag timelines
to only those from local users; Add link to "extended information" to getting started in the UI; Add defaults for posting privacy; Change how publish button looks depending on posting privacy chosengh/stable
parent
4d2be9f432
commit
347a153b3d
|
@ -9,6 +9,7 @@ const Button = React.createClass({
|
||||||
block: React.PropTypes.bool,
|
block: React.PropTypes.bool,
|
||||||
secondary: React.PropTypes.bool,
|
secondary: React.PropTypes.bool,
|
||||||
size: React.PropTypes.number,
|
size: React.PropTypes.number,
|
||||||
|
children: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
|
@ -38,7 +39,6 @@ const Button = React.createClass({
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
letterSpacing: '0',
|
letterSpacing: '0',
|
||||||
textTransform: 'uppercase',
|
|
||||||
padding: `0 ${this.props.size / 2.25}px`,
|
padding: `0 ${this.props.size / 2.25}px`,
|
||||||
height: `${this.props.size}px`,
|
height: `${this.props.size}px`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|
|
@ -117,9 +117,10 @@ const ComposeForm = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
let replyArea = '';
|
let replyArea = '';
|
||||||
const disabled = this.props.is_submitting || this.props.is_uploading;
|
let publishText = '';
|
||||||
|
const disabled = this.props.is_submitting || this.props.is_uploading;
|
||||||
|
|
||||||
if (this.props.in_reply_to) {
|
if (this.props.in_reply_to) {
|
||||||
replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
|
replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
|
||||||
|
@ -127,6 +128,12 @@ const ComposeForm = React.createClass({
|
||||||
|
|
||||||
let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
|
let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
|
||||||
|
|
||||||
|
if (this.props.private) {
|
||||||
|
publishText = <span><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||||
|
} else {
|
||||||
|
publishText = intl.formatMessage(messages.publish) + (!this.props.unlisted ? '!' : '');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '10px' }}>
|
<div style={{ padding: '10px' }}>
|
||||||
<Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}>
|
<Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}>
|
||||||
|
@ -154,7 +161,7 @@ const ComposeForm = React.createClass({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||||
<div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div>
|
<div style={{ float: 'right' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled} /></div>
|
||||||
<div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
|
<div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
|
||||||
<UploadButtonContainer style={{ paddingTop: '4px' }} />
|
<UploadButtonContainer style={{ paddingTop: '4px' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,14 +26,14 @@ const makeMapStateToProps = () => {
|
||||||
sensitive: state.getIn(['compose', 'sensitive']),
|
sensitive: state.getIn(['compose', 'sensitive']),
|
||||||
spoiler: state.getIn(['compose', 'spoiler']),
|
spoiler: state.getIn(['compose', 'spoiler']),
|
||||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||||
unlisted: state.getIn(['compose', 'unlisted']),
|
unlisted: state.getIn(['compose', 'unlisted'], ),
|
||||||
private: state.getIn(['compose', 'private']),
|
private: state.getIn(['compose', 'private']),
|
||||||
fileDropDate: state.getIn(['compose', 'fileDropDate']),
|
fileDropDate: state.getIn(['compose', 'fileDropDate']),
|
||||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||||
in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
|
in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
|
||||||
media_count: state.getIn(['compose', 'media_attachments']).size,
|
media_count: state.getIn(['compose', 'media_attachments']).size,
|
||||||
me: state.getIn(['compose', 'me'])
|
me: state.getIn(['compose', 'me']),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@ const messages = defineMessages({
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
|
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
|
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
@ -34,6 +35,7 @@ const GettingStarted = ({ intl, me }) => {
|
||||||
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
||||||
{followRequests}
|
{followRequests}
|
||||||
<ColumnLink icon='users' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
<ColumnLink icon='users' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||||
|
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ const initialState = Immutable.Map({
|
||||||
suggestion_token: null,
|
suggestion_token: null,
|
||||||
suggestions: Immutable.List(),
|
suggestions: Immutable.List(),
|
||||||
me: null,
|
me: null,
|
||||||
|
default_privacy: 'public',
|
||||||
resetFileKey: Math.floor((Math.random() * 0x10000))
|
resetFileKey: Math.floor((Math.random() * 0x10000))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,6 +65,8 @@ function clearAll(state) {
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
map.set('is_submitting', false);
|
map.set('is_submitting', false);
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
|
map.set('unlisted', state.get('default_privacy') === 'unlisted');
|
||||||
|
map.set('private', state.get('default_privacy') === 'private');
|
||||||
map.update('media_attachments', list => list.clear());
|
map.update('media_attachments', list => list.clear());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -97,7 +100,7 @@ const insertSuggestion = (state, position, token, completion) => {
|
||||||
export default function compose(state = initialState, action) {
|
export default function compose(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STORE_HYDRATE:
|
case STORE_HYDRATE:
|
||||||
return state.merge(action.state.get('compose'));
|
return clearAll(state.merge(action.state.get('compose')));
|
||||||
case COMPOSE_MOUNT:
|
case COMPOSE_MOUNT:
|
||||||
return state.set('mounted', true);
|
return state.set('mounted', true);
|
||||||
case COMPOSE_UNMOUNT:
|
case COMPOSE_UNMOUNT:
|
||||||
|
|
|
@ -28,15 +28,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.button-secondary {
|
&.button-secondary {
|
||||||
background-color: $color1;
|
//
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $color1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: $color3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ $color1: #282c37; // darkest
|
||||||
$color2: #d9e1e8; // lightest
|
$color2: #d9e1e8; // lightest
|
||||||
$color3: #9baec8; // lighter
|
$color3: #9baec8; // lighter
|
||||||
$color4: #2b90d9; // vibrant
|
$color4: #2b90d9; // vibrant
|
||||||
$color5: #fff; // white
|
$color5: #ffffff; // white
|
||||||
$color6: #df405a; // error red
|
$color6: #df405a; // error red
|
||||||
$color7: #79bd9a; // succ green
|
$color7: #79bd9a; // succ green
|
||||||
$color8: #000; // black
|
$color8: #000000; // black
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Api::V1::TimelinesController < ApiController
|
||||||
end
|
end
|
||||||
|
|
||||||
def public
|
def public
|
||||||
@statuses = Status.as_public_timeline(current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
@statuses = Status.as_public_timeline(current_account, params[:local]).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
||||||
@statuses = cache_collection(@statuses)
|
@statuses = cache_collection(@statuses)
|
||||||
|
|
||||||
set_maps(@statuses)
|
set_maps(@statuses)
|
||||||
|
@ -40,7 +40,7 @@ class Api::V1::TimelinesController < ApiController
|
||||||
|
|
||||||
def tag
|
def tag
|
||||||
@tag = Tag.find_by(name: params[:id].downcase)
|
@tag = Tag.find_by(name: params[:id].downcase)
|
||||||
@statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
@statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
||||||
@statuses = cache_collection(@statuses)
|
@statuses = cache_collection(@statuses)
|
||||||
|
|
||||||
set_maps(@statuses)
|
set_maps(@statuses)
|
||||||
|
|
|
@ -21,7 +21,9 @@ class Settings::PreferencesController < ApplicationController
|
||||||
must_be_following: user_params[:interactions][:must_be_following] == '1',
|
must_be_following: user_params[:interactions][:must_be_following] == '1',
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_user.update(user_params.except(:notification_emails, :interactions))
|
current_user.settings['default_privacy'] = user_params[:settings][:default_privacy]
|
||||||
|
|
||||||
|
if current_user.update(user_params.except(:notification_emails, :interactions, :settings))
|
||||||
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render action: :show
|
render action: :show
|
||||||
|
@ -31,6 +33,6 @@ class Settings::PreferencesController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:locale, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following])
|
params.require(:user).permit(:locale, settings: [:default_privacy], notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,21 +102,25 @@ class Status < ApplicationRecord
|
||||||
where(account: [account] + account.following)
|
where(account: [account] + account.following)
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_public_timeline(account = nil)
|
def as_public_timeline(account = nil, local_only = false)
|
||||||
query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
|
query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
|
||||||
.where(visibility: :public)
|
.where(visibility: :public)
|
||||||
.where('(statuses.in_reply_to_id IS NULL OR statuses.in_reply_to_account_id = statuses.account_id)')
|
.where('(statuses.in_reply_to_id IS NULL OR statuses.in_reply_to_account_id = statuses.account_id)')
|
||||||
.where('statuses.reblog_of_id IS NULL')
|
.where('statuses.reblog_of_id IS NULL')
|
||||||
|
|
||||||
|
query = query.where('accounts.domain IS NULL') if local_only
|
||||||
|
|
||||||
account.nil? ? filter_timeline_default(query) : filter_timeline_default(filter_timeline(query, account))
|
account.nil? ? filter_timeline_default(query) : filter_timeline_default(filter_timeline(query, account))
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_tag_timeline(tag, account = nil)
|
def as_tag_timeline(tag, account = nil, local_only = false)
|
||||||
query = tag.statuses
|
query = tag.statuses
|
||||||
.joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
|
.joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
|
||||||
.where(visibility: :public)
|
.where(visibility: :public)
|
||||||
.where('statuses.reblog_of_id IS NULL')
|
.where('statuses.reblog_of_id IS NULL')
|
||||||
|
|
||||||
|
query = query.where('accounts.domain IS NULL') if local_only
|
||||||
|
|
||||||
account.nil? ? filter_timeline_default(query) : filter_timeline_default(filter_timeline(query, account))
|
account.nil? ? filter_timeline_default(query) : filter_timeline_default(filter_timeline(query, account))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,4 +21,8 @@ class User < ApplicationRecord
|
||||||
def send_devise_notification(notification, *args)
|
def send_devise_notification(notification, *args)
|
||||||
devise_mailer.send(notification, self, *args).deliver_later
|
devise_mailer.send(notification, self, *args).deliver_later
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_default_privacy
|
||||||
|
settings.default_privacy || (account.locked? ? 'private' : 'public')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
object false
|
object false
|
||||||
|
|
||||||
node(:meta) {
|
node(:meta) do
|
||||||
{
|
{
|
||||||
access_token: @token,
|
access_token: @token,
|
||||||
locale: I18n.locale,
|
locale: I18n.locale,
|
||||||
me: current_account.id,
|
me: current_account.id,
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
|
|
||||||
node(:compose) {
|
node(:compose) do
|
||||||
{
|
{
|
||||||
me: current_account.id,
|
me: current_account.id,
|
||||||
private: current_account.locked?,
|
default_privacy: current_account.user.setting_default_privacy,
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
|
|
||||||
node(:accounts) {
|
node(:accounts) do
|
||||||
{
|
{
|
||||||
current_account.id => partial('api/v1/accounts/show', object: current_account),
|
current_account.id => partial('api/v1/accounts/show', object: current_account),
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
|
|
||||||
node(:settings) { @web_settings }
|
node(:settings) { @web_settings }
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
|
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
|
||||||
|
|
||||||
|
= f.input :setting_default_privacy, collection: Status.visibilities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| I18n.t("statuses.visibilities.#{visibility}") }, required: false
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
|
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
|
||||||
= ff.input :follow, as: :boolean, wrapper: :with_label
|
= ff.input :follow, as: :boolean, wrapper: :with_label
|
||||||
|
|
|
@ -97,8 +97,12 @@ en:
|
||||||
settings: Settings
|
settings: Settings
|
||||||
two_factor_auth: Two-factor Authentication
|
two_factor_auth: Two-factor Authentication
|
||||||
statuses:
|
statuses:
|
||||||
over_character_limit: character limit of %{max} exceeded
|
|
||||||
open_in_web: Open in web
|
open_in_web: Open in web
|
||||||
|
over_character_limit: character limit of %{max} exceeded
|
||||||
|
visibilities:
|
||||||
|
private: Only show to followers
|
||||||
|
public: Public
|
||||||
|
unlisted: Public, but do not display on the public timeline
|
||||||
stream_entries:
|
stream_entries:
|
||||||
click_to_show: Click to show
|
click_to_show: Click to show
|
||||||
favourited: favourited a post by
|
favourited: favourited a post by
|
||||||
|
|
|
@ -23,6 +23,7 @@ en:
|
||||||
note: Bio
|
note: Bio
|
||||||
otp_attempt: Two-factor code
|
otp_attempt: Two-factor code
|
||||||
password: Password
|
password: Password
|
||||||
|
setting_default_privacy: Post privacy
|
||||||
username: Username
|
username: Username
|
||||||
interactions:
|
interactions:
|
||||||
must_be_follower: Block notifications from non-followers
|
must_be_follower: Block notifications from non-followers
|
||||||
|
|
Reference in New Issue