diff options
68 files changed, 278 insertions, 542 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index f1095e022..8bd4c867f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,6 @@ +require: + - rubocop-rails + AllCops: TargetRubyVersion: 2.3 Exclude: @@ -82,6 +85,9 @@ Rails/Exit: - 'lib/mastodon/*' - 'lib/cli.rb' +Rails/HelperInstanceVariable: + Enabled: false + Style/ClassAndModuleChildren: Enabled: false diff --git a/.sass-lint.yml b/.sass-lint.yml index d2c1d059e..a84adff3f 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -4,261 +4,34 @@ files: include: app/javascript/styles/**/*.scss ignore: - - app/javascript/styles/reset.scss - -linters: - # Reports when you use improper spacing around ! (the "bang") in !default, - # !global, !important, and !optional flags. - BangFormat: - enabled: false - - # Whether or not to prefer `border: 0` over `border: none`. - BorderZero: - enabled: false - - # Reports when you define a rule set using a selector with chained classes - # (a.k.a. adjoining classes). - ChainedClasses: - enabled: false - - # Prefer hexadecimal color codes over color keywords. - # (e.g. `color: green` is a color keyword) - ColorKeyword: - enabled: false - - # Prefer color literals (keywords or hexadecimal codes) to be used only in - # variable declarations. They should be referred to via variables everywhere - # 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 + - app/javascript/styles/mastodon/reset.scss + +rules: + # Disallows + no-color-literals: 0 + no-css-comments: 0 + 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 + + # Nesting + force-element-nesting: 0 + force-attribute-nesting: 0 + force-pseudo-nesting: 0 + + # Name Formats + class-name-format: 0 + leading-zero: 0 + + # Style Guide + attribute-quotes: 0 + hex-length: 0 + indentation: 0 + nesting-depth: 0 + property-sort-order: 0 + quotes: 0 diff --git a/Gemfile b/Gemfile index 68fd5e98f..65f6fa3cb 100644 --- a/Gemfile +++ b/Gemfile @@ -132,6 +132,7 @@ group :development do gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' gem 'rubocop', '~> 0.71', require: false + gem 'rubocop-rails', '~> 2.0', require: false gem 'brakeman', '~> 4.5', require: false gem 'bundler-audit', '~> 0.6', require: false diff --git a/Gemfile.lock b/Gemfile.lock index be8ce27cd..73f0d63f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -534,6 +534,9 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 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-saml (1.9.0) nokogiri (>= 1.5.10) @@ -740,6 +743,7 @@ DEPENDENCIES rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) rubocop (~> 0.71) + rubocop-rails (~> 2.0) sanitize (~> 5.0) sidekiq (~> 5.2) sidekiq-bulk (~> 0.2.0) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index fcdebb47f..051b6ecbd 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -47,8 +47,6 @@ class AccountsController < ApplicationController end format.json do - mark_cacheable! - render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 853f4f907..012c3c538 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -9,8 +9,6 @@ class ActivityPub::CollectionsController < Api::BaseController before_action :set_cache_headers def show - skip_session! - render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do ActiveModelSerializers::SerializableResource.new( collection_presenter, diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 438fa226e..5147afbf7 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -10,10 +10,7 @@ class ActivityPub::OutboxesController < Api::BaseController before_action :set_cache_headers def show - unless page_requested? - skip_session! - expires_in 1.minute, public: true - end + expires_in 1.minute, public: true unless page_requested? render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index e7795e95c..b0d45ce47 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -48,13 +48,13 @@ module Admin def approve authorize @account.user, :approve? @account.user.approve! - redirect_to admin_accounts_path(pending: '1') + redirect_to admin_pending_accounts_path end def reject authorize @account.user, :reject? 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 def unsilence diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5401b9d59..333082f68 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -228,11 +228,6 @@ class ApplicationController < ActionController::Base end def mark_cacheable! - skip_session! expires_in 0, public: true end - - def skip_session! - request.session_options[:skip] = true - end end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 4f28941ae..1c422096c 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -70,7 +70,6 @@ module AccountControllerConcern def check_account_suspension if @account.suspended? - skip_session! expires_in(3.minutes, public: true) gone end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 31e501609..be768c089 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -4,7 +4,6 @@ class CustomCssController < ApplicationController before_action :set_cache_headers def show - skip_session! render plain: Setting.custom_css || '', content_type: 'text/css' end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 5d306e600..3feb08132 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -7,8 +7,6 @@ class EmojisController < ApplicationController def show respond_to do |format| format.json do - skip_session! - render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 1462b94fc..fab9c8462 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -20,10 +20,7 @@ class FollowerAccountsController < ApplicationController format.json do raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? - if params[:page].blank? - skip_session! - expires_in 3.minutes, public: true - end + expires_in 3.minutes, public: true if params[:page].blank? render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 181f85221..272116040 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -20,10 +20,7 @@ class FollowingAccountsController < ApplicationController format.json do raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? - if params[:page].blank? - skip_session! - expires_in 3.minutes, public: true - end + expires_in 3.minutes, public: true if params[:page].blank? render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 28eebda28..66ba260aa 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -29,10 +29,7 @@ class StatusesController < ApplicationController format.html do use_pack 'public' - unless user_signed_in? - skip_session! - expires_in 10.seconds, public: true - end + expires_in 10.seconds, public: true if current_account.nil? @body_classes = 'with-modals' @@ -43,8 +40,6 @@ class StatusesController < ApplicationController end 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 ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) end @@ -53,8 +48,6 @@ class StatusesController < ApplicationController end def activity - skip_session! - 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) end @@ -64,7 +57,6 @@ class StatusesController < ApplicationController use_pack 'embed' raise ActiveRecord::RecordNotFound if @status.hidden? - skip_session! expires_in 180, public: true response.headers['X-Frame-Options'] = 'ALLOWALL' @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) @@ -73,8 +65,6 @@ class StatusesController < ApplicationController end def replies - skip_session! - render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 1e16c5157..1ee85592c 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -17,19 +17,13 @@ class StreamEntriesController < ApplicationController format.html do use_pack 'public' - unless user_signed_in? - skip_session! - expires_in 5.minutes, public: true - end + expires_in 5.minutes, public: true unless @stream_entry.hidden? - 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 format.atom do - unless @stream_entry.hidden? - skip_session! - expires_in 3.minutes, public: true - end + expires_in 3.minutes, public: true unless @stream_entry.hidden? render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) end @@ -57,7 +51,7 @@ class StreamEntriesController < ApplicationController def set_stream_entry @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? authorize @stream_entry.activity, :show? if @stream_entry.hidden? || @stream_entry.local_only? diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js index e1ded2b3a..cf3907fbf 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -138,8 +138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.setState({ suggestionsHidden: true, focused: false }); } - onFocus = () => { + onFocus = (e) => { this.setState({ focused: true }); + if (this.props.onFocus) { + this.props.onFocus(e); + } } onSuggestionClick = (e) => { diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index cc82a50d4..0120be28f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -28,6 +28,10 @@ const messages = defineMessages({ export default @injectIntl class ComposeForm extends ImmutablePureComponent { + setRef = c => { + this.composeForm = c; + }; + static contextTypes = { router: PropTypes.object, }; @@ -208,6 +212,10 @@ class ComposeForm extends ImmutablePureComponent { } } + handleFocus = () => { + this.composeForm.scrollIntoView(); + } + // This statement does several things: // - If we're beginning a reply, and, // - 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); return ( - <div className='composer'> + <div className='composer' ref={this.setRef}> <WarningContainer /> <ReplyIndicatorContainer /> @@ -337,6 +345,7 @@ class ComposeForm extends ImmutablePureComponent { value={this.props.text} onChange={this.handleChange} suggestions={this.props.suggestions} + onFocus={this.handleFocus} onKeyDown={this.handleKeyDown} onSuggestionsFetchRequested={onFetchSuggestions} onSuggestionsClearRequested={onClearSuggestions} diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js index ef829b937..0405073c5 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.js +++ b/app/javascript/flavours/glitch/features/list_timeline/index.js @@ -75,6 +75,23 @@ export default class ListTimeline extends React.PureComponent { 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 () { if (this.disconnect) { this.disconnect(); diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 51a341c42..36dfb8f15 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -442,6 +442,7 @@ export default function compose(state = initialState, action) { map.set('focusDate', new Date()); map.set('caretPosition', null); map.set('idempotencyKey', uuid()); + map.set('sensitive', action.status.get('sensitive')); if (action.status.get('spoiler_text').length > 0) { map.set('spoiler', true); diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index cb233de1c..440b370e6 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -35,7 +35,9 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is 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 => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index 5cad43e82..4c50294ba 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -138,8 +138,11 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.setState({ suggestionsHidden: true, focused: false }); } - onFocus = () => { + onFocus = (e) => { this.setState({ focused: true }); + if (this.props.onFocus) { + this.props.onFocus(e); + } } onSuggestionClick = (e) => { diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index ff6c521f2..ff22a2953 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -34,6 +34,10 @@ const messages = defineMessages({ export default @injectIntl class ComposeForm extends ImmutablePureComponent { + setRef = c => { + this.composeForm = c; + }; + static contextTypes = { router: PropTypes.object, }; @@ -115,6 +119,10 @@ class ComposeForm extends ImmutablePureComponent { this.props.onChangeSpoilerText(e.target.value); } + handleFocus = () => { + this.composeForm.scrollIntoView(); + } + componentDidUpdate (prevProps) { // This statement does several things: // - If we're beginning a reply, and, @@ -178,7 +186,7 @@ class ComposeForm extends ImmutablePureComponent { } return ( - <div className='compose-form'> + <div className='compose-form' ref={this.setRef}> <WarningContainer /> <ReplyIndicatorContainer /> @@ -201,7 +209,7 @@ class ComposeForm extends ImmutablePureComponent { /> </div> - <div className='emoji-picker-wrapper'> + <div className={`emoji-picker-wrapper ${this.props.showSearch ? 'emoji-picker-wrapper--hidden' : ''}`}> <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> </div> @@ -212,6 +220,7 @@ class ComposeForm extends ImmutablePureComponent { value={this.props.text} onChange={this.handleChange} suggestions={this.props.suggestions} + onFocus={this.handleFocus} onKeyDown={this.handleKeyDown} onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested} diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 6833c43ef..7f9edfeee 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -21,7 +21,7 @@ class SearchPopout extends React.PureComponent { 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' />; 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 }) }}> {({ opacity, scaleX, scaleY }) => ( <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index b6722e91a..ad7d16f95 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -75,6 +75,23 @@ class ListTimeline extends React.PureComponent { 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 () { if (this.disconnect) { this.disconnect(); diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 708272591..29c691144 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -338,6 +338,7 @@ export default function compose(state = initialState, action) { map.set('focusDate', new Date()); map.set('caretPosition', null); map.set('idempotencyKey', uuid()); + map.set('sensitive', action.status.get('sensitive')); if (action.status.get('spoiler_text').length > 0) { map.set('spoiler', true); diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 6a972f967..309a95a19 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -35,14 +35,12 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is 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 => { 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 firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index f78e60597..5a40e7d79 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -5,7 +5,7 @@ &-description { input { &::placeholder { - opacity: 1.0; + opacity: 1; } } } diff --git a/app/javascript/styles/contrast/variables.scss b/app/javascript/styles/contrast/variables.scss index f6cadf029..cfe3b21db 100644 --- a/app/javascript/styles/contrast/variables.scss +++ b/app/javascript/styles/contrast/variables.scss @@ -20,5 +20,5 @@ $highlight-text-color: $classic-highlight-color !default; $action-button-color: #8d9ac2; $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; diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss index 74d1df8ed..b4fb1d709 100644 --- a/app/javascript/styles/mailer.scss +++ b/app/javascript/styles/mailer.scss @@ -279,6 +279,8 @@ h5 { } .hero-with-button { + padding-bottom: 16px; + h1 { margin-bottom: 4px; } @@ -286,8 +288,6 @@ h5 { p.lead { margin-bottom: 32px; } - - padding-bottom: 16px; } .header { diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss index 08806599e..faaffb30f 100644 --- a/app/javascript/styles/mastodon/_mixins.scss +++ b/app/javascript/styles/mastodon/_mixins.scss @@ -1,21 +1,21 @@ -@mixin avatar-radius() { +@mixin avatar-radius { border-radius: 4px; background: transparent no-repeat; background-position: 50%; background-clip: padding-box; } -@mixin avatar-size($size:48px) { +@mixin avatar-size($size: 48px) { width: $size; height: $size; background-size: $size $size; } -@mixin search-input() { +@mixin search-input { outline: 0; box-sizing: border-box; width: 100%; - border: none; + border: 0; box-shadow: none; font-family: inherit; background: $ui-base-color; @@ -42,7 +42,7 @@ } } -@mixin search-popout() { +@mixin search-popout { background: $simple-background-color; border-radius: 4px; padding: 10px 14px; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 74f91599a..692d86852 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -171,7 +171,7 @@ $content-width: 840px; text-transform: none; padding-bottom: 0; margin-bottom: 0; - border-bottom: none; + border-bottom: 0; } & > p { diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index 4411ca0b4..b5a77ce94 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -2,7 +2,8 @@ @if type-of($color) == 'color' { $color: str-slice(ie-hex-str($color), 4); } - @return '%23' + unquote($color) + + @return '%23' + unquote($color); } body { @@ -15,7 +16,7 @@ body { text-rendering: optimizelegibility; font-feature-settings: "kern"; 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; &.system-font { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index cd5ff35a4..63c38ff42 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -128,7 +128,7 @@ display: inline-block; padding: 0; color: $action-button-color; - border: none; + border: 0; background: transparent; cursor: pointer; transition: color 100ms ease-in; @@ -196,7 +196,7 @@ .text-icon-button { color: $lighter-text-color; - border: none; + border: 0; background: transparent; cursor: pointer; font-weight: 600; @@ -353,12 +353,12 @@ .spoiler-input { height: 0; transform-origin: bottom; - opacity: 0.0; + opacity: 0; &.spoiler-input--visible { height: 36px; margin-bottom: 11px; - opacity: 1.0; + opacity: 1; } } @@ -408,12 +408,20 @@ } } - .emoji-picker-wrapper, .autosuggest-textarea__suggestions-wrapper { position: relative; height: 0; } + .emoji-picker-wrapper { + position: relative; + height: 0; + + &.emoji-picker-wrapper--hidden { + display: none; + } + } + .autosuggest-textarea__suggestions { box-sizing: border-box; display: none; @@ -1185,7 +1193,7 @@ } .account__avatar { - @include avatar-radius(); + @include avatar-radius; position: relative; &-inline { @@ -1195,11 +1203,11 @@ } &-composite { - @include avatar-radius(); + @include avatar-radius; overflow: hidden; & > div { - @include avatar-radius(); + @include avatar-radius; float: left; position: relative; box-sizing: border-box; @@ -1215,12 +1223,12 @@ a .account__avatar { @include avatar-size(48px); &-base { - @include avatar-radius(); + @include avatar-radius; @include avatar-size(36px); } &-overlay { - @include avatar-radius(); + @include avatar-radius; @include avatar-size(24px); position: absolute; @@ -1598,13 +1606,13 @@ a.account__display-name { .icon-button.close { position: absolute; pointer-events: none; - transform: scale(0.0, 1.0) translate(-100%, 0); + transform: scale(0, 1) translate(-100%, 0); opacity: 0; } .compose__action-bar .icon-button { pointer-events: auto; - transform: scale(1.0, 1.0) translate(0, 0); + transform: scale(1, 1) translate(0, 0); opacity: 1; } } @@ -2071,6 +2079,10 @@ a.account__display-name { .account { padding: 15px 10px; + + &__header__bio { + margin: 0 -10px; + } } .notification { @@ -2699,7 +2711,7 @@ a.account__display-name { .setting-text { color: $darker-text-color; background: transparent; - border: none; + border: 0; border-bottom: 2px solid $ui-primary-color; box-sizing: border-box; display: block; @@ -3037,7 +3049,7 @@ a.status-card.compact:hover { & > button { margin: 0; - border: none; + border: 0; padding: 15px 0 15px 15px; color: inherit; background: transparent; @@ -3202,11 +3214,11 @@ a.status-card.compact:hover { } .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 { - 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 { @@ -3373,7 +3385,7 @@ a.status-card.compact:hover { .column-select { &__control { - @include search-input(); + @include search-input; } &__placeholder { @@ -3424,7 +3436,7 @@ a.status-card.compact:hover { } &__menu { - @include search-popout(); + @include search-popout; padding: 0; background: $ui-secondary-color; } @@ -3585,7 +3597,7 @@ a.status-card.compact:hover { .no-reduce-motion .shake-bottom { 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 { @@ -3880,10 +3892,11 @@ a.status-card.compact:hover { } .search__input { + @include search-input; + display: block; padding: 10px; padding-right: 30px; - @include search-input(); } .search__icon { @@ -4491,14 +4504,14 @@ a.status-card.compact:hover { } .actions-modal { + max-height: 80vh; + max-width: 80vw; + .status { overflow-y: auto; max-height: 300px; } - max-height: 80vh; - max-width: 80vw; - .actions-modal__item-label { font-weight: 500; } @@ -4713,7 +4726,7 @@ a.status-card.compact:hover { } .media-gallery__item { - border: none; + border: 0; box-sizing: border-box; display: block; float: left; @@ -5173,7 +5186,7 @@ a.status-card.compact:hover { } .account-gallery__item { - border: none; + border: 0; box-sizing: border-box; display: block; position: relative; @@ -5247,7 +5260,7 @@ a.status-card.compact:hover { } .search-popout { - @include search-popout(); + @include search-popout; } noscript { @@ -5349,14 +5362,14 @@ noscript { .icon-button.close { pointer-events: auto; opacity: 1; - transform: scale(1.0, 1.0) translate(0, 0); + transform: scale(1, 1) translate(0, 0); bottom: 5px; } .compose__action-bar .icon-button { pointer-events: none; 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; display: block; width: 100%; - border: none; + border: 0; padding: 10px; font-family: $font-monospace, monospace; background: $ui-base-color; diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 0eae4939f..2d68d2b70 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -121,7 +121,7 @@ grid-auto-rows: max-content; .column-0 { - grid-column: 1/3; + grid-column: 1 / 3; grid-row: 1; } @@ -136,7 +136,7 @@ } .column-3 { - grid-column: 1/3; + grid-column: 1 / 3; grid-row: 3; } diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss index e49084b5f..4bfd66504 100644 --- a/app/javascript/styles/mastodon/emoji_picker.scss +++ b/app/javascript/styles/mastodon/emoji_picker.scss @@ -1,14 +1,14 @@ .emoji-mart { + font-size: 13px; + display: inline-block; + color: $inverted-text-color; + &, * { box-sizing: border-box; line-height: 1.15; } - font-size: 13px; - display: inline-block; - color: $inverted-text-color; - .emoji-mart-emoji { padding: 6px; } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 2b8d7a682..f3de87791 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -553,7 +553,7 @@ code { box-sizing: border-box; display: block; width: 100%; - border: none; + border: 0; padding: 10px; font-family: $font-monospace, monospace; background: $ui-base-color; diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index 0d55afda4..12f57b7a9 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -47,7 +47,6 @@ width: 100%; font-size: 14px; color: $inverted-text-color; - display: block; outline: 0; font-family: inherit; background: $simple-background-color; diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index 940dc8af2..a59f59f59 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -180,7 +180,6 @@ body.rtl { } .fa-ul { - margin-left: 0; margin-left: 2.14285714em; } diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 54b175613..66b5763a9 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -143,7 +143,7 @@ class ActivityPub::Activity # 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 + actor_id = value_or_id(first_of_value(@object['attributedTo'])) if actor_id == @account.uri return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 1e805c0d1..3eb88339a 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::Activity::Follow < ActivityPub::Activity + include Payloadable + def perform target_account = account_from_uri(object_uri) @@ -28,7 +30,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity end 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) end end diff --git a/app/models/account.rb b/app/models/account.rb index 70697db30..520b183e8 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -208,6 +208,10 @@ class Account < ApplicationRecord end end + def sign? + true + end + def keypair @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 5bc44e809..f1b7a4566 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -3,6 +3,7 @@ class Form::AccountBatch include ActiveModel::Model include Authorization + include Payloadable attr_accessor :account_ids, :action, :current_account @@ -54,13 +55,7 @@ class Form::AccountBatch return unless follow.account.activitypub? - json = ActiveModelSerializers::SerializableResource.new( - follow, - serializer: ActivityPub::RejectFollowSerializer, - adapter: ActivityPub::Adapter - ).to_json - - ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), current_account.id, follow.account.inbox_url) end def approve! diff --git a/app/models/status.rb b/app/models/status.rb index 6f3ba4cc3..5ddce72de 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -211,6 +211,8 @@ class Status < ApplicationRecord public_visibility? || unlisted_visibility? end + alias sign? distributable? + def with_media? media_attachments.any? end @@ -529,7 +531,7 @@ class Status < ApplicationRecord return if direct_visibility? 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?) end @@ -537,7 +539,7 @@ class Status < ApplicationRecord return if direct_visibility? || marked_for_mass_destruction? 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?) end diff --git a/app/services/after_block_domain_from_account_service.rb b/app/services/after_block_domain_from_account_service.rb index 180f13403..a87c2e792 100644 --- a/app/services/after_block_domain_from_account_service.rb +++ b/app/services/after_block_domain_from_account_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AfterBlockDomainFromAccountService < BaseService + include Payloadable + # This service does not create an AccountDomainBlock record, # it's meant to be called after such a record has been created # synchronously, to "clean up" @@ -31,12 +33,6 @@ class AfterBlockDomainFromAccountService < BaseService return unless follow.account.activitypub? - json = ActiveModelSerializers::SerializableResource.new( - follow, - serializer: ActivityPub::RejectFollowSerializer, - adapter: ActivityPub::Adapter - ).to_json - - ActivityPub::DeliveryWorker.perform_async(json, @account.id, follow.account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url) end end diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index f2e3ebe7d..29b8700c7 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AuthorizeFollowService < BaseService + include Payloadable + def call(source_account, target_account, **options) if options[:skip_follow_request] follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri]) @@ -24,11 +26,7 @@ class AuthorizeFollowService < BaseService end def build_json(follow_request) - ActiveModelSerializers::SerializableResource.new( - follow_request, - serializer: ActivityPub::AcceptFollowSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer)) end def build_xml(follow_request) diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 10ed470e0..9050a4858 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class BlockService < BaseService + include Payloadable + def call(account, target_account) return if account.id == target_account.id @@ -26,11 +28,7 @@ class BlockService < BaseService end def build_json(block) - ActiveModelSerializers::SerializableResource.new( - block, - serializer: ActivityPub::BlockSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer)) end def build_xml(block) diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb new file mode 100644 index 000000000..13d9c3548 --- /dev/null +++ b/app/services/concerns/payloadable.rb @@ -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 diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index b565bcc32..128a24ad6 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -2,6 +2,7 @@ class FavouriteService < BaseService include Authorization + include Payloadable # Favourite a status and notify remote user # @param [Account] account @@ -43,11 +44,7 @@ class FavouriteService < BaseService end def build_json(favourite) - Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( - favourite, - serializer: ActivityPub::LikeSerializer, - adapter: ActivityPub::Adapter - ).as_json).sign!(favourite.account)) + Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer)) end def build_xml(favourite) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 92d8c864a..0305e2d62 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -2,6 +2,7 @@ class FollowService < BaseService include Redisable + include Payloadable # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow @@ -78,10 +79,6 @@ class FollowService < BaseService end def build_json(follow_request) - ActiveModelSerializers::SerializableResource.new( - follow_request, - serializer: ActivityPub::FollowSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer)) end end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 989cc19a6..1804e0c93 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -2,6 +2,7 @@ class ProcessMentionsService < BaseService include StreamEntryRenderer + include Payloadable # Scan status for mentions and fetch remote mentioned users, create # local mention pointers, send Salmon notifications to mentioned @@ -61,12 +62,7 @@ class ProcessMentionsService < BaseService def activitypub_json return @activitypub_json if defined?(@activitypub_json) - payload = ActiveModelSerializers::SerializableResource.new( - @status, - serializer: ActivityPub::ActivitySerializer, - adapter: ActivityPub::Adapter - ).as_json - @activitypub_json = Oj.dump(@status.distributable? ? ActivityPub::LinkedDataSignature.new(payload).sign!(@status.account) : payload) + @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account)) end def resolve_account_service diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 77ec52ab8..09403bae0 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -3,6 +3,7 @@ class ReblogService < BaseService include Authorization include StreamEntryRenderer + include Payloadable # Reblog a status and notify its remote author # @param [Account] account Account to reblog from @@ -56,10 +57,6 @@ class ReblogService < BaseService end def build_json(reblog) - Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( - reblog, - serializer: ActivityPub::ActivitySerializer, - adapter: ActivityPub::Adapter - ).as_json).sign!(reblog.account)) + Oj.dump(serialize_payload(reblog, ActivityPub::ActivitySerializer, signer: reblog.account)) end end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index a91266aa4..f87d0ba91 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class RejectFollowService < BaseService + include Payloadable + def call(source_account, target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request.reject! @@ -19,11 +21,7 @@ class RejectFollowService < BaseService end def build_json(follow_request) - ActiveModelSerializers::SerializableResource.new( - follow_request, - serializer: ActivityPub::RejectFollowSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer)) end def build_xml(follow_request) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 6e4998e07..98972fc70 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -3,6 +3,7 @@ class RemoveStatusService < BaseService include StreamEntryRenderer include Redisable + include Payloadable def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @@ -116,15 +117,7 @@ class RemoveStatusService < BaseService end def signed_activity_json - @signed_activity_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(activity_json).sign!(@account)) - end - - def activity_json - @activity_json ||= ActiveModelSerializers::SerializableResource.new( - @status, - serializer: @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, - adapter: ActivityPub::Adapter - ).as_json + @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account)) end def remove_reblogs diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 73bd6694f..1e955c1e7 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ReportService < BaseService + include Payloadable + def call(source_account, target_account, options = {}) @source_account = source_account @target_account = target_account @@ -44,12 +46,7 @@ class ReportService < BaseService end def payload - Oj.dump(ActiveModelSerializers::SerializableResource.new( - @report, - serializer: ActivityPub::FlagSerializer, - adapter: ActivityPub::Adapter, - account: some_local_account - ).as_json) + Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account)) end def some_local_account diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 412873f84..a5ce3dbd9 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class SuspendAccountService < BaseService + include Payloadable + ASSOCIATIONS_ON_SUSPEND = %w( account_pins active_relationships @@ -118,23 +120,11 @@ class SuspendAccountService < BaseService end def delete_actor_json - return @delete_actor_json if defined?(@delete_actor_json) - - payload = ActiveModelSerializers::SerializableResource.new( - @account, - serializer: ActivityPub::DeleteActorSerializer, - adapter: ActivityPub::Adapter - ).as_json - - @delete_actor_json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account)) + @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account)) end def build_reject_json(follow) - ActiveModelSerializers::SerializableResource.new( - follow, - serializer: ActivityPub::RejectFollowSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) end def delivery_inboxes diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index 72fc5ab15..95a858e9f 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class UnblockService < BaseService + include Payloadable + def call(account, target_account) return unless account.blocking?(target_account) @@ -20,11 +22,7 @@ class UnblockService < BaseService end def build_json(unblock) - ActiveModelSerializers::SerializableResource.new( - unblock, - serializer: ActivityPub::UndoBlockSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer)) end def build_xml(block) diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index 2fda11bd6..dcc890b7d 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class UnfavouriteService < BaseService + include Payloadable + def call(account, status) favourite = Favourite.find_by!(account: account, status: status) favourite.destroy! @@ -21,11 +23,7 @@ class UnfavouriteService < BaseService end def build_json(favourite) - Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new( - favourite, - serializer: ActivityPub::UndoLikeSerializer, - adapter: ActivityPub::Adapter - ).as_json).sign!(favourite.account)) + Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer)) end def build_xml(favourite) diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 95da2a667..17dc29735 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class UnfollowService < BaseService + include Payloadable + # Unfollow and notify the remote user # @param [Account] source_account Where to unfollow from # @param [Account] target_account Which to unfollow @@ -50,19 +52,11 @@ class UnfollowService < BaseService end def build_json(follow) - ActiveModelSerializers::SerializableResource.new( - follow, - serializer: ActivityPub::UndoFollowSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) end def build_reject_json(follow) - ActiveModelSerializers::SerializableResource.new( - follow, - serializer: ActivityPub::RejectFollowSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) end def build_xml(follow) diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb index 81af9ef3a..0eeb8fd56 100644 --- a/app/services/vote_service.rb +++ b/app/services/vote_service.rb @@ -2,6 +2,7 @@ class VoteService < BaseService include Authorization + include Payloadable def call(account, poll, choices) authorize_with account, poll, :vote? @@ -50,10 +51,6 @@ class VoteService < BaseService end def build_json(vote) - ActiveModelSerializers::SerializableResource.new( - vote, - serializer: ActivityPub::VoteSerializer, - adapter: ActivityPub::Adapter - ).to_json + Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer)) end end diff --git a/app/views/admin/pending_accounts/_account.html.haml b/app/views/admin/pending_accounts/_account.html.haml index 1ed5dafdd..7a9796a67 100644 --- a/app/views/admin/pending_accounts/_account.html.haml +++ b/app/views/admin/pending_accounts/_account.html.haml @@ -8,6 +8,8 @@ = "(@#{account.username})" %br/ = 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? .pending-account__body diff --git a/app/workers/activitypub/distribute_poll_update_worker.rb b/app/workers/activitypub/distribute_poll_update_worker.rb index 98b227111..37a13db2b 100644 --- a/app/workers/activitypub/distribute_poll_update_worker.rb +++ b/app/workers/activitypub/distribute_poll_update_worker.rb @@ -2,6 +2,7 @@ class ActivityPub::DistributePollUpdateWorker include Sidekiq::Worker + include Payloadable sidekiq_options queue: 'push', unique: :until_executed, retry: 0 @@ -41,20 +42,8 @@ class ActivityPub::DistributePollUpdateWorker @inboxes 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 - @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload) + @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account)) end def relay! diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index d83f01700..11b6a6111 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -2,6 +2,7 @@ class ActivityPub::DistributionWorker include Sidekiq::Worker + include Payloadable sidekiq_options queue: 'push' @@ -41,20 +42,8 @@ class ActivityPub::DistributionWorker 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 - @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload) + @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @account)) end def relay! diff --git a/app/workers/activitypub/reply_distribution_worker.rb b/app/workers/activitypub/reply_distribution_worker.rb index d8fea6c4e..1ff8a657e 100644 --- a/app/workers/activitypub/reply_distribution_worker.rb +++ b/app/workers/activitypub/reply_distribution_worker.rb @@ -5,6 +5,7 @@ class ActivityPub::ReplyDistributionWorker include Sidekiq::Worker + include Payloadable sidekiq_options queue: 'push' @@ -27,19 +28,7 @@ class ActivityPub::ReplyDistributionWorker @inboxes ||= @account.followers.inboxes 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 - @payload ||= @status.distributable? ? signed_payload : Oj.dump(unsigned_payload) + @payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account)) end end diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb index b9e5ff064..3a207f071 100644 --- a/app/workers/activitypub/update_distribution_worker.rb +++ b/app/workers/activitypub/update_distribution_worker.rb @@ -2,6 +2,7 @@ class ActivityPub::UpdateDistributionWorker include Sidekiq::Worker + include Payloadable sidekiq_options queue: 'push' @@ -27,14 +28,6 @@ class ActivityPub::UpdateDistributionWorker end def signed_payload - @signed_payload ||= Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account, sign_with: @options[:sign_with])) - end - - def payload - @payload ||= ActiveModelSerializers::SerializableResource.new( - @account, - serializer: ActivityPub::UpdateSerializer, - adapter: ActivityPub::Adapter - ).as_json + @signed_payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account, sign_with: @options[:sign_with])) end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 2a5751060..e70dd1c6b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -174,6 +174,7 @@ en: statuses: Statuses subscribe: Subscribe suspended: Suspended + time_in_queue: Waiting in queue %{time} title: Accounts unconfirmed_email: Unconfirmed email undo_silenced: Undo silence diff --git a/package.json b/package.json index 00ba6498a..078ad82f7 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,10 @@ "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack", "manage:translations": "node ./config/webpack/translationRunner.js", "start": "node ./streaming/index.js", - "test": "${npm_execpath} run test:lint && ${npm_execpath} run test:jest", - "test:lint": "eslint --ext=js .", - "test:lint:sass": "sass-lint .", + "test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:jest", + "test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass", + "test:lint:js": "eslint --ext=js .", + "test:lint:sass": "sass-lint -v", "test:jest": "cross-env NODE_ENV=test jest --coverage" }, "repository": { diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index 926083a4f..60fd96a18 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -58,21 +58,6 @@ RSpec.describe ActivityPub::Activity::Announce do 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 let(:object_json) do { @@ -122,6 +107,7 @@ RSpec.describe ActivityPub::Activity::Announce do type: 'Note', content: 'Lorem ipsum', to: 'http://example.com/followers', + attributedTo: 'https://example.com/actor', } end @@ -141,6 +127,7 @@ RSpec.describe ActivityPub::Activity::Announce do type: 'Note', content: 'Lorem ipsum', to: 'http://example.com/followers', + attributedTo: 'https://example.com/actor', } end @@ -161,6 +148,7 @@ RSpec.describe ActivityPub::Activity::Announce do type: 'Note', content: 'Lorem ipsum', to: 'http://example.com/followers', + attributedTo: 'https://example.com/actor', } end |