Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
commit
06cc04fd23
30 changed files with 408 additions and 148 deletions
2
Gemfile
2
Gemfile
|
@ -24,7 +24,7 @@ gem 'streamio-ffmpeg', '~> 3.0'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.6'
|
gem 'addressable', '~> 2.6'
|
||||||
gem 'bootsnap', '~> 1.3', require: false
|
gem 'bootsnap', '~> 1.4', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.6'
|
gem 'charlock_holmes', '~> 0.7.6'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
|
|
14
Gemfile.lock
14
Gemfile.lock
|
@ -92,13 +92,13 @@ GEM
|
||||||
aws-sigv4 (1.0.3)
|
aws-sigv4 (1.0.3)
|
||||||
bcrypt (3.1.12)
|
bcrypt (3.1.12)
|
||||||
benchmark-ips (2.7.2)
|
benchmark-ips (2.7.2)
|
||||||
better_errors (2.5.0)
|
better_errors (2.5.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
binding_of_caller (0.8.0)
|
binding_of_caller (0.8.0)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
bootsnap (1.3.2)
|
bootsnap (1.4.0)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.4.0)
|
brakeman (4.4.0)
|
||||||
browser (2.5.3)
|
browser (2.5.3)
|
||||||
|
@ -205,7 +205,7 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.62.0)
|
excon (0.62.0)
|
||||||
fabrication (2.20.1)
|
fabrication (2.20.1)
|
||||||
faker (1.9.1)
|
faker (1.9.3)
|
||||||
i18n (>= 0.7)
|
i18n (>= 0.7)
|
||||||
faraday (0.15.0)
|
faraday (0.15.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
@ -347,7 +347,7 @@ GEM
|
||||||
mini_mime (1.0.1)
|
mini_mime (1.0.1)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.11.3)
|
minitest (5.11.3)
|
||||||
msgpack (1.2.4)
|
msgpack (1.2.6)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
necromancer (0.4.0)
|
necromancer (0.4.0)
|
||||||
|
@ -402,7 +402,7 @@ GEM
|
||||||
pg (1.1.4)
|
pg (1.1.4)
|
||||||
pghero (2.2.0)
|
pghero (2.2.0)
|
||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.3.2)
|
pkg-config (1.3.3)
|
||||||
powerpack (0.1.2)
|
powerpack (0.1.2)
|
||||||
premailer (1.11.1)
|
premailer (1.11.1)
|
||||||
addressable
|
addressable
|
||||||
|
@ -565,7 +565,7 @@ GEM
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (6.0.8)
|
sidekiq-unique-jobs (6.0.9)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 4.0, < 6.0)
|
sidekiq (>= 4.0, < 6.0)
|
||||||
thor (~> 0)
|
thor (~> 0)
|
||||||
|
@ -662,7 +662,7 @@ DEPENDENCIES
|
||||||
aws-sdk-s3 (~> 1.30)
|
aws-sdk-s3 (~> 1.30)
|
||||||
better_errors (~> 2.5)
|
better_errors (~> 2.5)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
bootsnap (~> 1.3)
|
bootsnap (~> 1.4)
|
||||||
brakeman (~> 4.4)
|
brakeman (~> 4.4)
|
||||||
browser
|
browser
|
||||||
bullet (~> 5.9)
|
bullet (~> 5.9)
|
||||||
|
|
|
@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
define_type ::Status.unscoped.without_reblogs do
|
define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do
|
||||||
crutch :mentions do |collection|
|
crutch :mentions do |collection|
|
||||||
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
|
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
|
@ -50,7 +50,7 @@ class StatusesIndex < Chewy::Index
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :account_id, type: 'long'
|
field :account_id, type: 'long'
|
||||||
|
|
||||||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do
|
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do
|
||||||
field :stemmed, type: 'text', analyzer: 'content'
|
field :stemmed, type: 'text', analyzer: 'content'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||||
resource.agreement = true
|
resource.agreement = true
|
||||||
|
|
||||||
|
resource.current_sign_in_ip = request.remote_ip if resource.current_sign_in_ip.nil?
|
||||||
resource.build_account if resource.account.nil?
|
resource.build_account if resource.account.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ module SettingsHelper
|
||||||
HUMAN_LOCALES = {
|
HUMAN_LOCALES = {
|
||||||
en: 'English',
|
en: 'English',
|
||||||
ar: 'العربية',
|
ar: 'العربية',
|
||||||
ast: 'l\'asturianu',
|
ast: 'Asturianu',
|
||||||
bg: 'Български',
|
bg: 'Български',
|
||||||
ca: 'Català',
|
ca: 'Català',
|
||||||
co: 'Corsu',
|
co: 'Corsu',
|
||||||
|
@ -30,16 +30,16 @@ module SettingsHelper
|
||||||
ja: '日本語',
|
ja: '日本語',
|
||||||
ka: 'ქართული',
|
ka: 'ქართული',
|
||||||
ko: '한국어',
|
ko: '한국어',
|
||||||
lv: 'Latviešu valoda',
|
lv: 'Latviešu',
|
||||||
ml: 'മലയാളം',
|
ml: 'മലയാളം',
|
||||||
ms: 'بهاس ملايو',
|
ms: 'Bahasa Melayu',
|
||||||
nl: 'Nederlands',
|
nl: 'Nederlands',
|
||||||
no: 'Norsk',
|
no: 'Norsk',
|
||||||
oc: 'Occitan',
|
oc: 'Occitan',
|
||||||
pl: 'Polszczyzna',
|
pl: 'Polski',
|
||||||
pt: 'Português',
|
pt: 'Português',
|
||||||
'pt-BR': 'Português do Brasil',
|
'pt-BR': 'Português do Brasil',
|
||||||
ro: 'Limba română',
|
ro: 'Română',
|
||||||
ru: 'Русский',
|
ru: 'Русский',
|
||||||
sk: 'Slovenčina',
|
sk: 'Slovenčina',
|
||||||
sl: 'Slovenščina',
|
sl: 'Slovenščina',
|
||||||
|
@ -49,7 +49,7 @@ module SettingsHelper
|
||||||
sv: 'Svenska',
|
sv: 'Svenska',
|
||||||
ta: 'தமிழ்',
|
ta: 'தமிழ்',
|
||||||
te: 'తెలుగు',
|
te: 'తెలుగు',
|
||||||
th: 'ภาษาไทย',
|
th: 'ไทย',
|
||||||
tr: 'Türkçe',
|
tr: 'Türkçe',
|
||||||
uk: 'Українська',
|
uk: 'Українська',
|
||||||
zh: '中文',
|
zh: '中文',
|
||||||
|
|
|
@ -11,26 +11,36 @@ export default class DisplayName extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, others, localDomain } = this.props;
|
const { others, localDomain } = this.props;
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
|
||||||
|
|
||||||
let suffix;
|
let displayName, suffix, account;
|
||||||
|
|
||||||
if (others && others.size > 1) {
|
if (others && others.size > 1) {
|
||||||
suffix = `+${others.size}`;
|
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||||
|
|
||||||
|
if (others.size - 2 > 0) {
|
||||||
|
suffix = `+${others.size - 2}`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (others) {
|
||||||
|
account = others.first();
|
||||||
|
} else {
|
||||||
|
account = this.props.account;
|
||||||
|
}
|
||||||
|
|
||||||
let acct = account.get('acct');
|
let acct = account.get('acct');
|
||||||
|
|
||||||
if (acct.indexOf('@') === -1 && localDomain) {
|
if (acct.indexOf('@') === -1 && localDomain) {
|
||||||
acct = `${acct}@${localDomain}`;
|
acct = `${acct}@${localDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
suffix = <span className='display-name__account'>@{acct}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='display-name'>
|
<span className='display-name'>
|
||||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
|
{displayName} {suffix}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
// Track height changes we know about to compensate scrolling
|
// Track height changes we know about to compensate scrolling
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status.get('card');
|
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnapshotBeforeUpdate () {
|
getSnapshotBeforeUpdate () {
|
||||||
|
@ -99,7 +99,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
// Compensate height changes
|
// Compensate height changes
|
||||||
componentDidUpdate (prevProps, prevState, snapshot) {
|
componentDidUpdate (prevProps, prevState, snapshot) {
|
||||||
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status.get('card');
|
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||||
if (doShowCard && !this.didShowCard) {
|
if (doShowCard && !this.didShowCard) {
|
||||||
this.didShowCard = true;
|
this.didShowCard = true;
|
||||||
if (snapshot !== null && this.props.updateScrollBottom) {
|
if (snapshot !== null && this.props.updateScrollBottom) {
|
||||||
|
|
|
@ -108,9 +108,8 @@ class Upload extends ImmutablePureComponent {
|
||||||
<label>
|
<label>
|
||||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
|
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
|
||||||
|
|
||||||
<input
|
<textarea
|
||||||
placeholder={intl.formatMessage(messages.description)}
|
placeholder={intl.formatMessage(messages.description)}
|
||||||
type='text'
|
|
||||||
value={description}
|
value={description}
|
||||||
maxLength={420}
|
maxLength={420}
|
||||||
onFocus={this.handleInputFocus}
|
onFocus={this.handleInputFocus}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import Toggle from 'react-toggle';
|
import Toggle from 'react-toggle';
|
||||||
import AsyncSelect from 'react-select/lib/Async';
|
import AsyncSelect from 'react-select/lib/Async';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
|
||||||
|
noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
|
||||||
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
class ColumnSettings extends React.PureComponent {
|
class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -25,6 +30,7 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
tags (mode) {
|
tags (mode) {
|
||||||
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
||||||
|
|
||||||
if (tags.toJSON) {
|
if (tags.toJSON) {
|
||||||
return tags.toJSON();
|
return tags.toJSON();
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,33 +38,36 @@ class ColumnSettings extends React.PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelect = (mode) => {
|
onSelect = mode => value => this.props.onChange(['tags', mode], value);
|
||||||
return (value) => {
|
|
||||||
this.props.onChange(['tags', mode], value);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
onToggle = () => {
|
onToggle = () => {
|
||||||
if (this.state.open && this.hasTags()) {
|
if (this.state.open && this.hasTags()) {
|
||||||
this.props.onChange('tags', {});
|
this.props.onChange('tags', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ open: !this.state.open });
|
this.setState({ open: !this.state.open });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
|
||||||
|
|
||||||
modeSelect (mode) {
|
modeSelect (mode) {
|
||||||
return (
|
return (
|
||||||
<div className='column-settings__section'>
|
<div className='column-settings__row'>
|
||||||
|
<span className='column-settings__section'>
|
||||||
{this.modeLabel(mode)}
|
{this.modeLabel(mode)}
|
||||||
|
</span>
|
||||||
|
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
isMulti
|
isMulti
|
||||||
autoFocus
|
autoFocus
|
||||||
value={this.tags(mode)}
|
value={this.tags(mode)}
|
||||||
settings={this.props.settings}
|
|
||||||
settingPath={['tags', mode]}
|
|
||||||
onChange={this.onSelect(mode)}
|
onChange={this.onSelect(mode)}
|
||||||
loadOptions={this.props.onLoad}
|
loadOptions={this.props.onLoad}
|
||||||
classNamePrefix='column-settings__hashtag-select'
|
className='column-select__container'
|
||||||
|
classNamePrefix='column-select'
|
||||||
name='tags'
|
name='tags'
|
||||||
|
placeholder={this.props.intl.formatMessage(messages.placeholder)}
|
||||||
|
noOptionsMessage={this.noOptionsMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -66,11 +75,15 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
modeLabel (mode) {
|
modeLabel (mode) {
|
||||||
switch(mode) {
|
switch(mode) {
|
||||||
case 'any': return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
|
case 'any':
|
||||||
case 'all': return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
|
return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
|
||||||
case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
|
case 'all':
|
||||||
}
|
return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
|
||||||
|
case 'none':
|
||||||
|
return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
|
||||||
|
default:
|
||||||
return '';
|
return '';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -78,23 +91,21 @@ class ColumnSettings extends React.PureComponent {
|
||||||
<div>
|
<div>
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<div className='setting-toggle'>
|
<div className='setting-toggle'>
|
||||||
<Toggle
|
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||||
id='hashtag.column_settings.tag_toggle'
|
|
||||||
onChange={this.onToggle}
|
|
||||||
checked={this.state.open}
|
|
||||||
/>
|
|
||||||
<span className='setting-toggle__label'>
|
<span className='setting-toggle__label'>
|
||||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.open &&
|
|
||||||
|
{this.state.open && (
|
||||||
<div className='column-settings__hashtags'>
|
<div className='column-settings__hashtags'>
|
||||||
{this.modeSelect('any')}
|
{this.modeSelect('any')}
|
||||||
{this.modeSelect('all')}
|
{this.modeSelect('all')}
|
||||||
{this.modeSelect('none')}
|
{this.modeSelect('none')}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,15 +41,19 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
|
|
||||||
title = () => {
|
title = () => {
|
||||||
let title = [this.props.params.id];
|
let title = [this.props.params.id];
|
||||||
|
|
||||||
if (this.additionalFor('any')) {
|
if (this.additionalFor('any')) {
|
||||||
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
|
title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.additionalFor('all')) {
|
if (this.additionalFor('all')) {
|
||||||
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
|
title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.additionalFor('none')) {
|
if (this.additionalFor('none')) {
|
||||||
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
|
title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,9 +81,10 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
let all = (tags.all || []).map(tag => tag.value);
|
let all = (tags.all || []).map(tag => tag.value);
|
||||||
let none = (tags.none || []).map(tag => tag.value);
|
let none = (tags.none || []).map(tag => tag.value);
|
||||||
|
|
||||||
[id, ...any].map((tag) => {
|
[id, ...any].map(tag => {
|
||||||
this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => {
|
this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
|
||||||
let tags = status.tags.map(tag => tag.name);
|
let tags = status.tags.map(tag => tag.name);
|
||||||
|
|
||||||
return all.filter(tag => tags.includes(tag)).length === all.length &&
|
return all.filter(tag => tags.includes(tag)).length === all.length &&
|
||||||
none.filter(tag => tags.includes(tag)).length === 0;
|
none.filter(tag => tags.includes(tag)).length === 0;
|
||||||
})));
|
})));
|
||||||
|
@ -95,12 +100,14 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { id, tags } = this.props.params;
|
const { id, tags } = this.props.params;
|
||||||
|
|
||||||
|
this._subscribe(dispatch, id, tags);
|
||||||
dispatch(expandHashtagTimeline(id, { tags }));
|
dispatch(expandHashtagTimeline(id, { tags }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
const { dispatch, params } = this.props;
|
const { dispatch, params } = this.props;
|
||||||
const { id, tags } = nextProps.params;
|
const { id, tags } = nextProps.params;
|
||||||
|
|
||||||
if (id !== params.id || !isEqual(tags, params.tags)) {
|
if (id !== params.id || !isEqual(tags, params.tags)) {
|
||||||
this._unsubscribe();
|
this._unsubscribe();
|
||||||
this._subscribe(dispatch, id, tags);
|
this._subscribe(dispatch, id, tags);
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
||||||
|
import IconButton from '../../../components/icon_button';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
value: state.getIn(['listEditor', 'title']),
|
||||||
|
disabled: !state.getIn(['listEditor', 'isChanged']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||||
|
onSubmit: () => dispatch(submitListEditor(false)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class ListForm extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = e => {
|
||||||
|
this.props.onChange(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { value, disabled, intl } = this.props;
|
||||||
|
|
||||||
|
const title = intl.formatMessage(messages.title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||||
|
<input
|
||||||
|
className='setting-text'
|
||||||
|
value={value}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
disabled={disabled}
|
||||||
|
icon='check'
|
||||||
|
title={title}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,11 +7,11 @@ import { injectIntl } from 'react-intl';
|
||||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||||
import Account from './components/account';
|
import Account from './components/account';
|
||||||
import Search from './components/search';
|
import Search from './components/search';
|
||||||
|
import EditListForm from './components/edit_list_form';
|
||||||
import Motion from '../ui/util/optional_motion';
|
import Motion from '../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
title: state.getIn(['listEditor', 'title']),
|
|
||||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,6 @@ class ListEditor extends ImmutablePureComponent {
|
||||||
onInitialize: PropTypes.func.isRequired,
|
onInitialize: PropTypes.func.isRequired,
|
||||||
onClear: PropTypes.func.isRequired,
|
onClear: PropTypes.func.isRequired,
|
||||||
onReset: PropTypes.func.isRequired,
|
onReset: PropTypes.func.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
accountIds: ImmutablePropTypes.list.isRequired,
|
accountIds: ImmutablePropTypes.list.isRequired,
|
||||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -49,12 +48,12 @@ class ListEditor extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { title, accountIds, searchAccountIds, onClear } = this.props;
|
const { accountIds, searchAccountIds, onClear } = this.props;
|
||||||
const showSearch = searchAccountIds.size > 0;
|
const showSearch = searchAccountIds.size > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root__modal list-editor'>
|
<div className='modal-root__modal list-editor'>
|
||||||
<h4>{title}</h4>
|
<EditListForm />
|
||||||
|
|
||||||
<Search />
|
<Search />
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
listId: null,
|
listId: null,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
isChanged: false,
|
||||||
title: '',
|
title: '',
|
||||||
|
|
||||||
accounts: ImmutableMap({
|
accounts: ImmutableMap({
|
||||||
|
@ -47,10 +48,16 @@ export default function listEditorReducer(state = initialState, action) {
|
||||||
map.set('isSubmitting', false);
|
map.set('isSubmitting', false);
|
||||||
});
|
});
|
||||||
case LIST_EDITOR_TITLE_CHANGE:
|
case LIST_EDITOR_TITLE_CHANGE:
|
||||||
return state.set('title', action.value);
|
return state.withMutations(map => {
|
||||||
|
map.set('title', action.value);
|
||||||
|
map.set('isChanged', true);
|
||||||
|
});
|
||||||
case LIST_CREATE_REQUEST:
|
case LIST_CREATE_REQUEST:
|
||||||
case LIST_UPDATE_REQUEST:
|
case LIST_UPDATE_REQUEST:
|
||||||
return state.set('isSubmitting', true);
|
return state.withMutations(map => {
|
||||||
|
map.set('isSubmitting', true);
|
||||||
|
map.set('isChanged', false);
|
||||||
|
});
|
||||||
case LIST_CREATE_FAIL:
|
case LIST_CREATE_FAIL:
|
||||||
case LIST_UPDATE_FAIL:
|
case LIST_UPDATE_FAIL:
|
||||||
return state.set('isSubmitting', false);
|
return state.set('isSubmitting', false);
|
||||||
|
|
|
@ -13,6 +13,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rich-formatting a,
|
||||||
|
.rich-formatting p a,
|
||||||
|
.rich-formatting li a,
|
||||||
|
.landing-page__short-description p a,
|
||||||
.status__content a,
|
.status__content a,
|
||||||
.reply-indicator__content a {
|
.reply-indicator__content a {
|
||||||
color: lighten($ui-highlight-color, 12%);
|
color: lighten($ui-highlight-color, 12%);
|
||||||
|
|
|
@ -352,6 +352,8 @@
|
||||||
.moved-account-widget,
|
.moved-account-widget,
|
||||||
.memoriam-widget,
|
.memoriam-widget,
|
||||||
.activity-stream,
|
.activity-stream,
|
||||||
.nothing-here {
|
.nothing-here,
|
||||||
|
.directory__tag > a,
|
||||||
|
.directory__tag > div {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,3 +41,34 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin search-popout() {
|
||||||
|
background: $simple-background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: $light-text-color;
|
||||||
|
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: $light-text-color;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-weight: 500;
|
||||||
|
color: $inverted-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,15 +49,9 @@ $small-breakpoint: 960px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strong,
|
||||||
em {
|
em {
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: transparent;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
color: lighten($darker-text-color, 10%);
|
color: lighten($darker-text-color, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -796,7 +790,7 @@ $small-breakpoint: 960px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -846,14 +840,7 @@ $small-breakpoint: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
display: inline;
|
font-weight: 500;
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 700;
|
|
||||||
background: transparent;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
color: lighten($darker-text-color, 10%);
|
color: lighten($darker-text-color, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -476,7 +476,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .1s ease;
|
transition: opacity .1s ease;
|
||||||
|
|
||||||
input {
|
textarea {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -3056,14 +3056,41 @@ a.status-card.compact:hover {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.column-settings__hashtag-select {
|
.column-settings__hashtags {
|
||||||
|
.column-settings__row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-select {
|
||||||
&__control {
|
&__control {
|
||||||
@include search-input();
|
@include search-input();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__placeholder {
|
||||||
|
color: $dark-text-color;
|
||||||
|
padding-left: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value-container {
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
&__multi-value {
|
&__multi-value {
|
||||||
background: lighten($ui-base-color, 8%);
|
background: lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
|
&__remove {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: lighten($ui-base-color, 12%);
|
||||||
|
color: lighten($darker-text-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__multi-value__label,
|
&__multi-value__label,
|
||||||
|
@ -3071,9 +3098,42 @@ a.status-card.compact:hover {
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__indicator-separator,
|
&__clear-indicator,
|
||||||
&__dropdown-indicator {
|
&__dropdown-indicator {
|
||||||
display: none;
|
cursor: pointer;
|
||||||
|
transition: none;
|
||||||
|
color: $dark-text-color;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
color: lighten($dark-text-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__indicator-separator {
|
||||||
|
background-color: lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
@include search-popout();
|
||||||
|
padding: 0;
|
||||||
|
background: $ui-secondary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-list {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
color: $inverted-text-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&--is-focused,
|
||||||
|
&--is-selected {
|
||||||
|
background: darken($ui-secondary-color, 10%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4867,34 +4927,7 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-popout {
|
.search-popout {
|
||||||
background: $simple-background-color;
|
@include search-popout();
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
padding-bottom: 14px;
|
|
||||||
margin-top: 10px;
|
|
||||||
color: $light-text-color;
|
|
||||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $light-text-color;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
em {
|
|
||||||
font-weight: 500;
|
|
||||||
color: $inverted-text-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
noscript {
|
noscript {
|
||||||
|
@ -5130,7 +5163,7 @@ noscript {
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-left: 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,9 @@ class ActivityPub::Activity
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
|
SUPPORTED_TYPES = %w(Note).freeze
|
||||||
|
CONVERTED_TYPES = %w(Image Video Article Page).freeze
|
||||||
|
|
||||||
def initialize(json, account, **options)
|
def initialize(json, account, **options)
|
||||||
@json = json
|
@json = json
|
||||||
@account = account
|
@account = account
|
||||||
|
@ -71,6 +74,18 @@ class ActivityPub::Activity
|
||||||
@object_uri ||= value_or_id(@object)
|
@object_uri ||= value_or_id(@object)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unsupported_object_type?
|
||||||
|
@object.is_a?(String) || !(supported_object_type? || converted_object_type?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def supported_object_type?
|
||||||
|
equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
|
||||||
|
end
|
||||||
|
|
||||||
|
def converted_object_type?
|
||||||
|
equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
|
||||||
|
end
|
||||||
|
|
||||||
def distribute(status)
|
def distribute(status)
|
||||||
crawl_links(status)
|
crawl_links(status)
|
||||||
|
|
||||||
|
@ -120,6 +135,23 @@ class ActivityPub::Activity
|
||||||
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
|
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_from_object
|
||||||
|
# If the status is already known, return it
|
||||||
|
status = status_from_uri(object_uri)
|
||||||
|
return status unless status.nil?
|
||||||
|
|
||||||
|
# If the boosted toot is embedded and it is a self-boost, handle it like a Create
|
||||||
|
unless unsupported_object_type?
|
||||||
|
actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
|
||||||
|
if actor_id == @account.uri
|
||||||
|
return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the status is not from the actor, try to fetch it
|
||||||
|
return fetch_remote_original_status if value_or_id(first_of_value(@json['attributedTo'])) == @account.uri
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_remote_original_status
|
def fetch_remote_original_status
|
||||||
if object_uri.start_with?('http')
|
if object_uri.start_with?('http')
|
||||||
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
class ActivityPub::Activity::Announce < ActivityPub::Activity
|
class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
original_status = status_from_uri(object_uri)
|
original_status = status_from_object
|
||||||
original_status ||= fetch_remote_original_status
|
|
||||||
|
|
||||||
return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
|
return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
|
||||||
|
|
||||||
status = Status.find_by(account: @account, reblog: original_status)
|
status = Status.find_by(account: @account, reblog: original_status)
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
SUPPORTED_TYPES = %w(Note).freeze
|
|
||||||
CONVERTED_TYPES = %w(Image Video Article Page).freeze
|
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if unsupported_object_type? || invalid_origin?(@object['id'])
|
return if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
|
||||||
return if Tombstone.exists?(uri: @object['id'])
|
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
|
@ -318,22 +314,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
@object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
|
@object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsupported_object_type?
|
|
||||||
@object.is_a?(String) || !(supported_object_type? || converted_object_type?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def unsupported_media_type?(mime_type)
|
def unsupported_media_type?(mime_type)
|
||||||
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
|
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def supported_object_type?
|
|
||||||
equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
|
|
||||||
end
|
|
||||||
|
|
||||||
def converted_object_type?
|
|
||||||
equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
|
|
||||||
end
|
|
||||||
|
|
||||||
def skip_download?
|
def skip_download?
|
||||||
return @skip_download if defined?(@skip_download)
|
return @skip_download if defined?(@skip_download)
|
||||||
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
||||||
|
@ -352,6 +336,37 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
!replied_to_status.nil? && replied_to_status.account.local?
|
!replied_to_status.nil? && replied_to_status.account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def related_to_local_activity?
|
||||||
|
fetch? || followed_by_local_accounts? || requested_through_relay? ||
|
||||||
|
responds_to_followed_account? || addresses_local_accounts?
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch?
|
||||||
|
!@options[:delivery]
|
||||||
|
end
|
||||||
|
|
||||||
|
def followed_by_local_accounts?
|
||||||
|
@account.passive_relationships.exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
def requested_through_relay?
|
||||||
|
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def responds_to_followed_account?
|
||||||
|
!replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def addresses_local_accounts?
|
||||||
|
return true if @options[:delivered_to_account_id]
|
||||||
|
|
||||||
|
local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||||
|
|
||||||
|
return false if local_usernames.empty?
|
||||||
|
|
||||||
|
Account.local.where(username: local_usernames).exists?
|
||||||
|
end
|
||||||
|
|
||||||
def forward_for_reply
|
def forward_for_reply
|
||||||
return unless @json['signature'].present? && reply_to_local?
|
return unless @json['signature'].present? && reply_to_local?
|
||||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
||||||
|
|
|
@ -29,6 +29,7 @@ class Relay < ApplicationRecord
|
||||||
payload = Oj.dump(follow_activity(activity_id))
|
payload = Oj.dump(follow_activity(activity_id))
|
||||||
|
|
||||||
update!(state: :pending, follow_activity_id: activity_id)
|
update!(state: :pending, follow_activity_id: activity_id)
|
||||||
|
DeliveryFailureTracker.new(inbox_url).track_success!
|
||||||
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ class Relay < ApplicationRecord
|
||||||
payload = Oj.dump(unfollow_activity(activity_id))
|
payload = Oj.dump(unfollow_activity(activity_id))
|
||||||
|
|
||||||
update!(state: :idle, follow_activity_id: nil)
|
update!(state: :idle, follow_activity_id: nil)
|
||||||
|
DeliveryFailureTracker.new(inbox_url).track_success!
|
||||||
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
class ActivityPub::ActivitySerializer < ActiveModel::Serializer
|
class ActivityPub::ActivitySerializer < ActiveModel::Serializer
|
||||||
attributes :id, :type, :actor, :published, :to, :cc
|
attributes :id, :type, :actor, :published, :to, :cc
|
||||||
|
|
||||||
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :announce?
|
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :owned_announce?
|
||||||
attribute :proper_uri, key: :object, if: :announce?
|
attribute :proper_uri, key: :object, if: :owned_announce?
|
||||||
attribute :atom_uri, if: :announce?
|
attribute :atom_uri, if: :announce?
|
||||||
|
|
||||||
def id
|
def id
|
||||||
|
@ -42,4 +42,8 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
|
||||||
def announce?
|
def announce?
|
||||||
object.reblog?
|
object.reblog?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def owned_announce?
|
||||||
|
announce? && object.account == object.proper.account && object.proper.private_visibility?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,6 +44,7 @@ class ActivityPub::ProcessCollectionService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_account!
|
def verify_account!
|
||||||
|
@options[:relayed_through_account] = @account
|
||||||
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
|
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
|
||||||
rescue JSON::LD::JsonLdError => e
|
rescue JSON::LD::JsonLdError => e
|
||||||
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
|
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
|
||||||
|
|
|
@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker
|
||||||
sidekiq_options backtrace: true
|
sidekiq_options backtrace: true
|
||||||
|
|
||||||
def perform(account_id, body, delivered_to_account_id = nil)
|
def perform(account_id, body, delivered_to_account_id = nil)
|
||||||
ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id)
|
ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,14 +46,14 @@ class Rack::Attack
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
|
throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
|
||||||
req.api_request? && req.authenticated_user_id
|
req.authenticated_user_id if req.api_request?
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req|
|
throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req|
|
||||||
req.ip if req.api_request?
|
req.ip if req.api_request?
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_media', limit: 30, period: 30.minutes) do |req|
|
throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req|
|
||||||
req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
|
req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,6 +61,13 @@ class Rack::Attack
|
||||||
req.ip if req.post? && req.path == '/api/v1/accounts'
|
req.ip if req.post? && req.path == '/api/v1/accounts'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog/.freeze
|
||||||
|
API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+/.freeze
|
||||||
|
|
||||||
|
throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
|
||||||
|
req.authenticated_user_id if (req.post? && req.path =~ API_DELETE_REBLOG_REGEX) || (req.delete? && req.path =~ API_DELETE_STATUS_REGEX)
|
||||||
|
end
|
||||||
|
|
||||||
throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
|
throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
|
||||||
req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
|
req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,7 +46,7 @@ cs:
|
||||||
choices_html: 'Volby uživatele %{name}:'
|
choices_html: 'Volby uživatele %{name}:'
|
||||||
follow: Sledovat
|
follow: Sledovat
|
||||||
followers:
|
followers:
|
||||||
few: Sledovatelé
|
few: Sledující
|
||||||
one: Sledující
|
one: Sledující
|
||||||
other: Sledujících
|
other: Sledujících
|
||||||
following: Sledovaných
|
following: Sledovaných
|
||||||
|
@ -618,7 +618,7 @@ cs:
|
||||||
lock_link: Zamkněte svůj účet
|
lock_link: Zamkněte svůj účet
|
||||||
purge: Odstranit ze sledujících
|
purge: Odstranit ze sledujících
|
||||||
success:
|
success:
|
||||||
few: V průběhu blokování sledovatelů ze %{count} domén...
|
few: V průběhu blokování sledujících ze %{count} domén...
|
||||||
one: V průběhu blokování sledujících z jedné domény...
|
one: V průběhu blokování sledujících z jedné domény...
|
||||||
other: V průběhu blokování sledujících z %{count} domén...
|
other: V průběhu blokování sledujících z %{count} domén...
|
||||||
true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
|
true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
|
||||||
|
@ -688,7 +688,7 @@ cs:
|
||||||
body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
|
body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
|
||||||
mention: "%{name} vás zmínil/a v:"
|
mention: "%{name} vás zmínil/a v:"
|
||||||
new_followers_summary:
|
new_followers_summary:
|
||||||
few: Navíc jste získal/a %{count} nové sledovatele, zatímco jste byl/a pryč! Skvělé!
|
few: Navíc jste získal/a %{count} nové sledující, zatímco jste byl/a pryč! Skvělé!
|
||||||
one: Navíc jste získal/a jednoho nového sledujícího, zatímco jste byl/a pryč! Hurá!
|
one: Navíc jste získal/a jednoho nového sledujícího, zatímco jste byl/a pryč! Hurá!
|
||||||
other: Navíc jste získal/a %{count} nových sledujících, zatímco jste byl/a pryč! Úžasné!
|
other: Navíc jste získal/a %{count} nových sledujících, zatímco jste byl/a pryč! Úžasné!
|
||||||
subject:
|
subject:
|
||||||
|
|
2
dist/mastodon-streaming.service
vendored
2
dist/mastodon-streaming.service
vendored
|
@ -9,7 +9,7 @@ WorkingDirectory=/home/mastodon/live
|
||||||
Environment="NODE_ENV=production"
|
Environment="NODE_ENV=production"
|
||||||
Environment="PORT=4000"
|
Environment="PORT=4000"
|
||||||
Environment="STREAMING_CLUSTER_NUM=1"
|
Environment="STREAMING_CLUSTER_NUM=1"
|
||||||
ExecStart=/usr/bin/npm run start
|
ExecStart=/usr/bin/node ./streaming
|
||||||
TimeoutSec=15
|
TimeoutSec=15
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||||
#
|
|
||||||
# To ban all spiders from the entire site uncomment the next two lines:
|
User-agent: *
|
||||||
# User-agent: *
|
Disallow: /media_proxy/
|
||||||
# Disallow: /
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ActivityPub::Activity::Announce do
|
RSpec.describe ActivityPub::Activity::Announce do
|
||||||
let(:sender) { Fabricate(:account) }
|
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers') }
|
||||||
let(:recipient) { Fabricate(:account) }
|
let(:recipient) { Fabricate(:account) }
|
||||||
let(:status) { Fabricate(:status, account: recipient) }
|
let(:status) { Fabricate(:status, account: recipient) }
|
||||||
|
|
||||||
|
@ -11,19 +11,60 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
object: object_json,
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
before do
|
before do
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'a known status' do
|
||||||
|
let(:object_json) do
|
||||||
|
ActivityPub::TagManager.instance.uri_for(status)
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a reblog by sender of status' do
|
it 'creates a reblog by sender of status' do
|
||||||
expect(sender.reblogged?(status)).to be true
|
expect(sender.reblogged?(status)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'self-boost of a previously unknown status with missing attributedTo' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'self-boost of a previously unknown status with correct attributedTo' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue