Merge pull request #1097 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
remotes/1727458204337373841/tmp_refs/heads/signup-info-prompt
ThibG 2019-06-07 22:15:44 +02:00 committed by GitHub
commit 11c28abcfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 274 additions and 538 deletions

View File

@ -1,3 +1,6 @@
require:
- rubocop-rails
AllCops: AllCops:
TargetRubyVersion: 2.3 TargetRubyVersion: 2.3
Exclude: Exclude:
@ -82,6 +85,9 @@ Rails/Exit:
- 'lib/mastodon/*' - 'lib/mastodon/*'
- 'lib/cli.rb' - 'lib/cli.rb'
Rails/HelperInstanceVariable:
Enabled: false
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false

View File

@ -4,261 +4,34 @@
files: files:
include: app/javascript/styles/**/*.scss include: app/javascript/styles/**/*.scss
ignore: ignore:
- app/javascript/styles/reset.scss - app/javascript/styles/mastodon/reset.scss
linters: rules:
# Reports when you use improper spacing around ! (the "bang") in !default, # Disallows
# !global, !important, and !optional flags. no-color-literals: 0
BangFormat: no-css-comments: 0
enabled: false no-duplicate-properties: 0
no-ids: 0
no-important: 0
no-mergeable-selectors: 0
no-misspelled-properties: 0
no-qualifying-elements: 0
no-transition-all: 0
no-vendor-prefixes: 0
# Whether or not to prefer `border: 0` over `border: none`. # Nesting
BorderZero: force-element-nesting: 0
enabled: false force-attribute-nesting: 0
force-pseudo-nesting: 0
# Reports when you define a rule set using a selector with chained classes # Name Formats
# (a.k.a. adjoining classes). class-name-format: 0
ChainedClasses: leading-zero: 0
enabled: false
# Prefer hexadecimal color codes over color keywords. # Style Guide
# (e.g. `color: green` is a color keyword) attribute-quotes: 0
ColorKeyword: hex-length: 0
enabled: false indentation: 0
nesting-depth: 0
# Prefer color literals (keywords or hexadecimal codes) to be used only in property-sort-order: 0
# variable declarations. They should be referred to via variables everywhere quotes: 0
# else.
ColorVariable:
enabled: true
# Which form of comments to prefer in CSS.
Comment:
enabled: false
# Reports @debug statements (which you probably left behind accidentally).
DebugStatement:
enabled: false
# Rule sets should be ordered as follows:
# - @extend declarations
# - @include declarations without inner @content
# - properties, @include declarations with inner @content
# - nested rule sets.
DeclarationOrder:
enabled: false
# `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file.
# See https://github.com/brigade/scss-lint#disabling-linters-via-source for
# more information.
DisableLinterReason:
enabled: true
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: false
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
enabled: true
# Reports when you have an empty rule set.
EmptyRule:
enabled: true
# Reports when you have an @extend directive.
ExtendDirective:
enabled: false
# Files should always have a final newline. This results in better diffs
# when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line.
FinalNewline:
enabled: false
# HEX colors should use three-character values where possible.
HexLength:
enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation:
enabled: true
# Avoid using ID selectors.
IdSelector:
enabled: false
# The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension.
ImportPath:
enabled: false
# Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule:
enabled: false
# Indentation should always be done in increments of 2 spaces.
Indentation:
enabled: true
width: 2
# Don't write leading zeros for numeric values with a decimal point.
LeadingZero:
enabled: false
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
NameFormat:
enabled: false
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
# Always use placeholder selectors in @extend.
PlaceholderInExtend:
enabled: false
# Sort properties in a strict order.
PropertySortOrder:
enabled: false
# Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties).
PropertySpelling:
enabled: false
# Configure which units are allowed for property values.
PropertyUnits:
enabled: false
# Pseudo-elements, like ::before, and ::first-letter, should be declared
# with two colons. Pseudo-classes, like :hover and :first-child, should
# be declared with one colon.
PseudoElement:
enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
enabled: false
# Don't write selectors with a depth of applicability greater than 3.
SelectorDepth:
enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
Shorthand:
enabled: true
# Each property should have its own line, except in the special case of
# single line rulesets.
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
# Split selectors onto separate lines after each comma, and have each
# individual selector occupy a single line.
SingleLinePerSelector:
enabled: true
# Commas in lists should be followed by a space.
SpaceAfterComma:
enabled: false
# Properties should be formatted with a single space separating the colon
# from the property's value.
SpaceAfterPropertyColon:
enabled: true
# Properties should be formatted with no space between the name and the
# colon.
SpaceAfterPropertyName:
enabled: true
# Variables should be formatted with a single space separating the colon
# from the variable's value.
SpaceAfterVariableColon:
enabled: true
# Variables should be formatted with no space between the name and the
# colon.
SpaceAfterVariableName:
enabled: false
# Operators should be formatted with a single space on both sides of an
# infix operator.
SpaceAroundOperator:
enabled: true
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
enabled: true
# Parentheses should not be padded with spaces.
SpaceBetweenParens:
enabled: false
# Enforces that string literals should be written with a consistent form
# of quotes (single or double).
StringQuotes:
enabled: false
# Property values, @extend, @include, and @import directives, and variable
# declarations should always end with a semicolon.
TrailingSemicolon:
enabled: true
# Reports lines containing trailing whitespace.
TrailingWhitespace:
enabled: true
# Don't write trailing zeros for numeric values with a decimal point.
TrailingZero:
enabled: false
# Don't use the `all` keyword to specify transition properties.
TransitionAll:
enabled: false
# Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
enabled: false
# Do not use parent selector references (&) when they would otherwise
# be unnecessary.
UnnecessaryParentReference:
enabled: false
# URLs should be valid and not contain protocols or domain names.
UrlFormat:
enabled: true
# URLs should always be enclosed within quotes.
UrlQuotes:
enabled: true
# Properties, like color and font, are easier to read and maintain
# when defined using variables rather than literals.
VariableForProperty:
enabled: false
# Avoid vendor prefixes. Or rather: don't write them yourself.
VendorPrefix:
enabled: false
# Omit length units on zero values, e.g. `0px` vs. `0`.
ZeroUnit:
enabled: true

View File

@ -132,6 +132,7 @@ group :development do
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.71', require: false gem 'rubocop', '~> 0.71', require: false
gem 'rubocop-rails', '~> 2.0', require: false
gem 'brakeman', '~> 4.5', require: false gem 'brakeman', '~> 4.5', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false

View File

@ -534,6 +534,9 @@ GEM
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7) unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.0.0)
rack (>= 2.0)
rubocop (>= 0.70.0)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
ruby-saml (1.9.0) ruby-saml (1.9.0)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
@ -740,6 +743,7 @@ DEPENDENCIES
rspec-rails (~> 3.8) rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.71) rubocop (~> 0.71)
rubocop-rails (~> 2.0)
sanitize (~> 5.0) sanitize (~> 5.0)
sidekiq (~> 5.2) sidekiq (~> 5.2)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)

View File

@ -47,8 +47,6 @@ class AccountsController < ApplicationController
end end
format.json do format.json do
mark_cacheable!
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end end

View File

@ -9,8 +9,6 @@ class ActivityPub::CollectionsController < Api::BaseController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
skip_session!
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new( ActiveModelSerializers::SerializableResource.new(
collection_presenter, collection_presenter,

View File

@ -10,10 +10,7 @@ class ActivityPub::OutboxesController < Api::BaseController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
unless page_requested? expires_in 1.minute, public: true unless page_requested?
skip_session!
expires_in 1.minute, public: true
end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end

View File

@ -48,13 +48,13 @@ module Admin
def approve def approve
authorize @account.user, :approve? authorize @account.user, :approve?
@account.user.approve! @account.user.approve!
redirect_to admin_accounts_path(pending: '1') redirect_to admin_pending_accounts_path
end end
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
redirect_to admin_accounts_path(pending: '1') redirect_to admin_pending_accounts_path
end end
def unsilence def unsilence

View File

@ -228,11 +228,6 @@ class ApplicationController < ActionController::Base
end end
def mark_cacheable! def mark_cacheable!
skip_session!
expires_in 0, public: true expires_in 0, public: true
end end
def skip_session!
request.session_options[:skip] = true
end
end end

View File

@ -70,7 +70,6 @@ module AccountControllerConcern
def check_account_suspension def check_account_suspension
if @account.suspended? if @account.suspended?
skip_session!
expires_in(3.minutes, public: true) expires_in(3.minutes, public: true)
gone gone
end end

View File

@ -4,7 +4,6 @@ class CustomCssController < ApplicationController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
skip_session!
render plain: Setting.custom_css || '', content_type: 'text/css' render plain: Setting.custom_css || '', content_type: 'text/css'
end end
end end

View File

@ -7,8 +7,6 @@ class EmojisController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.json do format.json do
skip_session!
render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
end end

View File

@ -20,10 +20,7 @@ class FollowerAccountsController < ApplicationController
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
if params[:page].blank? expires_in 3.minutes, public: true if params[:page].blank?
skip_session!
expires_in 3.minutes, public: true
end
render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,

View File

@ -20,10 +20,7 @@ class FollowingAccountsController < ApplicationController
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
if params[:page].blank? expires_in 3.minutes, public: true if params[:page].blank?
skip_session!
expires_in 3.minutes, public: true
end
render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,

View File

@ -29,10 +29,7 @@ class StatusesController < ApplicationController
format.html do format.html do
use_pack 'public' use_pack 'public'
unless user_signed_in? expires_in 10.seconds, public: true if current_account.nil?
skip_session!
expires_in 10.seconds, public: true
end
@body_classes = 'with-modals' @body_classes = 'with-modals'
@ -43,8 +40,6 @@ class StatusesController < ApplicationController
end end
format.json do format.json do
mark_cacheable! unless @stream_entry.hidden?
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
end end
@ -53,8 +48,6 @@ class StatusesController < ApplicationController
end end
def activity def activity
skip_session!
render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
end end
@ -64,7 +57,6 @@ class StatusesController < ApplicationController
use_pack 'embed' use_pack 'embed'
raise ActiveRecord::RecordNotFound if @status.hidden? raise ActiveRecord::RecordNotFound if @status.hidden?
skip_session!
expires_in 180, public: true expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL' response.headers['X-Frame-Options'] = 'ALLOWALL'
@autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay])
@ -73,8 +65,6 @@ class StatusesController < ApplicationController
end end
def replies def replies
skip_session!
render json: replies_collection_presenter, render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter, adapter: ActivityPub::Adapter,

View File

@ -17,19 +17,13 @@ class StreamEntriesController < ApplicationController
format.html do format.html do
use_pack 'public' use_pack 'public'
unless user_signed_in? expires_in 5.minutes, public: true unless @stream_entry.hidden?
skip_session!
expires_in 5.minutes, public: true
end
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status' redirect_to short_account_status_url(params[:account_username], @stream_entry.activity)
end end
format.atom do format.atom do
unless @stream_entry.hidden? expires_in 3.minutes, public: true unless @stream_entry.hidden?
skip_session!
expires_in 3.minutes, public: true
end
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
end end
@ -57,7 +51,7 @@ class StreamEntriesController < ApplicationController
def set_stream_entry def set_stream_entry
@stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id]) @stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id])
@type = @stream_entry.activity_type.downcase @type = 'status'
raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil?
authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only? authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only?

View File

@ -138,8 +138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
this.setState({ suggestionsHidden: true, focused: false }); this.setState({ suggestionsHidden: true, focused: false });
} }
onFocus = () => { onFocus = (e) => {
this.setState({ focused: true }); this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus(e);
}
} }
onSuggestionClick = (e) => { onSuggestionClick = (e) => {

View File

@ -28,6 +28,10 @@ const messages = defineMessages({
export default @injectIntl export default @injectIntl
class ComposeForm extends ImmutablePureComponent { class ComposeForm extends ImmutablePureComponent {
setRef = c => {
this.composeForm = c;
};
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
}; };
@ -208,6 +212,10 @@ class ComposeForm extends ImmutablePureComponent {
} }
} }
handleFocus = () => {
this.composeForm.scrollIntoView();
}
// This statement does several things: // This statement does several things:
// - If we're beginning a reply, and, // - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end // - Replying to zero or one users, places the cursor at the end
@ -302,7 +310,7 @@ class ComposeForm extends ImmutablePureComponent {
let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia); let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
return ( return (
<div className='composer'> <div className='composer' ref={this.setRef}>
<WarningContainer /> <WarningContainer />
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
@ -337,6 +345,7 @@ class ComposeForm extends ImmutablePureComponent {
value={this.props.text} value={this.props.text}
onChange={this.handleChange} onChange={this.handleChange}
suggestions={this.props.suggestions} suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={onFetchSuggestions} onSuggestionsFetchRequested={onFetchSuggestions}
onSuggestionsClearRequested={onClearSuggestions} onSuggestionsClearRequested={onClearSuggestions}

View File

@ -75,6 +75,23 @@ export default class ListTimeline extends React.PureComponent {
this.disconnect = dispatch(connectListStream(id)); this.disconnect = dispatch(connectListStream(id));
} }
componentWillReceiveProps (nextProps) {
const { dispatch } = this.props;
const { id } = nextProps.params;
if (id !== this.props.params.id) {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
dispatch(fetchList(id));
dispatch(expandListTimeline(id));
this.disconnect = dispatch(connectListStream(id));
}
}
componentWillUnmount () { componentWillUnmount () {
if (this.disconnect) { if (this.disconnect) {
this.disconnect(); this.disconnect();

View File

@ -442,6 +442,7 @@ export default function compose(state = initialState, action) {
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('caretPosition', null); map.set('caretPosition', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('sensitive', action.status.get('sensitive'));
if (action.status.get('spoiler_text').length > 0) { if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true); map.set('spoiler', true);

View File

@ -35,7 +35,9 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
if (!next && !isLoadingRecent) mMap.set('hasMore', false); if (!next && !isLoadingRecent) mMap.set('hasMore', false);
if (!statuses.isEmpty()) { if (timeline.endsWith(':pinned')) {
mMap.set('items', statuses.map(status => status.get('id')));
} else if (!statuses.isEmpty()) {
mMap.update('items', ImmutableList(), oldIds => { mMap.update('items', ImmutableList(), oldIds => {
const newIds = statuses.map(status => status.get('id')); const newIds = statuses.map(status => status.get('id'));
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;

View File

@ -138,8 +138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
this.setState({ suggestionsHidden: true, focused: false }); this.setState({ suggestionsHidden: true, focused: false });
} }
onFocus = () => { onFocus = (e) => {
this.setState({ focused: true }); this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus(e);
}
} }
onSuggestionClick = (e) => { onSuggestionClick = (e) => {

View File

@ -34,6 +34,10 @@ const messages = defineMessages({
export default @injectIntl export default @injectIntl
class ComposeForm extends ImmutablePureComponent { class ComposeForm extends ImmutablePureComponent {
setRef = c => {
this.composeForm = c;
};
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
}; };
@ -115,6 +119,10 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onChangeSpoilerText(e.target.value); this.props.onChangeSpoilerText(e.target.value);
} }
handleFocus = () => {
this.composeForm.scrollIntoView();
}
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
// This statement does several things: // This statement does several things:
// - If we're beginning a reply, and, // - If we're beginning a reply, and,
@ -178,7 +186,7 @@ class ComposeForm extends ImmutablePureComponent {
} }
return ( return (
<div className='compose-form'> <div className='compose-form' ref={this.setRef}>
<WarningContainer /> <WarningContainer />
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
@ -201,7 +209,7 @@ class ComposeForm extends ImmutablePureComponent {
/> />
</div> </div>
<div className='emoji-picker-wrapper'> <div className={`emoji-picker-wrapper ${this.props.showSearch ? 'emoji-picker-wrapper--hidden' : ''}`}>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
</div> </div>
@ -212,6 +220,7 @@ class ComposeForm extends ImmutablePureComponent {
value={this.props.text} value={this.props.text}
onChange={this.handleChange} onChange={this.handleChange}
suggestions={this.props.suggestions} suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}

View File

@ -21,7 +21,7 @@ class SearchPopout extends React.PureComponent {
const { style } = this.props; const { style } = this.props;
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />; const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
return ( return (
<div style={{ ...style, position: 'absolute', width: 285 }}> <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => ( {({ opacity, scaleX, scaleY }) => (
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>

View File

@ -75,6 +75,23 @@ class ListTimeline extends React.PureComponent {
this.disconnect = dispatch(connectListStream(id)); this.disconnect = dispatch(connectListStream(id));
} }
componentWillReceiveProps (nextProps) {
const { dispatch } = this.props;
const { id } = nextProps.params;
if (id !== this.props.params.id) {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
dispatch(fetchList(id));
dispatch(expandListTimeline(id));
this.disconnect = dispatch(connectListStream(id));
}
}
componentWillUnmount () { componentWillUnmount () {
if (this.disconnect) { if (this.disconnect) {
this.disconnect(); this.disconnect();

View File

@ -338,6 +338,7 @@ export default function compose(state = initialState, action) {
map.set('focusDate', new Date()); map.set('focusDate', new Date());
map.set('caretPosition', null); map.set('caretPosition', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('sensitive', action.status.get('sensitive'));
if (action.status.get('spoiler_text').length > 0) { if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true); map.set('spoiler', true);

View File

@ -35,14 +35,12 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
if (!next && !isLoadingRecent) mMap.set('hasMore', false); if (!next && !isLoadingRecent) mMap.set('hasMore', false);
if (!statuses.isEmpty()) { if (timeline.endsWith(':pinned')) {
mMap.set('items', statuses.map(status => status.get('id')));
} else if (!statuses.isEmpty()) {
mMap.update('items', ImmutableList(), oldIds => { mMap.update('items', ImmutableList(), oldIds => {
const newIds = statuses.map(status => status.get('id')); const newIds = statuses.map(status => status.get('id'));
if (timeline.indexOf(':pinned') !== -1) {
return newIds;
}
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);

View File

@ -5,7 +5,7 @@
&-description { &-description {
input { input {
&::placeholder { &::placeholder {
opacity: 1.0; opacity: 1;
} }
} }
} }

View File

@ -20,5 +20,5 @@ $highlight-text-color: $classic-highlight-color !default;
$action-button-color: #8d9ac2; $action-button-color: #8d9ac2;
$inverted-text-color: $black !default; $inverted-text-color: $black !default;
$lighter-text-color: darken($ui-base-color,6%) !default; $lighter-text-color: darken($ui-base-color, 6%) !default;
$light-text-color: darken($ui-primary-color, 40%) !default; $light-text-color: darken($ui-primary-color, 40%) !default;

View File

@ -279,6 +279,8 @@ h5 {
} }
.hero-with-button { .hero-with-button {
padding-bottom: 16px;
h1 { h1 {
margin-bottom: 4px; margin-bottom: 4px;
} }
@ -286,8 +288,6 @@ h5 {
p.lead { p.lead {
margin-bottom: 32px; margin-bottom: 32px;
} }
padding-bottom: 16px;
} }
.header { .header {

View File

@ -1,21 +1,21 @@
@mixin avatar-radius() { @mixin avatar-radius {
border-radius: 4px; border-radius: 4px;
background: transparent no-repeat; background: transparent no-repeat;
background-position: 50%; background-position: 50%;
background-clip: padding-box; background-clip: padding-box;
} }
@mixin avatar-size($size:48px) { @mixin avatar-size($size: 48px) {
width: $size; width: $size;
height: $size; height: $size;
background-size: $size $size; background-size: $size $size;
} }
@mixin search-input() { @mixin search-input {
outline: 0; outline: 0;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
border: none; border: 0;
box-shadow: none; box-shadow: none;
font-family: inherit; font-family: inherit;
background: $ui-base-color; background: $ui-base-color;
@ -42,7 +42,7 @@
} }
} }
@mixin search-popout() { @mixin search-popout {
background: $simple-background-color; background: $simple-background-color;
border-radius: 4px; border-radius: 4px;
padding: 10px 14px; padding: 10px 14px;

View File

@ -171,7 +171,7 @@ $content-width: 840px;
text-transform: none; text-transform: none;
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: 0;
} }
& > p { & > p {

View File

@ -2,7 +2,8 @@
@if type-of($color) == 'color' { @if type-of($color) == 'color' {
$color: str-slice(ie-hex-str($color), 4); $color: str-slice(ie-hex-str($color), 4);
} }
@return '%23' + unquote($color)
@return '%23' + unquote($color);
} }
body { body {
@ -15,7 +16,7 @@ body {
text-rendering: optimizelegibility; text-rendering: optimizelegibility;
font-feature-settings: "kern"; font-feature-settings: "kern";
text-size-adjust: none; text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
&.system-font { &.system-font {

View File

@ -128,7 +128,7 @@
display: inline-block; display: inline-block;
padding: 0; padding: 0;
color: $action-button-color; color: $action-button-color;
border: none; border: 0;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
transition: color 100ms ease-in; transition: color 100ms ease-in;
@ -196,7 +196,7 @@
.text-icon-button { .text-icon-button {
color: $lighter-text-color; color: $lighter-text-color;
border: none; border: 0;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
@ -353,12 +353,12 @@
.spoiler-input { .spoiler-input {
height: 0; height: 0;
transform-origin: bottom; transform-origin: bottom;
opacity: 0.0; opacity: 0;
&.spoiler-input--visible { &.spoiler-input--visible {
height: 36px; height: 36px;
margin-bottom: 11px; margin-bottom: 11px;
opacity: 1.0; opacity: 1;
} }
} }
@ -408,12 +408,20 @@
} }
} }
.emoji-picker-wrapper,
.autosuggest-textarea__suggestions-wrapper { .autosuggest-textarea__suggestions-wrapper {
position: relative; position: relative;
height: 0; height: 0;
} }
.emoji-picker-wrapper {
position: relative;
height: 0;
&.emoji-picker-wrapper--hidden {
display: none;
}
}
.autosuggest-textarea__suggestions { .autosuggest-textarea__suggestions {
box-sizing: border-box; box-sizing: border-box;
display: none; display: none;
@ -1185,7 +1193,7 @@
} }
.account__avatar { .account__avatar {
@include avatar-radius(); @include avatar-radius;
position: relative; position: relative;
&-inline { &-inline {
@ -1195,11 +1203,11 @@
} }
&-composite { &-composite {
@include avatar-radius(); @include avatar-radius;
overflow: hidden; overflow: hidden;
& > div { & > div {
@include avatar-radius(); @include avatar-radius;
float: left; float: left;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
@ -1215,12 +1223,12 @@ a .account__avatar {
@include avatar-size(48px); @include avatar-size(48px);
&-base { &-base {
@include avatar-radius(); @include avatar-radius;
@include avatar-size(36px); @include avatar-size(36px);
} }
&-overlay { &-overlay {
@include avatar-radius(); @include avatar-radius;
@include avatar-size(24px); @include avatar-size(24px);
position: absolute; position: absolute;
@ -1598,13 +1606,13 @@ a.account__display-name {
.icon-button.close { .icon-button.close {
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
transform: scale(0.0, 1.0) translate(-100%, 0); transform: scale(0, 1) translate(-100%, 0);
opacity: 0; opacity: 0;
} }
.compose__action-bar .icon-button { .compose__action-bar .icon-button {
pointer-events: auto; pointer-events: auto;
transform: scale(1.0, 1.0) translate(0, 0); transform: scale(1, 1) translate(0, 0);
opacity: 1; opacity: 1;
} }
} }
@ -2071,6 +2079,10 @@ a.account__display-name {
.account { .account {
padding: 15px 10px; padding: 15px 10px;
&__header__bio {
margin: 0 -10px;
}
} }
.notification { .notification {
@ -2699,7 +2711,7 @@ a.account__display-name {
.setting-text { .setting-text {
color: $darker-text-color; color: $darker-text-color;
background: transparent; background: transparent;
border: none; border: 0;
border-bottom: 2px solid $ui-primary-color; border-bottom: 2px solid $ui-primary-color;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
@ -3037,7 +3049,7 @@ a.status-card.compact:hover {
& > button { & > button {
margin: 0; margin: 0;
border: none; border: 0;
padding: 15px 0 15px 15px; padding: 15px 0 15px 15px;
color: inherit; color: inherit;
background: transparent; background: transparent;
@ -3202,11 +3214,11 @@ a.status-card.compact:hover {
} }
.no-reduce-motion .loading-indicator span { .no-reduce-motion .loading-indicator span {
animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
} }
.no-reduce-motion .loading-indicator__figure { .no-reduce-motion .loading-indicator__figure {
animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
} }
@keyframes loader-figure { @keyframes loader-figure {
@ -3373,7 +3385,7 @@ a.status-card.compact:hover {
.column-select { .column-select {
&__control { &__control {
@include search-input(); @include search-input;
} }
&__placeholder { &__placeholder {
@ -3424,7 +3436,7 @@ a.status-card.compact:hover {
} }
&__menu { &__menu {
@include search-popout(); @include search-popout;
padding: 0; padding: 0;
background: $ui-secondary-color; background: $ui-secondary-color;
} }
@ -3585,7 +3597,7 @@ a.status-card.compact:hover {
.no-reduce-motion .shake-bottom { .no-reduce-motion .shake-bottom {
transform-origin: 50% 100%; transform-origin: 50% 100%;
animation: shake-bottom 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) 2s 2 both; animation: shake-bottom 0.8s cubic-bezier(0.455, 0.03, 0.515, 0.955) 2s 2 both;
} }
.emoji-picker-dropdown__menu { .emoji-picker-dropdown__menu {
@ -3880,10 +3892,11 @@ a.status-card.compact:hover {
} }
.search__input { .search__input {
@include search-input;
display: block; display: block;
padding: 10px; padding: 10px;
padding-right: 30px; padding-right: 30px;
@include search-input();
} }
.search__icon { .search__icon {
@ -4491,14 +4504,14 @@ a.status-card.compact:hover {
} }
.actions-modal { .actions-modal {
max-height: 80vh;
max-width: 80vw;
.status { .status {
overflow-y: auto; overflow-y: auto;
max-height: 300px; max-height: 300px;
} }
max-height: 80vh;
max-width: 80vw;
.actions-modal__item-label { .actions-modal__item-label {
font-weight: 500; font-weight: 500;
} }
@ -4713,7 +4726,7 @@ a.status-card.compact:hover {
} }
.media-gallery__item { .media-gallery__item {
border: none; border: 0;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
float: left; float: left;
@ -5173,7 +5186,7 @@ a.status-card.compact:hover {
} }
.account-gallery__item { .account-gallery__item {
border: none; border: 0;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
position: relative; position: relative;
@ -5247,7 +5260,7 @@ a.status-card.compact:hover {
} }
.search-popout { .search-popout {
@include search-popout(); @include search-popout;
} }
noscript { noscript {
@ -5349,14 +5362,14 @@ noscript {
.icon-button.close { .icon-button.close {
pointer-events: auto; pointer-events: auto;
opacity: 1; opacity: 1;
transform: scale(1.0, 1.0) translate(0, 0); transform: scale(1, 1) translate(0, 0);
bottom: 5px; bottom: 5px;
} }
.compose__action-bar .icon-button { .compose__action-bar .icon-button {
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
transform: scale(0.0, 1.0) translate(100%, 0); transform: scale(0, 1) translate(100%, 0);
} }
} }
} }
@ -5386,7 +5399,7 @@ noscript {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
width: 100%; width: 100%;
border: none; border: 0;
padding: 10px; padding: 10px;
font-family: $font-monospace, monospace; font-family: $font-monospace, monospace;
background: $ui-base-color; background: $ui-base-color;

View File

@ -121,7 +121,7 @@
grid-auto-rows: max-content; grid-auto-rows: max-content;
.column-0 { .column-0 {
grid-column: 1/3; grid-column: 1 / 3;
grid-row: 1; grid-row: 1;
} }
@ -136,7 +136,7 @@
} }
.column-3 { .column-3 {
grid-column: 1/3; grid-column: 1 / 3;
grid-row: 3; grid-row: 3;
} }

View File

@ -1,14 +1,14 @@
.emoji-mart { .emoji-mart {
font-size: 13px;
display: inline-block;
color: $inverted-text-color;
&, &,
* { * {
box-sizing: border-box; box-sizing: border-box;
line-height: 1.15; line-height: 1.15;
} }
font-size: 13px;
display: inline-block;
color: $inverted-text-color;
.emoji-mart-emoji { .emoji-mart-emoji {
padding: 6px; padding: 6px;
} }

View File

@ -553,7 +553,7 @@ code {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
width: 100%; width: 100%;
border: none; border: 0;
padding: 10px; padding: 10px;
font-family: $font-monospace, monospace; font-family: $font-monospace, monospace;
background: $ui-base-color; background: $ui-base-color;

View File

@ -47,7 +47,6 @@
width: 100%; width: 100%;
font-size: 14px; font-size: 14px;
color: $inverted-text-color; color: $inverted-text-color;
display: block;
outline: 0; outline: 0;
font-family: inherit; font-family: inherit;
background: $simple-background-color; background: $simple-background-color;

View File

@ -180,7 +180,6 @@ body.rtl {
} }
.fa-ul { .fa-ul {
margin-left: 0;
margin-left: 2.14285714em; margin-left: 2.14285714em;
} }

View File

@ -143,7 +143,7 @@ class ActivityPub::Activity
# If the boosted toot is embedded and it is a self-boost, handle it like a Create # If the boosted toot is embedded and it is a self-boost, handle it like a Create
unless unsupported_object_type? unless unsupported_object_type?
actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri actor_id = value_or_id(first_of_value(@object['attributedTo']))
if actor_id == @account.uri if actor_id == @account.uri
return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::Activity::Follow < ActivityPub::Activity class ActivityPub::Activity::Follow < ActivityPub::Activity
include Payloadable
def perform def perform
target_account = account_from_uri(object_uri) target_account = account_from_uri(object_uri)
@ -28,7 +30,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
end end
def reject_follow_request!(target_account) def reject_follow_request!(target_account)
json = ActiveModelSerializers::SerializableResource.new(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).to_json json = Oj.dump(serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer))
ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url) ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url)
end end
end end

View File

@ -208,6 +208,10 @@ class Account < ApplicationRecord
end end
end end
def sign?
true
end
def keypair def keypair
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end end

View File

@ -3,6 +3,7 @@
class Form::AccountBatch class Form::AccountBatch
include ActiveModel::Model include ActiveModel::Model
include Authorization include Authorization
include Payloadable
attr_accessor :account_ids, :action, :current_account attr_accessor :account_ids, :action, :current_account
@ -54,13 +55,7 @@ class Form::AccountBatch
return unless follow.account.activitypub? return unless follow.account.activitypub?
json = ActiveModelSerializers::SerializableResource.new( ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), current_account.id, follow.account.inbox_url)
follow,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url)
end end
def approve! def approve!

View File

@ -211,6 +211,8 @@ class Status < ApplicationRecord
public_visibility? || unlisted_visibility? public_visibility? || unlisted_visibility?
end end
alias sign? distributable?
def with_media? def with_media?
media_attachments.any? media_attachments.any?
end end
@ -529,7 +531,7 @@ class Status < ApplicationRecord
return if direct_visibility? return if direct_visibility?
account&.increment_count!(:statuses_count) account&.increment_count!(:statuses_count)
reblog&.increment_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?) reblog&.increment_count!(:reblogs_count) if reblog?
thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
end end
@ -537,7 +539,7 @@ class Status < ApplicationRecord
return if direct_visibility? || marked_for_mass_destruction? return if direct_visibility? || marked_for_mass_destruction?
account&.decrement_count!(:statuses_count) account&.decrement_count!(:statuses_count)
reblog&.decrement_count!(:reblogs_count) if reblog? && (public_visibility? || unlisted_visibility?) reblog&.decrement_count!(:reblogs_count) if reblog?
thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
end end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class AfterBlockDomainFromAccountService < BaseService class AfterBlockDomainFromAccountService < BaseService
include Payloadable
# This service does not create an AccountDomainBlock record, # This service does not create an AccountDomainBlock record,
# it's meant to be called after such a record has been created # it's meant to be called after such a record has been created
# synchronously, to "clean up" # synchronously, to "clean up"
@ -31,12 +33,6 @@ class AfterBlockDomainFromAccountService < BaseService
return unless follow.account.activitypub? return unless follow.account.activitypub?
json = ActiveModelSerializers::SerializableResource.new( ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url)
follow,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
ActivityPub::DeliveryWorker.perform_async(json, @account.id, follow.account.inbox_url)
end end
end end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class AuthorizeFollowService < BaseService class AuthorizeFollowService < BaseService
include Payloadable
def call(source_account, target_account, **options) def call(source_account, target_account, **options)
if options[:skip_follow_request] if options[:skip_follow_request]
follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri]) follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri])
@ -24,11 +26,7 @@ class AuthorizeFollowService < BaseService
end end
def build_json(follow_request) def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer))
follow_request,
serializer: ActivityPub::AcceptFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def build_xml(follow_request) def build_xml(follow_request)

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class BlockService < BaseService class BlockService < BaseService
include Payloadable
def call(account, target_account) def call(account, target_account)
return if account.id == target_account.id return if account.id == target_account.id
@ -26,11 +28,7 @@ class BlockService < BaseService
end end
def build_json(block) def build_json(block)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer))
block,
serializer: ActivityPub::BlockSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def build_xml(block) def build_xml(block)

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Payloadable
def serialize_payload(record, serializer, options = {})
signer = options.delete(:signer)
sign_with = options.delete(:sign_with)
payload = ActiveModelSerializers::SerializableResource.new(record, options.merge(serializer: serializer, adapter: ActivityPub::Adapter)).as_json
if (record.respond_to?(:sign?) && record.sign?) && signer && signing_enabled?
ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with)
else
payload
end
end
def signing_enabled?
true
end
end

View File

@ -2,6 +2,7 @@
class FavouriteService < BaseService class FavouriteService < BaseService
include Authorization include Authorization
include Payloadable
# Favourite a status and notify remote user # Favourite a status and notify remote user
# @param [Account] account # @param [Account] account
@ -43,11 +44,7 @@ class FavouriteService < BaseService
end end
def build_json(favourite) def build_json(favourite)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer))
favourite,
serializer: ActivityPub::LikeSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(favourite.account))
end end
def build_xml(favourite) def build_xml(favourite)

View File

@ -2,6 +2,7 @@
class FollowService < BaseService class FollowService < BaseService
include Redisable include Redisable
include Payloadable
# Follow a remote user, notify remote user about the follow # Follow a remote user, notify remote user about the follow
# @param [Account] source_account From which to follow # @param [Account] source_account From which to follow
@ -78,10 +79,6 @@ class FollowService < BaseService
end end
def build_json(follow_request) def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer))
follow_request,
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
end end

View File

@ -2,6 +2,7 @@
class ProcessMentionsService < BaseService class ProcessMentionsService < BaseService
include StreamEntryRenderer include StreamEntryRenderer
include Payloadable
# Scan status for mentions and fetch remote mentioned users, create # Scan status for mentions and fetch remote mentioned users, create
# local mention pointers, send Salmon notifications to mentioned # local mention pointers, send Salmon notifications to mentioned
@ -61,12 +62,7 @@ class ProcessMentionsService < BaseService
def activitypub_json def activitypub_json
return @activitypub_json if defined?(@activitypub_json) return @activitypub_json if defined?(@activitypub_json)
payload = ActiveModelSerializers::SerializableResource.new( @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
@status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).as_json
@activitypub_json = Oj.dump(@status.distributable? ? ActivityPub::LinkedDataSignature.new(payload).sign!(@status.account) : payload)
end end
def resolve_account_service def resolve_account_service

View File

@ -3,6 +3,7 @@
class ReblogService < BaseService class ReblogService < BaseService
include Authorization include Authorization
include StreamEntryRenderer include StreamEntryRenderer
include Payloadable
# Reblog a status and notify its remote author # Reblog a status and notify its remote author
# @param [Account] account Account to reblog from # @param [Account] account Account to reblog from
@ -56,10 +57,6 @@ class ReblogService < BaseService
end end
def build_json(reblog) def build_json(reblog)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(reblog, ActivityPub::ActivitySerializer, signer: reblog.account))
reblog,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(reblog.account))
end end
end end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class RejectFollowService < BaseService class RejectFollowService < BaseService
include Payloadable
def call(source_account, target_account) def call(source_account, target_account)
follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
follow_request.reject! follow_request.reject!
@ -19,11 +21,7 @@ class RejectFollowService < BaseService
end end
def build_json(follow_request) def build_json(follow_request)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer))
follow_request,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def build_xml(follow_request) def build_xml(follow_request)

View File

@ -3,6 +3,7 @@
class RemoveStatusService < BaseService class RemoveStatusService < BaseService
include StreamEntryRenderer include StreamEntryRenderer
include Redisable include Redisable
include Payloadable
def call(status, **options) def call(status, **options)
@payload = Oj.dump(event: :delete, payload: status.id.to_s) @payload = Oj.dump(event: :delete, payload: status.id.to_s)
@ -116,15 +117,7 @@ class RemoveStatusService < BaseService
end end
def signed_activity_json def signed_activity_json
@signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account)) @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account))
end
def activity_json
@activity_json ||= ActiveModelSerializers::SerializableResource.new(
@status,
serializer: @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
adapter: ActivityPub::Adapter
).as_json
end end
def remove_reblogs def remove_reblogs

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ReportService < BaseService class ReportService < BaseService
include Payloadable
def call(source_account, target_account, options = {}) def call(source_account, target_account, options = {})
@source_account = source_account @source_account = source_account
@target_account = target_account @target_account = target_account
@ -44,12 +46,7 @@ class ReportService < BaseService
end end
def payload def payload
Oj.dump(ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
@report,
serializer: ActivityPub::FlagSerializer,
adapter: ActivityPub::Adapter,
account: some_local_account
).as_json)
end end
def some_local_account def some_local_account

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class SuspendAccountService < BaseService class SuspendAccountService < BaseService
include Payloadable
ASSOCIATIONS_ON_SUSPEND = %w( ASSOCIATIONS_ON_SUSPEND = %w(
account_pins account_pins
active_relationships active_relationships
@ -118,23 +120,11 @@ class SuspendAccountService < BaseService
end end
def delete_actor_json def delete_actor_json
return @delete_actor_json if defined?(@delete_actor_json) @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
payload = ActiveModelSerializers::SerializableResource.new(
@account,
serializer: ActivityPub::DeleteActorSerializer,
adapter: ActivityPub::Adapter
).as_json
@delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
end end
def build_reject_json(follow) def build_reject_json(follow)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
follow,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def delivery_inboxes def delivery_inboxes

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class UnblockService < BaseService class UnblockService < BaseService
include Payloadable
def call(account, target_account) def call(account, target_account)
return unless account.blocking?(target_account) return unless account.blocking?(target_account)
@ -20,11 +22,7 @@ class UnblockService < BaseService
end end
def build_json(unblock) def build_json(unblock)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer))
unblock,
serializer: ActivityPub::UndoBlockSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def build_xml(block) def build_xml(block)

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class UnfavouriteService < BaseService class UnfavouriteService < BaseService
include Payloadable
def call(account, status) def call(account, status)
favourite = Favourite.find_by!(account: account, status: status) favourite = Favourite.find_by!(account: account, status: status)
favourite.destroy! favourite.destroy!
@ -21,11 +23,7 @@ class UnfavouriteService < BaseService
end end
def build_json(favourite) def build_json(favourite)
Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer))
favourite,
serializer: ActivityPub::UndoLikeSerializer,
adapter: ActivityPub::Adapter
).as_json).sign!(favourite.account))
end end
def build_xml(favourite) def build_xml(favourite)

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class UnfollowService < BaseService class UnfollowService < BaseService
include Payloadable
# Unfollow and notify the remote user # Unfollow and notify the remote user
# @param [Account] source_account Where to unfollow from # @param [Account] source_account Where to unfollow from
# @param [Account] target_account Which to unfollow # @param [Account] target_account Which to unfollow
@ -50,19 +52,11 @@ class UnfollowService < BaseService
end end
def build_json(follow) def build_json(follow)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
follow,
serializer: ActivityPub::UndoFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def build_reject_json(follow) def build_reject_json(follow)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
follow,
serializer: ActivityPub::RejectFollowSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
def build_xml(follow) def build_xml(follow)

View File

@ -2,6 +2,7 @@
class VoteService < BaseService class VoteService < BaseService
include Authorization include Authorization
include Payloadable
def call(account, poll, choices) def call(account, poll, choices)
authorize_with account, poll, :vote? authorize_with account, poll, :vote?
@ -50,10 +51,6 @@ class VoteService < BaseService
end end
def build_json(vote) def build_json(vote)
ActiveModelSerializers::SerializableResource.new( Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer))
vote,
serializer: ActivityPub::VoteSerializer,
adapter: ActivityPub::Adapter
).to_json
end end
end end

View File

@ -8,6 +8,8 @@
= "(@#{account.username})" = "(@#{account.username})"
%br/ %br/
= account.user_current_sign_in_ip = account.user_current_sign_in_ip
= t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at)
- if account.user&.invite_request&.text&.present? - if account.user&.invite_request&.text&.present?
.pending-account__body .pending-account__body

View File

@ -2,6 +2,7 @@
class ActivityPub::DistributePollUpdateWorker class ActivityPub::DistributePollUpdateWorker
include Sidekiq::Worker include Sidekiq::Worker
include Payloadable
sidekiq_options queue: 'push', unique: :until_executed, retry: 0 sidekiq_options queue: 'push', unique: :until_executed, retry: 0
@ -41,20 +42,8 @@ class ActivityPub::DistributePollUpdateWorker
@inboxes @inboxes
end end
def signed_payload
Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
end
def unsigned_payload
ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::UpdatePollSerializer,
adapter: ActivityPub::Adapter
).as_json
end
def payload def payload
@payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload) @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account))
end end
def relay! def relay!

View File

@ -2,6 +2,7 @@
class ActivityPub::DistributionWorker class ActivityPub::DistributionWorker
include Sidekiq::Worker include Sidekiq::Worker
include Payloadable
sidekiq_options queue: 'push' sidekiq_options queue: 'push'
@ -41,20 +42,8 @@ class ActivityPub::DistributionWorker
end end
end end
def signed_payload
Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@account))
end
def unsigned_payload
ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).as_json
end
def payload def payload
@payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload) @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @account))
end end
def relay! def relay!

View File

@ -5,6 +5,7 @@
class ActivityPub::ReplyDistributionWorker class ActivityPub::ReplyDistributionWorker
include Sidekiq::Worker include Sidekiq::Worker
include Payloadable
sidekiq_options queue: 'push' sidekiq_options queue: 'push'
@ -27,19 +28,7 @@ class ActivityPub::ReplyDistributionWorker
@inboxes ||= @account.followers.inboxes @inboxes ||= @account.followers.inboxes
end end
def signed_payload
Oj.dump(ActivityPub::LinkedDataSignature.new(unsigned_payload).sign!(@status.account))
end
def unsigned_payload
ActiveModelSerializers::SerializableResource.new(
@status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter
).as_json
end
def payload def payload
@payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload) @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
end end
end end

View File

@ -2,6 +2,7 @@
class ActivityPub::UpdateDistributionWorker class ActivityPub::UpdateDistributionWorker
include Sidekiq::Worker include Sidekiq::Worker
include Payloadable
sidekiq_options queue: 'push' sidekiq_options queue: 'push'
@ -27,14 +28,6 @@ class ActivityPub::UpdateDistributionWorker
end end
def signed_payload def signed_payload
@signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account, sign_with: @options[:sign_with])) @signed_payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account, sign_with: @options[:sign_with]))
end
def payload
@payload ||= ActiveModelSerializers::SerializableResource.new(
@account,
serializer: ActivityPub::UpdateSerializer,
adapter: ActivityPub::Adapter
).as_json
end end
end end

View File

@ -174,6 +174,7 @@ en:
statuses: Statuses statuses: Statuses
subscribe: Subscribe subscribe: Subscribe
suspended: Suspended suspended: Suspended
time_in_queue: Waiting in queue %{time}
title: Accounts title: Accounts
unconfirmed_email: Unconfirmed email unconfirmed_email: Unconfirmed email
undo_silenced: Undo silence undo_silenced: Undo silence

View File

@ -10,9 +10,10 @@
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack", "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
"manage:translations": "node ./config/webpack/translationRunner.js", "manage:translations": "node ./config/webpack/translationRunner.js",
"start": "node ./streaming/index.js", "start": "node ./streaming/index.js",
"test": "${npm_execpath} run test:lint && ${npm_execpath} run test:jest", "test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:jest",
"test:lint": "eslint --ext=js .", "test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
"test:lint:sass": "sass-lint .", "test:lint:js": "eslint --ext=js .",
"test:lint:sass": "sass-lint -v",
"test:jest": "cross-env NODE_ENV=test jest --coverage" "test:jest": "cross-env NODE_ENV=test jest --coverage"
}, },
"repository": { "repository": {

View File

@ -58,21 +58,6 @@ RSpec.describe ActivityPub::Activity::Announce do
end end
end end
context 'self-boost of a previously unknown status with missing attributedTo' do
let(:object_json) do
{
id: 'https://example.com/actor#bar',
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 context 'self-boost of a previously unknown status with correct attributedTo' do
let(:object_json) do let(:object_json) do
{ {
@ -122,6 +107,7 @@ RSpec.describe ActivityPub::Activity::Announce do
type: 'Note', type: 'Note',
content: 'Lorem ipsum', content: 'Lorem ipsum',
to: 'http://example.com/followers', to: 'http://example.com/followers',
attributedTo: 'https://example.com/actor',
} }
end end
@ -141,6 +127,7 @@ RSpec.describe ActivityPub::Activity::Announce do
type: 'Note', type: 'Note',
content: 'Lorem ipsum', content: 'Lorem ipsum',
to: 'http://example.com/followers', to: 'http://example.com/followers',
attributedTo: 'https://example.com/actor',
} }
end end
@ -161,6 +148,7 @@ RSpec.describe ActivityPub::Activity::Announce do
type: 'Note', type: 'Note',
content: 'Lorem ipsum', content: 'Lorem ipsum',
to: 'http://example.com/followers', to: 'http://example.com/followers',
attributedTo: 'https://example.com/actor',
} }
end end