diff options
Diffstat (limited to 'app')
32 files changed, 134 insertions, 63 deletions
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 7c44e88b7..519405726 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -49,7 +49,7 @@ module Admin private def set_instance - @instance = Instance.find(params[:id]) + @instance = Instance.find(TagManager.instance.normalize_domain(params[:id]&.strip)) end def set_instances diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index b6050c913..23c008803 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -4,8 +4,8 @@ module WebAppControllerConcern extend ActiveSupport::Concern included do + prepend_before_action :redirect_unauthenticated_to_permalinks! before_action :set_pack - before_action :redirect_unauthenticated_to_permalinks! before_action :set_app_body_class before_action :set_referrer_policy_header end @@ -19,7 +19,7 @@ module WebAppControllerConcern end def redirect_unauthenticated_to_permalinks! - return if user_signed_in? + return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in redirect_path = PermalinkRedirector.new(request.path).redirect_path diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index 3175ff560..ac1b2f95f 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -194,7 +194,7 @@ ready(() => { } document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => { - const domain = document.getElementById('by_domain')?.value; + const domain = document.querySelector('input[type="text"]#by_domain')?.value; if (domain) { const url = new URL(event.target.href); diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 49858f2e2..b7daf82a4 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -27,6 +27,7 @@ export default class IconButton extends React.PureComponent { counter: PropTypes.number, obfuscateCount: PropTypes.bool, href: PropTypes.string, + ariaHidden: PropTypes.bool, }; static defaultProps = { @@ -36,6 +37,7 @@ export default class IconButton extends React.PureComponent { animate: false, overlay: false, tabIndex: '0', + ariaHidden: false, }; state = { @@ -102,6 +104,7 @@ export default class IconButton extends React.PureComponent { counter, obfuscateCount, href, + ariaHidden, } = this.props; const { @@ -142,6 +145,7 @@ export default class IconButton extends React.PureComponent { type='button' aria-label={title} aria-expanded={expanded} + aria-hidden={ariaHidden} title={title} className={classes} onClick={this.handleClick} diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index bf7982cea..e4a8be338 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -345,7 +345,7 @@ class MediaGallery extends React.PureComponent { </button> ); } else if (visible) { - spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} />; + spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} ariaHidden />; } else { spoilerButton = ( <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 40c86afdf..00fc94358 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -8,7 +8,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../initial_state'; import classNames from 'classnames'; -import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -37,9 +37,10 @@ const messages = defineMessages({ unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, - admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, - copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, - hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, + admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' }, + admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, + copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, + hide: { id: 'status.hide', defaultMessage: 'Hide post' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, @@ -232,7 +233,7 @@ class StatusActionBar extends ImmutablePureComponent { render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; - const { signedIn } = this.context.identity; + const { signedIn, permissions } = this.context.identity; const anonymousAccess = !signedIn; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -312,10 +313,16 @@ class StatusActionBar extends ImmutablePureComponent { } } - if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); - menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); + menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + } + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + const domain = account.get('acct').split('@')[1]; + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); + } } } diff --git a/app/javascript/mastodon/extra_polyfills.js b/app/javascript/mastodon/extra_polyfills.js index 395f1ed05..6e8004f07 100644 --- a/app/javascript/mastodon/extra_polyfills.js +++ b/app/javascript/mastodon/extra_polyfills.js @@ -1,6 +1,3 @@ import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'; import 'intersection-observer'; import 'requestidlecallback'; -import objectFitImages from 'object-fit-images'; - -objectFitImages(); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index dddbf4dd4..2481e4783 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -15,7 +15,7 @@ import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container'; -import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { Helmet } from 'react-helmet'; const messages = defineMessages({ @@ -53,6 +53,7 @@ const messages = defineMessages({ unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, + admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, }); @@ -163,7 +164,7 @@ class Header extends ImmutablePureComponent { render () { const { account, hidden, intl, domain } = this.props; - const { signedIn } = this.context.identity; + const { signedIn, permissions } = this.context.identity; if (!account) { return null; @@ -288,9 +289,14 @@ class Header extends ImmutablePureComponent { } } - if (account.get('id') !== me && (this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + if ((account.get('id') !== me && (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); + } + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` }); + } } const content = { __html: account.get('note_emojified') }; diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index abd3ba2f7..ebdd55d33 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -226,7 +226,7 @@ class ComposeForm extends ImmutablePureComponent { <ReplyIndicatorContainer /> - <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}> + <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}> <AutosuggestInput placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index c1242754c..46ee9f6c1 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -7,7 +7,7 @@ import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import { me } from '../../../initial_state'; import classNames from 'classnames'; -import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -34,6 +34,7 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, @@ -243,10 +244,16 @@ class ActionBar extends React.PureComponent { } } - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); - menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); + menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + } + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + const domain = account.get('acct').split('@')[1]; + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); + } } } diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js index cc5bcd18f..f5a897f75 100644 --- a/app/javascript/mastodon/load_polyfills.js +++ b/app/javascript/mastodon/load_polyfills.js @@ -23,15 +23,14 @@ function loadPolyfills() { ); // Latest version of Firefox and Safari do not have IntersectionObserver. - // Edge does not have requestIdleCallback and object-fit CSS property. + // Edge does not have requestIdleCallback. // This avoids shipping them all the polyfills. const needsExtraPolyfills = !( window.AbortController && window.IntersectionObserver && window.IntersectionObserverEntry && 'isIntersecting' in IntersectionObserverEntry.prototype && - window.requestIdleCallback && - 'object-fit' in (new Image()).style + window.requestIdleCallback ); return Promise.all([ diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 8958b1ec0..d0fa1022c 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -563,7 +563,7 @@ "status.favourite": "Favourite", "status.filter": "Filter this post", "status.filtered": "Filtered", - "status.hide": "Hide toot", + "status.hide": "Hide post", "status.history.created": "{name} created {date}", "status.history.edited": "{name} edited {date}", "status.load_more": "Load more", diff --git a/app/javascript/mastodon/permissions.js b/app/javascript/mastodon/permissions.js index 752ddd6c5..9ea149e5f 100644 --- a/app/javascript/mastodon/permissions.js +++ b/app/javascript/mastodon/permissions.js @@ -1,3 +1,4 @@ -export const PERMISSION_INVITE_USERS = 0x0000000000010000; -export const PERMISSION_MANAGE_USERS = 0x0000000000000400; -export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010; +export const PERMISSION_INVITE_USERS = 0x0000000000010000; +export const PERMISSION_MANAGE_USERS = 0x0000000000000400; +export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020; +export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 5e7fedf48..4250cf2b6 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1216,7 +1216,7 @@ a.name-tag, path:first-child { fill: rgba($highlight-text-color, 0.25) !important; - fill-opacity: 100% !important; + fill-opacity: 1 !important; } path:last-child { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 713144f7d..23c29260b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4268,7 +4268,7 @@ a.status-card.compact:hover { } @keyframes heartbeat { - from { + 0% { transform: scale(1); animation-timing-function: ease-out; } @@ -7343,7 +7343,7 @@ noscript { path:first-child { fill: rgba($highlight-text-color, 0.25) !important; - fill-opacity: 100% !important; + fill-opacity: 1 !important; } path:last-child { diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index f553c5501..6812d5462 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -279,10 +279,10 @@ color: $dark-text-color; &__chart { - background: rgba(darken($ui-primary-color, 14%), 0.2); + background: rgba(darken($ui-primary-color, 14%), 0.7); &.leading { - background: rgba($ui-highlight-color, 0.2); + background: rgba($ui-highlight-color, 0.5); } } } diff --git a/app/lib/admin/system_check/elasticsearch_check.rb b/app/lib/admin/system_check/elasticsearch_check.rb index a63988224..7f922978f 100644 --- a/app/lib/admin/system_check/elasticsearch_check.rb +++ b/app/lib/admin/system_check/elasticsearch_check.rb @@ -13,7 +13,14 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck def message if running_version.present? - Admin::SystemCheck::Message.new(:elasticsearch_version_check, I18n.t('admin.system_checks.elasticsearch_version_check.version_comparison', running_version: running_version, required_version: required_version)) + Admin::SystemCheck::Message.new( + :elasticsearch_version_check, + I18n.t( + 'admin.system_checks.elasticsearch_version_check.version_comparison', + running_version: running_version, + required_version: required_version + ) + ) else Admin::SystemCheck::Message.new(:elasticsearch_running_check) end @@ -23,7 +30,8 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck def running_version @running_version ||= begin - Chewy.client.info['version']['number'] + Chewy.client.info['version']['minimum_wire_compatibility_version'] || + Chewy.client.info['version']['number'] rescue Faraday::ConnectionFailed nil end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 9fe9ec346..14208e557 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -414,6 +414,7 @@ class FeedManager end return true if check_for_blocks.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } + return true if crutches[:blocked_by][status.account_id] if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply should_filter = !crutches[:following][status.in_reply_to_account_id] # and I'm not following the person it's a reply to @@ -606,7 +607,7 @@ class FeedManager crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) - crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).index_with(true) + crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true) crutches end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index b53a82db2..b595529f8 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -83,6 +83,7 @@ class Form::AdminSettings validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } validates :site_short_description, length: { maximum: 200 }, if: -> { defined?(@site_short_description) } + validate :validate_site_uploads KEYS.each do |key| define_method(key) do @@ -104,11 +105,16 @@ class Form::AdminSettings define_method("#{key}=") do |file| value = public_send(key) value.file = file + rescue Mastodon::DimensionsValidationError => e + errors.add(key.to_sym, e.message) end end def save - return false unless valid? + # NOTE: Annoyingly, files are processed and can error out before + # validations are called, and `valid?` clears errors… + # So for now, return early if errors aren't empty. + return false unless errors.empty? && valid? KEYS.each do |key| next if PSEUDO_KEYS.include?(key) || !instance_variable_defined?("@#{key}") @@ -141,4 +147,16 @@ class Form::AdminSettings value end end + + def validate_site_uploads + UPLOAD_KEYS.each do |key| + next unless instance_variable_defined?("@#{key}") + upload = instance_variable_get("@#{key}") + next if upload.valid? + + upload.errors.each do |error| + errors.import(error, attribute: key) + end + end + end end diff --git a/app/models/relay.rb b/app/models/relay.rb index d6ddd30ed..c66bfe4ff 100644 --- a/app/models/relay.rb +++ b/app/models/relay.rb @@ -18,6 +18,7 @@ class Relay < ApplicationRecord scope :enabled, -> { accepted } + before_validation :strip_url before_destroy :ensure_disabled alias enabled? accepted? @@ -74,4 +75,8 @@ class Relay < ApplicationRecord def ensure_disabled disable! if enabled? end + + def strip_url + inbox_url&.strip! + end end diff --git a/app/models/tag.rb b/app/models/tag.rb index b66f85423..47a05d00a 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -26,8 +26,12 @@ class Tag < ApplicationRecord has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_many :followers, through: :passive_relationships, source: :account - HASHTAG_SEPARATORS = "_\u00B7\u200c" - HASHTAG_NAME_PAT = "([[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}][[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)" + HASHTAG_SEPARATORS = "_\u00B7\u30FB\u200c" + HASHTAG_FIRST_SEQUENCE_CHUNK_ONE = "[[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}]" + HASHTAG_FIRST_SEQUENCE_CHUNK_TWO = "[[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_]" + HASHTAG_FIRST_SEQUENCE = "(#{HASHTAG_FIRST_SEQUENCE_CHUNK_ONE}#{HASHTAG_FIRST_SEQUENCE_CHUNK_TWO})" + HASTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' + HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASTAG_LAST_SEQUENCE}" HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_PAT})/i HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i @@ -45,7 +49,11 @@ class Tag < ApplicationRecord scope :listable, -> { where(listable: [true, nil]) } scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) } scope :not_trendable, -> { where(trendable: false) } - scope :recently_used, ->(account) { joins(:statuses).where(statuses: { id: account.statuses.select(:id).limit(1000) }).group(:id).order(Arel.sql('count(*) desc')) } + scope :recently_used, ->(account) { + joins(:statuses) + .where(statuses: { id: account.statuses.select(:id).limit(1000) }) + .group(:id).order(Arel.sql('count(*) desc')) + } scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index update_index('tags', :self) @@ -105,7 +113,8 @@ class Tag < ApplicationRecord names = Array(name_or_names).map { |str| [normalize(str), str] }.uniq(&:first) names.map do |(normalized_name, display_name)| - tag = matching_name(normalized_name).first || create(name: normalized_name, display_name: display_name.gsub(HASHTAG_INVALID_CHARS_RE, '')) + tag = matching_name(normalized_name).first || create(name: normalized_name, + display_name: display_name.gsub(HASHTAG_INVALID_CHARS_RE, '')) yield tag if block_given? @@ -154,6 +163,9 @@ class Tag < ApplicationRecord end def validate_display_name_change - errors.add(:display_name, I18n.t('tags.does_not_match_previous_name')) unless HashtagNormalizer.new.normalize(display_name).casecmp(name.mb_chars).zero? + unless HashtagNormalizer.new.normalize(display_name).casecmp(name.mb_chars).zero? + errors.add(:display_name, + I18n.t('tags.does_not_match_previous_name')) + end end end diff --git a/app/models/user.rb b/app/models/user.rb index 4344da2ff..2e3c067ec 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -498,6 +498,7 @@ class User < ApplicationRecord BootstrapTimelineWorker.perform_async(account_id) ActivityTracker.increment('activity:accounts:local') UserMailer.welcome(self).deliver_later + TriggerWebhookWorker.perform_async('account.approved', 'Account', account_id) end def prepare_returning_user! diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 431edd75d..4aafb1257 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -15,6 +15,7 @@ class Webhook < ApplicationRecord EVENTS = %w( + account.approved account.created report.created ).freeze diff --git a/app/serializers/rest/preferences_serializer.rb b/app/serializers/rest/preferences_serializer.rb index 874bd990d..e1c654460 100644 --- a/app/serializers/rest/preferences_serializer.rb +++ b/app/serializers/rest/preferences_serializer.rb @@ -7,6 +7,7 @@ class REST::PreferencesSerializer < ActiveModel::Serializer attribute :reading_default_sensitive_media, key: 'reading:expand:media' attribute :reading_default_sensitive_text, key: 'reading:expand:spoilers' + attribute :reading_autoplay_gifs, key: 'reading:autoplay:gifs' def posting_default_privacy object.user.setting_default_privacy @@ -27,4 +28,8 @@ class REST::PreferencesSerializer < ActiveModel::Serializer def reading_default_sensitive_text object.user.setting_expand_spoilers end + + def reading_autoplay_gifs + object.user.setting_auto_play_gif + end end diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb index 4cbaa04c6..7d0879c79 100644 --- a/app/services/fetch_oembed_service.rb +++ b/app/services/fetch_oembed_service.rb @@ -28,7 +28,7 @@ class FetchOEmbedService page = Nokogiri::HTML(html) if @format.nil? || @format == :json - @endpoint_url ||= page.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value + @endpoint_url ||= page.at_xpath('//link[@type="application/json+oembed"]|//link[@type="text/json+oembed"]')&.attribute('href')&.value @format ||= :json if @endpoint_url end @@ -100,7 +100,7 @@ class FetchOEmbedService end def validate(oembed) - oembed if oembed[:version] == '1.0' && oembed[:type].present? + oembed if oembed[:version].to_s == '1.0' && oembed[:type].present? end def html diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 6856c2c51..211544fea 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -3,10 +3,13 @@ class SuspendAccountService < BaseService include Payloadable + # Carry out the suspension of a recently-suspended account + # @param [Account] account Account to suspend def call(account) + return unless account.suspended? + @account = account - suspend! reject_remote_follows! distribute_update_actor! unmerge_from_home_timelines! @@ -16,10 +19,6 @@ class SuspendAccountService < BaseService private - def suspend! - @account.suspend! unless @account.suspended? - end - def reject_remote_follows! return if @account.local? || !@account.activitypub? diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 534203dce..70667308e 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -2,10 +2,12 @@ class UnsuspendAccountService < BaseService include Payloadable + + # Restores a recently-unsuspended account + # @param [Account] account Account to restore def call(account) @account = account - unsuspend! refresh_remote_account! return if @account.nil? || @account.suspended? @@ -18,10 +20,6 @@ class UnsuspendAccountService < BaseService private - def unsuspend! - @account.unsuspend! if @account.suspended? - end - def refresh_remote_account! return if @account.local? diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index 75d1edb87..a90fb6958 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -10,5 +10,7 @@ class URLValidator < ActiveModel::EachValidator def compliant?(url) parsed_url = Addressable::URI.parse(url) parsed_url && %w(http https).include?(parsed_url.scheme) && parsed_url.host + rescue Addressable::URI::InvalidURIError + false end end diff --git a/app/views/admin/report_notes/_report_note.html.haml b/app/views/admin/report_notes/_report_note.html.haml index 14df2f609..54c252ee8 100644 --- a/app/views/admin/report_notes/_report_note.html.haml +++ b/app/views/admin/report_notes/_report_note.html.haml @@ -4,8 +4,8 @@ .report-notes__item__header %span.username = link_to report_note.account.username, admin_account_path(report_note.account_id) - %time.relative-formatted{ datetime: report_note.created_at } - = t('admin.report_notes.created_at') + %time.relative-formatted{ datetime: report_note.created_at.iso8601 } + = l report_note.created_at.to_date .report-notes__item__content = simple_format(h(report_note.content)) diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 1535e5003..181067802 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -141,7 +141,7 @@ - else = link_to @report.account.domain, admin_instance_path(@report.account.domain) %time.relative-formatted{ datetime: @report.created_at.iso8601 } - = t('admin.report_notes.created_at') + = l @report.created_at.to_date .report-notes__item__content = simple_format(h(@report.comment)) diff --git a/app/views/disputes/strikes/show.html.haml b/app/views/disputes/strikes/show.html.haml index cab0a17eb..7797348dd 100644 --- a/app/views/disputes/strikes/show.html.haml +++ b/app/views/disputes/strikes/show.html.haml @@ -111,7 +111,7 @@ %span.username = link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account) %time.relative-formatted{ datetime: @appeal.created_at.iso8601 } - = t('admin.report_notes.created_at') + = l @appeal.created_at.to_date .report-notes__item__content = simple_format(h(@appeal.text)) diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml index dee7c63d9..cf608766b 100644 --- a/app/views/layouts/modal.html.haml +++ b/app/views/layouts/modal.html.haml @@ -5,7 +5,7 @@ .name = t 'users.signed_in_as' %span.username @#{current_account.local_username_and_domain} - = link_to destroy_user_session_path(continue: true), method: :delete, class: 'logout-link icon-button' do + = link_to destroy_user_session_path(continue: true), method: :delete, class: 'logout-link icon-button', title: t('applications.logout'), 'aria-label': t('applications.logout') do = fa_icon 'sign-out' .container-alt= yield |