From a1ad2ad9519fda525858ba5aef86815a6f6385f2 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 26 Jun 2020 14:31:13 +0200 Subject: Change sensitive preview cards to not blur text (#14143) Also only require click-through for interactive embeds. --- app/javascript/mastodon/features/status/components/card.js | 8 +++----- app/javascript/styles/mastodon/components.scss | 5 ----- 2 files changed, 3 insertions(+), 10 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index e35b1fd5f..0af7c54e4 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -6,7 +6,6 @@ import { FormattedMessage } from 'react-intl'; import punycode from 'punycode'; import classnames from 'classnames'; import Icon from 'mastodon/components/icon'; -import classNames from 'classnames'; import { useBlurhash } from 'mastodon/initial_state'; import { decode } from 'blurhash'; import { debounce } from 'lodash'; @@ -231,7 +230,7 @@ export default class Card extends React.PureComponent { const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); const description = ( -
+
{title} {!(horizontal || compact) &&

{trim(card.get('description') || '', maxDescription)}

} {provider} @@ -239,7 +238,7 @@ export default class Card extends React.PureComponent { ); let embed = ''; - let canvas = ; + let canvas = ; let thumbnail = ; let spoilerButton = ( ); spoilerButton = ( -
+
{spoilerButton}
); @@ -305,7 +304,6 @@ export default class Card extends React.PureComponent { {embed} {description} - {!revealed && spoilerButton} ); } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index e3da780ef..7a22aeb04 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3105,11 +3105,6 @@ a.status-card { flex: 1 1 auto; overflow: hidden; padding: 14px 14px 14px 8px; - - &--blurred { - filter: blur(2px); - pointer-events: none; - } } .status-card__description { -- cgit From fa4876a1b93d4bb62038cca75bd5017fe49b59ae Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 28 Jun 2020 20:55:32 +0200 Subject: Fix read marker state not being udpated internally (#14155) Fixes #14151 --- app/javascript/mastodon/reducers/markers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/reducers/markers.js b/app/javascript/mastodon/reducers/markers.js index 2e67be82e..fb1572ff5 100644 --- a/app/javascript/mastodon/reducers/markers.js +++ b/app/javascript/mastodon/reducers/markers.js @@ -1,6 +1,6 @@ import { MARKERS_SUBMIT_SUCCESS, -} from '../actions/notifications'; +} from '../actions/markers'; const initialState = ImmutableMap({ home: '0', -- cgit From 64aac3073340dbc92c33f5f1c6f76dcafa77a450 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 29 Jun 2020 13:56:55 +0200 Subject: Add customizable thumbnails for audio and video attachments (#14145) - Change audio files to not be stripped of metadata - Automatically extract cover art from audio if it exists - Add `thumbnail` parameter to `POST /api/v1/media`, `POST /api/v2/media` and `PUT /api/v1/media/:id` - Add `icon` to represent it in attachments in ActivityPub - Fix `preview_url` containing URL of missing missing image when there is no thumbnail instead of null - Fix duration of audio not being displayed on public pages until the file is loaded --- app/controllers/api/v1/media_controller.rb | 2 +- app/controllers/media_proxy_controller.rb | 4 +- app/controllers/settings/pictures_controller.rb | 13 +-- app/javascript/mastodon/components/status.js | 3 +- app/javascript/mastodon/features/audio/index.js | 42 +++++--- .../features/status/components/detailed_status.js | 3 +- app/lib/activitypub/activity/create.rb | 12 ++- app/models/concerns/remotable.rb | 29 +++--- app/models/media_attachment.rb | 108 ++++++++++++++------- app/serializers/activitypub/note_serializer.rb | 10 ++ .../rest/media_attachment_serializer.rb | 4 +- .../activitypub/process_account_service.rb | 4 +- app/views/statuses/_detailed_status.html.haml | 2 +- app/views/statuses/_simple_status.html.haml | 2 +- app/workers/post_process_media_worker.rb | 2 +- app/workers/redownload_media_worker.rb | 3 +- ...0_add_thumbnail_columns_to_media_attachments.rb | 11 +++ db/schema.rb | 7 +- lib/mastodon/media_cli.rb | 10 +- lib/paperclip/attachment_extensions.rb | 2 +- lib/paperclip/image_extractor.rb | 49 ++++++++++ lib/paperclip/type_corrector.rb | 10 +- spec/models/concerns/remotable_spec.rb | 53 +++------- 23 files changed, 247 insertions(+), 138 deletions(-) create mode 100644 db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb create mode 100644 lib/paperclip/image_extractor.rb (limited to 'app') diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 0bb3d0d27..a2a919a3e 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -39,7 +39,7 @@ class Api::V1::MediaController < Api::BaseController end def media_attachment_params - params.permit(:file, :description, :focus) + params.permit(:file, :thumbnail, :description, :focus) end def file_type_error diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 014b89de1..a8261ec2b 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -28,8 +28,8 @@ class MediaProxyController < ApplicationController private def redownload! - @media_attachment.file_remote_url = @media_attachment.remote_url - @media_attachment.created_at = Time.now.utc + @media_attachment.download_file! + @media_attachment.created_at = Time.now.utc @media_attachment.save! end diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index 73926707b..df2a6eed3 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -7,13 +7,8 @@ module Settings before_action :set_picture def destroy - if valid_picture - account_params = { - @picture => nil, - (@picture + '_remote_url') => nil, - } - - msg = UpdateAccountService.new.call(@account, account_params) ? I18n.t('generic.changes_saved_msg') : nil + if valid_picture? + msg = I18n.t('generic.changes_saved_msg') if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' }) redirect_to settings_profile_path, notice: msg, status: 303 else bad_request @@ -30,8 +25,8 @@ module Settings @picture = params[:id] end - def valid_picture - @picture == 'avatar' || @picture == 'header' + def valid_picture? + %w(avatar header).include?(@picture) end end end diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 2dc961936..827b69500 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -352,7 +352,8 @@ class Status extends ImmutablePureComponent { this.handlePosterLoad(img); - img.src = this.props.poster; + if (!this.props.blurhash) { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => this.handlePosterLoad(img); + img.src = this.props.poster; + } else { + this._setColorScheme(); + this._decodeBlurhash(); + } } componentDidUpdate (prevProps, prevState) { - if (prevProps.poster !== this.props.poster) { + if (prevProps.poster !== this.props.poster && !this.props.blurhash) { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => this.handlePosterLoad(img); img.src = this.props.poster; } - if (prevState.blurhash !== this.state.blurhash) { - const context = this.blurhashCanvas.getContext('2d'); - const pixels = decode(this.state.blurhash, 32, 32); - const outputImageData = new ImageData(pixels, 32, 32); - - context.putImageData(outputImageData, 0, 0); + if (prevState.blurhash !== this.state.blurhash || prevProps.blurhash !== this.props.blurhash) { + this._setColorScheme(); + this._decodeBlurhash(); } this._clear(); this._draw(); } + _decodeBlurhash () { + const context = this.blurhashCanvas.getContext('2d'); + const pixels = decode(this.props.blurhash || this.state.blurhash, 32, 32); + const outputImageData = new ImageData(pixels, 32, 32); + + context.putImageData(outputImageData, 0, 0); + } + componentWillUnmount () { window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.handleResize); @@ -415,7 +426,7 @@ class Audio extends React.PureComponent { } handlePosterLoad = image => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = image.width; @@ -425,10 +436,15 @@ class Audio extends React.PureComponent { const inputImageData = context.getImageData(0, 0, image.width, image.height); const blurhash = encode(inputImageData.data, image.width, image.height, 4, 4); + + this.setState({ blurhash }); + } + + _setColorScheme () { + const blurhash = this.props.blurhash || this.state.blurhash; const averageColor = decodeRGB(decode83(blurhash.slice(2, 6))); this.setState({ - blurhash, color: adjustColor(averageColor), darkText: luma(averageColor) >= 165, }); diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 935e4207e..f7d0c9bd4 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -125,7 +125,8 @@ class DetailedStatus extends ImmutablePureComponent { src={attachment.get('url')} alt={attachment.get('description')} duration={attachment.getIn(['meta', 'original', 'duration'], 0)} - poster={status.getIn(['account', 'avatar_static'])} + poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} + blurhash={attachment.get('blurhash')} height={150} /> ); diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 3509a6c40..d3d460551 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -238,12 +238,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity begin href = Addressable::URI.parse(attachment['url']).normalize.to_s - media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['summary'].presence || attachment['name'].presence, focus: attachment['focalPoint'], blurhash: supported_blurhash?(attachment['blurhash']) ? attachment['blurhash'] : nil) + media_attachment = MediaAttachment.create(account: @account, remote_url: href, thumbnail_remote_url: icon_url_from_attachment(attachment), description: attachment['summary'].presence || attachment['name'].presence, focus: attachment['focalPoint'], blurhash: supported_blurhash?(attachment['blurhash']) ? attachment['blurhash'] : nil) media_attachments << media_attachment next if unsupported_media_type?(attachment['mediaType']) || skip_download? - media_attachment.file_remote_url = href + media_attachment.download_file! + media_attachment.download_thumbnail! media_attachment.save rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id) @@ -256,6 +257,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity media_attachments end + def icon_url_from_attachment(attachment) + url = attachment['icon'].is_a?(Hash) ? attachment['icon']['url'] : attachment['icon'] + Addressable::URI.parse(url).normalize.to_s if url.present? + rescue Addressable::URI::InvalidURIError + nil + end + def process_poll return unless @object['type'] == 'Question' && (@object['anyOf'].is_a?(Array) || @object['oneOf'].is_a?(Array)) diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index c728a460e..6fc1dcc26 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -4,12 +4,12 @@ module Remotable extend ActiveSupport::Concern class_methods do - def remotable_attachment(attachment_name, limit, suppress_errors: true) - attribute_name = "#{attachment_name}_remote_url".to_sym - method_name = "#{attribute_name}=".to_sym - alt_method_name = "reset_#{attachment_name}!".to_sym + def remotable_attachment(attachment_name, limit, suppress_errors: true, download_on_assign: true, attribute_name: nil) + attribute_name ||= "#{attachment_name}_remote_url".to_sym + + define_method("download_#{attachment_name}!") do + url = self[attribute_name] - define_method method_name do |url| return if url.blank? begin @@ -18,7 +18,7 @@ module Remotable return end - return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.blank? || (self[attribute_name] == url && send("#{attachment_name}_file_name").present?) + return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.blank? begin Request.new(:get, url).perform do |response| @@ -36,10 +36,8 @@ module Remotable basename = SecureRandom.hex(8) - send("#{attachment_name}_file_name=", basename + extname) - send("#{attachment_name}=", StringIO.new(response.body_with_limit(limit))) - - self[attribute_name] = url if has_attribute?(attribute_name) + public_send("#{attachment_name}_file_name=", basename + extname) + public_send("#{attachment_name}=", StringIO.new(response.body_with_limit(limit))) end rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" @@ -50,14 +48,15 @@ module Remotable end end - define_method alt_method_name do - url = self[attribute_name] + define_method("#{attribute_name}=") do |url| + return if self[attribute_name] == url && public_send("#{attachment_name}_file_name").present? - return if url.blank? + self[attribute_name] = url - self[attribute_name] = '' - send(method_name, url) + public_send("download_#{attachment_name}!") if download_on_assign end + + alias_method("reset_#{attachment_name}!", "download_#{attachment_name}!") end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index d44467009..f67566a18 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -21,6 +21,11 @@ # blurhash :string # processing :integer # file_storage_schema_version :integer +# thumbnail_file_name :string +# thumbnail_content_type :string +# thumbnail_file_size :integer +# thumbnail_updated_at :datetime +# thumbnail_remote_url :string # class MediaAttachment < ApplicationRecord @@ -49,13 +54,13 @@ class MediaAttachment < ApplicationRecord original: { pixels: 1_638_400, # 1280x1280px file_geometry_parser: FastGeometryParser, - }, + }.freeze, small: { pixels: 160_000, # 400x400px file_geometry_parser: FastGeometryParser, blurhash: BLURHASH_OPTIONS, - }, + }.freeze, }.freeze VIDEO_FORMAT = { @@ -74,14 +79,14 @@ class MediaAttachment < ApplicationRecord 'frames:v' => 60 * 60 * 3, 'crf' => 18, 'map_metadata' => '-1', - }, - }, + }.freeze, + }.freeze, }.freeze VIDEO_PASSTHROUGH_OPTIONS = { - video_codecs: ['h264'], - audio_codecs: ['aac', nil], - colorspaces: ['yuv420p'], + video_codecs: ['h264'].freeze, + audio_codecs: ['aac', nil].freeze, + colorspaces: ['yuv420p'].freeze, options: { format: 'mp4', convert_options: { @@ -90,9 +95,9 @@ class MediaAttachment < ApplicationRecord 'map_metadata' => '-1', 'c:v' => 'copy', 'c:a' => 'copy', - }, - }, - }, + }.freeze, + }.freeze, + }.freeze, }.freeze VIDEO_STYLES = { @@ -101,15 +106,15 @@ class MediaAttachment < ApplicationRecord output: { 'loglevel' => 'fatal', vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', - }, - }, + }.freeze, + }.freeze, format: 'png', time: 0, file_geometry_parser: FastGeometryParser, blurhash: BLURHASH_OPTIONS, - }, + }.freeze, - original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS), + original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS).freeze, }.freeze AUDIO_STYLES = { @@ -119,16 +124,23 @@ class MediaAttachment < ApplicationRecord convert_options: { output: { 'loglevel' => 'fatal', - 'map_metadata' => '-1', 'q:a' => 2, - }, - }, - }, + }.freeze, + }.freeze, + }.freeze, }.freeze VIDEO_CONVERTED_STYLES = { - small: VIDEO_STYLES[:small], - original: VIDEO_FORMAT, + small: VIDEO_STYLES[:small].freeze, + original: VIDEO_FORMAT.freeze, + }.freeze + + THUMBNAIL_STYLES = { + original: IMAGE_STYLES[:small].freeze, + }.freeze + + GLOBAL_CONVERT_OPTIONS = { + all: '-quality 90 -strip +set modify-date +set create-date', }.freeze IMAGE_LIMIT = 10.megabytes @@ -144,18 +156,28 @@ class MediaAttachment < ApplicationRecord has_attached_file :file, styles: ->(f) { file_styles f }, processors: ->(f) { file_processors f }, - convert_options: { all: '-quality 90 -strip +set modify-date +set create-date' } + convert_options: GLOBAL_CONVERT_OPTIONS validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :larger_media_format? validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :larger_media_format? - remotable_attachment :file, VIDEO_LIMIT, suppress_errors: false + remotable_attachment :file, VIDEO_LIMIT, suppress_errors: false, download_on_assign: false, attribute_name: :remote_url + + has_attached_file :thumbnail, + styles: THUMBNAIL_STYLES, + processors: [:lazy_thumbnail, :blurhash_transcoder], + convert_options: GLOBAL_CONVERT_OPTIONS + + validates_attachment_content_type :thumbnail, content_type: IMAGE_MIME_TYPES + validates_attachment_size :thumbnail, less_than: IMAGE_LIMIT + remotable_attachment :thumbnail, IMAGE_LIMIT, suppress_errors: true, download_on_assign: false include Attachmentable validates :account, presence: true validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, if: :local? validates :file, presence: true, if: :local? + validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? } scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) } scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) } @@ -215,16 +237,21 @@ class MediaAttachment < ApplicationRecord @delay_processing end + def delay_processing_for_attachment?(attachment_name) + @delay_processing && attachment_name == :file + end + after_commit :enqueue_processing, on: :create after_commit :reset_parent_cache, on: :update before_create :prepare_description, unless: :local? before_create :set_shortcode before_create :set_processing - before_create :set_meta - before_post_process :set_type_and_extension - before_post_process :check_video_dimensions + after_post_process :set_meta + + before_file_post_process :set_type_and_extension + before_file_post_process :check_video_dimensions class << self def supported_mime_types @@ -237,25 +264,25 @@ class MediaAttachment < ApplicationRecord private - def file_styles(f) - if f.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type) + def file_styles(attachment) + if attachment.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(attachment.instance.file_content_type) VIDEO_CONVERTED_STYLES - elsif IMAGE_MIME_TYPES.include?(f.instance.file_content_type) + elsif IMAGE_MIME_TYPES.include?(attachment.instance.file_content_type) IMAGE_STYLES - elsif VIDEO_MIME_TYPES.include?(f.instance.file_content_type) + elsif VIDEO_MIME_TYPES.include?(attachment.instance.file_content_type) VIDEO_STYLES else AUDIO_STYLES end end - def file_processors(f) - if f.file_content_type == 'image/gif' + def file_processors(instance) + if instance.file_content_type == 'image/gif' [:gif_transcoder, :blurhash_transcoder] - elsif VIDEO_MIME_TYPES.include?(f.file_content_type) + elsif VIDEO_MIME_TYPES.include?(instance.file_content_type) [:video_transcoder, :blurhash_transcoder, :type_corrector] - elsif AUDIO_MIME_TYPES.include?(f.file_content_type) - [:transcoder, :type_corrector] + elsif AUDIO_MIME_TYPES.include?(instance.file_content_type) + [:image_extractor, :transcoder, :type_corrector] else [:lazy_thumbnail, :blurhash_transcoder, :type_corrector] end @@ -298,7 +325,7 @@ class MediaAttachment < ApplicationRecord def check_video_dimensions return unless (video? || gifv?) && file.queued_for_write[:original].present? - movie = FFMPEG::Movie.new(file.queued_for_write[:original].path) + movie = ffmpeg_data(file.queued_for_write[:original].path) return unless movie.valid? @@ -317,6 +344,8 @@ class MediaAttachment < ApplicationRecord meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file) end + meta[:small] = image_geometry(thumbnail.queued_for_write[:original]) if thumbnail.queued_for_write.key?(:original) + meta end @@ -334,7 +363,7 @@ class MediaAttachment < ApplicationRecord end def video_metadata(file) - movie = FFMPEG::Movie.new(file.path) + movie = ffmpeg_data(file.path) return {} unless movie.valid? @@ -347,6 +376,13 @@ class MediaAttachment < ApplicationRecord }.compact end + # We call this method about 3 different times on potentially different + # paths but ultimately the same file, so it makes sense to memoize the + # result while disregarding the path + def ffmpeg_data(path = nil) + @ffmpeg_data ||= FFMPEG::Movie.new(path) + end + def enqueue_processing PostProcessMediaWorker.perform_async(id) if delay_processing? end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 110621a28..f26fd93a4 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -167,6 +167,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attributes :type, :media_type, :url, :name, :blurhash attribute :focal_point, if: :focal_point? + has_one :icon, serializer: ActivityPub::ImageSerializer, if: :thumbnail? + def type 'Document' end @@ -190,6 +192,14 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer def focal_point [object.file.meta['focus']['x'], object.file.meta['focus']['y']] end + + def icon + object.thumbnail + end + + def thumbnail? + object.thumbnail.present? + end end class MentionSerializer < ActivityPub::Serializer diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb index cc10e3001..e65f7acf1 100644 --- a/app/serializers/rest/media_attachment_serializer.rb +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -28,7 +28,9 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer def preview_url if object.needs_redownload? media_proxy_url(object.id, :small) - else + elsif object.thumbnail.present? + full_asset_url(object.thumbnail.url(:original)) + elsif object.file.styles.key?(:small) full_asset_url(object.file.url(:small)) end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f4276cece..85b915ec6 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -89,8 +89,8 @@ class ActivityPub::ProcessAccountService < BaseService end def set_fetchable_attributes! - @account.avatar_remote_url = image_url('icon') unless skip_download? - @account.header_remote_url = image_url('image') unless skip_download? + @account.avatar_remote_url = image_url('icon') || '' unless skip_download? + @account.header_remote_url = image_url('image') || '' unless skip_download? @account.public_key = public_key || '' @account.statuses_count = outbox_total_items if outbox_total_items.present? @account.following_count = following_total_items if following_total_items.present? diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index 684dd08d1..d10017db9 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -33,7 +33,7 @@ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first - = react_component :audio, src: audio.file.url(:original), poster: full_asset_url(status.account.avatar_static_url), width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do + = react_component :audio, src: audio.file.url(:original), poster: audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url, blurhash: audio.blurhash, width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else = react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index 06dc5ff93..ab09dfe45 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -39,7 +39,7 @@ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first - = react_component :audio, src: audio.file.url(:original), poster: full_asset_url(status.account.avatar_static_url), width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do + = react_component :audio, src: audio.file.url(:original), poster: audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url, blurhash: audio.blurhash, width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else = react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do diff --git a/app/workers/post_process_media_worker.rb b/app/workers/post_process_media_worker.rb index 73f9ae2bf..a904f35b1 100644 --- a/app/workers/post_process_media_worker.rb +++ b/app/workers/post_process_media_worker.rb @@ -32,7 +32,7 @@ class PostProcessMediaWorker media_attachment.file.reprocess!(:original) media_attachment.processing = :complete - media_attachment.file_meta = previous_meta + media_attachment.file_meta = previous_meta.merge(media_attachment.file_meta).with_indifferent_access.slice(:focus, :original, :small) media_attachment.save rescue ActiveRecord::RecordNotFound true diff --git a/app/workers/redownload_media_worker.rb b/app/workers/redownload_media_worker.rb index 071501a49..0638cd0f0 100644 --- a/app/workers/redownload_media_worker.rb +++ b/app/workers/redownload_media_worker.rb @@ -11,7 +11,8 @@ class RedownloadMediaWorker return if media_attachment.remote_url.blank? - media_attachment.file_remote_url = media_attachment.remote_url + media_attachment.download_file! + media_attachment.download_thumbnail! media_attachment.save rescue ActiveRecord::RecordNotFound true diff --git a/db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb b/db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb new file mode 100644 index 000000000..f9c87a53c --- /dev/null +++ b/db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb @@ -0,0 +1,11 @@ +class AddThumbnailColumnsToMediaAttachments < ActiveRecord::Migration[5.2] + def up + add_attachment :media_attachments, :thumbnail + add_column :media_attachments, :thumbnail_remote_url, :string + end + + def down + remove_attachment :media_attachments, :thumbnail + remove_column :media_attachments, :thumbnail_remote_url + end +end diff --git a/db/schema.rb b/db/schema.rb index d603d98d7..277eccba7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_06_20_164023) do +ActiveRecord::Schema.define(version: 2020_06_27_125810) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -489,6 +489,11 @@ ActiveRecord::Schema.define(version: 2020_06_20_164023) do t.string "blurhash" t.integer "processing" t.integer "file_storage_schema_version" + t.string "thumbnail_file_name" + t.string "thumbnail_content_type" + t.integer "thumbnail_file_size" + t.datetime "thumbnail_updated_at" + t.string "thumbnail_remote_url" t.index ["account_id"], name: "index_media_attachments_on_account_id" t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id" t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb index c95f3410a..2a4e3e379 100644 --- a/lib/mastodon/media_cli.rb +++ b/lib/mastodon/media_cli.rb @@ -31,10 +31,11 @@ module Mastodon processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where('created_at < ?', time_ago)) do |media_attachment| next if media_attachment.file.blank? - size = media_attachment.file_file_size + size = media_attachment.file_file_size + (media_attachment.thumbnail_file_size || 0) unless options[:dry_run] media_attachment.file.destroy + media_attachment.thumbnail.destroy media_attachment.save end @@ -227,11 +228,12 @@ module Mastodon next if media_attachment.remote_url.blank? || (!options[:force] && media_attachment.file_file_name.present?) unless options[:dry_run] - media_attachment.file_remote_url = media_attachment.remote_url + media_attachment.reset_file! + media_attachment.reset_thumbnail! media_attachment.save end - media_attachment.file_file_size + media_attachment.file_file_size + (media_attachment.thumbnail_file_size || 0) end say("Downloaded #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true) @@ -239,7 +241,7 @@ module Mastodon desc 'usage', 'Calculate disk space consumed by Mastodon' def usage - say("Attachments:\t#{number_to_human_size(MediaAttachment.sum(:file_file_size))} (#{number_to_human_size(MediaAttachment.where(account: Account.local).sum(:file_file_size))} local)") + say("Attachments:\t#{number_to_human_size(MediaAttachment.sum(Arel.sql('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')))} (#{number_to_human_size(MediaAttachment.where(account: Account.local).sum(Arel.sql('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')))} local)") say("Custom emoji:\t#{number_to_human_size(CustomEmoji.sum(:image_file_size))} (#{number_to_human_size(CustomEmoji.local.sum(:image_file_size))} local)") say("Preview cards:\t#{number_to_human_size(PreviewCard.sum(:image_file_size))}") say("Avatars:\t#{number_to_human_size(Account.sum(:avatar_file_size))} (#{number_to_human_size(Account.local.sum(:avatar_file_size))} local)") diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index f3e51dbd3..93df0a326 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -7,7 +7,7 @@ module Paperclip # usage, and we still want to generate thumbnails straight # away, it's the only style we need to exclude def process_style?(style_name, style_args) - if style_name == :original && instance.respond_to?(:delay_processing?) && instance.delay_processing? + if style_name == :original && instance.respond_to?(:delay_processing_for_attachment?) && instance.delay_processing_for_attachment?(name) false else style_args.empty? || style_args.include?(style_name) diff --git a/lib/paperclip/image_extractor.rb b/lib/paperclip/image_extractor.rb new file mode 100644 index 000000000..114852e8b --- /dev/null +++ b/lib/paperclip/image_extractor.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'mime/types/columnar' + +module Paperclip + class ImageExtractor < Paperclip::Processor + IMAGE_EXTRACTION_OPTIONS = { + convert_options: { + output: { + 'loglevel' => 'fatal', + vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + }.freeze, + }.freeze, + format: 'png', + time: -1, + file_geometry_parser: FastGeometryParser, + }.freeze + + def make + return @file unless options[:style] == :original + + image = begin + begin + Paperclip::Transcoder.make(file, IMAGE_EXTRACTION_OPTIONS.dup, attachment) + rescue Paperclip::Error, ::Av::CommandError + nil + end + end + + unless image.nil? + begin + attachment.instance.thumbnail = image if image.size.positive? + ensure + # Paperclip does not automatically delete the source file of + # a new attachment while working on copies of it, so we need + # to make sure it's cleaned up + + begin + FileUtils.rm(image) + rescue Errno::ENOENT + nil + end + end + end + + @file + end + end +end diff --git a/lib/paperclip/type_corrector.rb b/lib/paperclip/type_corrector.rb index 0b0c10a56..17e2fc5da 100644 --- a/lib/paperclip/type_corrector.rb +++ b/lib/paperclip/type_corrector.rb @@ -5,13 +5,15 @@ require 'mime/types/columnar' module Paperclip class TypeCorrector < Paperclip::Processor def make - target_extension = options[:format] - extension = File.extname(attachment.instance.file_file_name) + return @file unless options[:format] + + target_extension = '.' + options[:format] + extension = File.extname(attachment.instance_read(:file_name)) return @file unless options[:style] == :original && target_extension && extension != target_extension - attachment.instance.file_content_type = options[:content_type] || attachment.instance.file_content_type - attachment.instance.file_file_name = File.basename(attachment.instance.file_file_name, '.*') + '.' + target_extension + attachment.instance_write(:content_type, options[:content_type] || attachment.instance_read(:content_type)) + attachment.instance_write(:file_name, File.basename(attachment.instance_read(:file_name), '.*') + target_extension) @file end diff --git a/spec/models/concerns/remotable_spec.rb b/spec/models/concerns/remotable_spec.rb index 99a60cbf6..6957b044f 100644 --- a/spec/models/concerns/remotable_spec.rb +++ b/spec/models/concerns/remotable_spec.rb @@ -58,7 +58,11 @@ RSpec.describe Remotable do expect(foo).to respond_to(:reset_hoge!) end - describe '#hoge_remote_url' do + it 'defines a method #download_hoge!' do + expect(foo).to respond_to(:download_hoge!) + end + + describe '#hoge_remote_url=' do before do request end @@ -138,8 +142,8 @@ RSpec.describe Remotable do let(:code) { 500 } it 'calls not send' do - expect(foo).not_to receive(:send).with("#{hoge}=", any_args) - expect(foo).not_to receive(:send).with("#{hoge}_file_name=", any_args) + expect(foo).not_to receive(:public_send).with("#{hoge}=", any_args) + expect(foo).not_to receive(:public_send).with("#{hoge}_file_name=", any_args) foo.hoge_remote_url = url end end @@ -159,26 +163,14 @@ RSpec.describe Remotable do allow(SecureRandom).to receive(:hex).and_return(basename) allow(StringIO).to receive(:new).with(anything).and_return(string_io) - expect(foo).to receive(:send).with("#{hoge}=", string_io) - expect(foo).to receive(:send).with("#{hoge}_file_name=", basename + extname) - foo.hoge_remote_url = url - end - end + expect(foo).to receive(:public_send).with("download_#{hoge}!") - context 'if has_attribute?' do - it 'calls foo[attribute_name] = url' do - allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(true) - expect(foo).to receive('[]=').with(attribute_name, url) foo.hoge_remote_url = url - end - end - context 'unless has_attribute?' do - it 'calls not foo[attribute_name] = url' do - allow(foo).to receive(:has_attribute?) - .with(attribute_name).and_return(false) - expect(foo).not_to receive('[]=').with(attribute_name, url) - foo.hoge_remote_url = url + expect(foo).to receive(:public_send).with("#{hoge}=", string_io) + expect(foo).to receive(:public_send).with("#{hoge}_file_name=", basename + extname) + + foo.download_hoge! end end end @@ -205,26 +197,5 @@ RSpec.describe Remotable do end end end - - describe '#reset_hoge!' do - context 'if url.blank?' do - it 'returns nil, without clearing foo[attribute_name] and calling #hoge_remote_url=' do - url = nil - expect(foo).not_to receive(:send).with(:hoge_remote_url=, url) - foo[attribute_name] = url - expect(foo.reset_hoge!).to be_nil - expect(foo[attribute_name]).to be_nil - end - end - - context 'unless url.blank?' do - it 'clears foo[attribute_name] and calls #hoge_remote_url=' do - foo[attribute_name] = url - expect(foo).to receive(:send).with(:hoge_remote_url=, url) - foo.reset_hoge! - expect(foo[attribute_name]).to be '' - end - end - end end end -- cgit From d1c6dd2d277fee0cf65b480adc868c69df952622 Mon Sep 17 00:00:00 2001 From: trwnh Date: Mon, 29 Jun 2020 06:57:44 -0500 Subject: Fix padding on account header (#14179) --- app/javascript/styles/mastodon/components.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 7a22aeb04..e22f71261 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -6497,7 +6497,7 @@ noscript { &__tabs { display: flex; align-items: flex-start; - padding: 7px 5px; + padding: 7px 10px; margin-top: -55px; &__buttons { @@ -6519,7 +6519,7 @@ noscript { } &__name { - padding: 5px; + padding: 5px 10px; .account-role { vertical-align: top; -- cgit From 5e8f51b29fccfb7d19d53854f3472f7370593ebf Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 29 Jun 2020 20:58:07 +0900 Subject: Replace to testing-library from enzyme (#14152) --- .../mastodon/components/__tests__/button-test.js | 10 +- .../ui/components/__tests__/column-test.js | 15 +- app/javascript/mastodon/test_setup.js | 6 +- package.json | 4 +- yarn.lock | 436 +++++++-------------- 5 files changed, 159 insertions(+), 312 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/components/__tests__/button-test.js b/app/javascript/mastodon/components/__tests__/button-test.js index 160cd3cbc..f5a649f70 100644 --- a/app/javascript/mastodon/components/__tests__/button-test.js +++ b/app/javascript/mastodon/components/__tests__/button-test.js @@ -1,4 +1,4 @@ -import { shallow } from 'enzyme'; +import { render, fireEvent, screen } from '@testing-library/react'; import React from 'react'; import renderer from 'react-test-renderer'; import Button from '../button'; @@ -21,16 +21,16 @@ describe('); + fireEvent.click(screen.getByText('button')); expect(handler.mock.calls.length).toEqual(1); }); it('does not handle click events if props.disabled given', () => { const handler = jest.fn(); - const button = shallow(); + fireEvent.click(screen.getByText('button')); expect(handler.mock.calls.length).toEqual(0); }); diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js index d2791ce08..a56859be0 100644 --- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js +++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js @@ -1,25 +1,24 @@ +import { render, fireEvent, screen } from '@testing-library/react'; import React from 'react'; -import { mount } from 'enzyme'; import Column from '../column'; -import ColumnHeader from '../column_header'; describe('', () => { describe(' click handler', () => { it('runs the scroll animation if the column contains scrollable content', () => { - const wrapper = mount( + const scrollToMock = jest.fn(); + const { container } = render(
, ); - const scrollToMock = jest.fn(); - wrapper.find(Column).find('.scrollable').getDOMNode().scrollTo = scrollToMock; - wrapper.find(ColumnHeader).find('button').simulate('click'); + container.querySelector('.scrollable').scrollTo = scrollToMock; + fireEvent.click(screen.getByText('notifications')); expect(scrollToMock).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 }); }); it('does not try to scroll if there is no scrollable content', () => { - const wrapper = mount(); - wrapper.find(ColumnHeader).find('button').simulate('click'); + render(); + fireEvent.click(screen.getByText('notifications')); }); }); }); diff --git a/app/javascript/mastodon/test_setup.js b/app/javascript/mastodon/test_setup.js index 80148379b..666127af3 100644 --- a/app/javascript/mastodon/test_setup.js +++ b/app/javascript/mastodon/test_setup.js @@ -1,5 +1 @@ -import { configure } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -const adapter = new Adapter(); -configure({ adapter }); +import '@testing-library/jest-dom/extend-expect'; diff --git a/package.json b/package.json index 0f2e7f4f9..01cd3548f 100644 --- a/package.json +++ b/package.json @@ -169,10 +169,10 @@ "wicg-inert": "^3.0.3" }, "devDependencies": { + "@testing-library/jest-dom": "^5.11.0", + "@testing-library/react": "^10.4.3", "babel-eslint": "^10.1.0", "babel-jest": "^25.2.4", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.2", "eslint": "^6.8.0", "eslint-plugin-import": "~2.21.2", "eslint-plugin-jsx-a11y": "~6.3.1", diff --git a/yarn.lock b/yarn.lock index df1cb0a73..10f401cfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -952,7 +952,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== @@ -1370,6 +1370,40 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@testing-library/dom@^7.17.1": + version "7.18.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.18.1.tgz#c49530410fb184522b3b59c4f9cd6397dc5b462d" + integrity sha512-tGq4KAFjaI7j375sMM1RRVleWA0viJWs/w69B+nyDkqYLNkhdTHdV6mGkspJlkn3PUfyBDi3rERDv4PA/LrpVA== + dependencies: + "@babel/runtime" "^7.10.3" + aria-query "^4.2.2" + dom-accessibility-api "^0.4.5" + pretty-format "^25.5.0" + +"@testing-library/jest-dom@^5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.0.tgz#1439f08dc85ce7c6d3bbad0ee5d53b2206f55768" + integrity sha512-mhaCySy7dZlyfcxcYy+0jLllODHEiHkVdmwQ00wD0HrWiSx0fSVHz/0WmdlRkvhfSOuqsRsBUreXOtBvruWGQA== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^4.2.2" + chalk "^3.0.0" + css "^2.2.4" + css.escape "^1.5.1" + jest-diff "^25.1.0" + jest-matcher-utils "^25.1.0" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@^10.4.3": + version "10.4.3" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.3.tgz#c6f356688cffc51f6b35385583d664bb11a161f4" + integrity sha512-A/ydYXcwAcfY7vkPrfUkUTf9HQLL3/GtixTefcu3OyGQtAYQ7XBQj1S9FWbLEhfWa0BLwFwTBFS3Ao1O0tbMJg== + dependencies: + "@babel/runtime" "^7.10.3" + "@testing-library/dom" "^7.17.1" + "@types/babel__core@^7.1.0", "@types/babel__core@^7.1.3": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -1460,6 +1494,14 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "26.0.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.3.tgz#79534e0e94857171c0edc596db0ebe7cb7863251" + integrity sha512-v89ga1clpVL/Y1+YI0eIu1VMW+KU7Xl8PhylVtDKVWaSUHBHYPLXMQGBdrpHewaKoTvlXkksbYqPgz8b4cmRZg== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + "@types/json-schema@^7.0.4": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -1510,6 +1552,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/testing-library__jest-dom@^5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.1.tgz#aba5ee062b7880f69c212ef769389f30752806e5" + integrity sha512-yYn5EKHO3MPEMSOrcAb1dLWY+68CG29LiXKsWmmpVHqoP5+ZRiAVLyUHvPNrO2dABDdUGZvavMsaGpWNjM6N2g== + dependencies: + "@types/jest" "*" + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -1743,22 +1792,6 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -airbnb-prop-types@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" - integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== - dependencies: - array.prototype.find "^2.1.0" - function.prototype.name "^1.1.1" - has "^1.0.3" - is-regex "^1.0.4" - object-is "^1.0.1" - object.assign "^4.1.0" - object.entries "^1.1.0" - prop-types "^15.7.2" - prop-types-exact "^1.2.0" - react-is "^16.9.0" - ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -1918,11 +1951,6 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -1964,14 +1992,6 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.find@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" - integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.4" - array.prototype.flat@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" @@ -2773,18 +2793,6 @@ check-types@^8.0.3: resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ== -cheerio@^1.0.0-rc.3: - version "1.0.0-rc.3" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" - integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== - dependencies: - css-select "~1.2.0" - dom-serializer "~0.1.1" - entities "~1.1.1" - htmlparser2 "^3.9.1" - lodash "^4.15.0" - parse5 "^3.0.1" - "chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" @@ -2999,7 +3007,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.8.1: +commander@^2.18.0, commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3355,16 +3363,6 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" -css-select@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= - dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" - css-system-font-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz#85c6f086aba4eb32c571a3086affc434b84823ed" @@ -3386,16 +3384,26 @@ css-tree@1.0.0-alpha.39: mdn-data "2.0.6" source-map "^0.6.1" -css-what@2.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - css-what@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + +css@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -3705,6 +3713,11 @@ detect-passive-events@^1.0.2: resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a" integrity sha1-btR35uW863kHlzXc01d4nTf5qRo= +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diff-sequences@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6" @@ -3726,11 +3739,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -discontinuous-range@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" - integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= - dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -3773,6 +3781,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz#d9c1cefa89f509d8cf132ab5d250004d755e76e3" + integrity sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg== + dom-helpers@^3.2.1, dom-helpers@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" @@ -3796,20 +3809,12 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" - integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== - dependencies: - domelementtype "^1.3.0" - entities "^1.1.1" - domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: +domelementtype@1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -3826,22 +3831,7 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1, domutils@^1.7.0: +domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -3962,79 +3952,11 @@ enhanced-resolve@4.1.0, enhanced-resolve@^4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" -entities@^1.1.1, entities@~1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== -enzyme-adapter-react-16@^1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz#b16db2f0ea424d58a808f9df86ab6212895a4501" - integrity sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q== - dependencies: - enzyme-adapter-utils "^1.13.0" - enzyme-shallow-equal "^1.0.1" - has "^1.0.3" - object.assign "^4.1.0" - object.values "^1.1.1" - prop-types "^15.7.2" - react-is "^16.12.0" - react-test-renderer "^16.0.0-0" - semver "^5.7.0" - -enzyme-adapter-utils@^1.13.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz#01c885dde2114b4690bf741f8dc94cee3060eb78" - integrity sha512-YuEtfQp76Lj5TG1NvtP2eGJnFKogk/zT70fyYHXK2j3v6CtuHqc8YmgH/vaiBfL8K1SgVVbQXtTcgQZFwzTVyQ== - dependencies: - airbnb-prop-types "^2.15.0" - function.prototype.name "^1.1.2" - object.assign "^4.1.0" - object.fromentries "^2.0.2" - prop-types "^15.7.2" - semver "^5.7.1" - -enzyme-shallow-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz#7afe03db3801c9b76de8440694096412a8d9d49e" - integrity sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ== - dependencies: - has "^1.0.3" - object-is "^1.0.2" - -enzyme@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" - integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== - dependencies: - array.prototype.flat "^1.2.3" - cheerio "^1.0.0-rc.3" - enzyme-shallow-equal "^1.0.1" - function.prototype.name "^1.1.2" - has "^1.0.3" - html-element-map "^1.2.0" - is-boolean-object "^1.0.1" - is-callable "^1.1.5" - is-number-object "^1.0.4" - is-regex "^1.0.5" - is-string "^1.0.5" - is-subset "^0.1.1" - lodash.escape "^4.0.1" - lodash.isequal "^4.5.0" - object-inspect "^1.7.0" - object-is "^1.0.2" - object.assign "^4.1.0" - object.entries "^1.1.1" - object.values "^1.1.1" - raf "^3.4.1" - rst-selector-parser "^2.2.3" - string.prototype.trim "^1.2.1" - errno@^0.1.3, errno@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" @@ -4056,7 +3978,7 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5: +es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: version "1.17.6" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== @@ -4993,25 +4915,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.1, function.prototype.name@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45" - integrity sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - functions-have-names "^1.2.0" - functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91" - integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA== - gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -5419,13 +5327,6 @@ html-comment-regex@^1.1.0: resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== -html-element-map@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22" - integrity sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw== - dependencies: - array-filter "^1.0.0" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -5443,18 +5344,6 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlparser2@^3.9.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -5877,12 +5766,7 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== - -is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.0: +is-callable@^1.1.4, is-callable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== @@ -6034,11 +5918,6 @@ is-nan@^1.3.0: dependencies: define-properties "^1.1.3" -is-number-object@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -6102,7 +5981,7 @@ is-property@^1.0.0, is-property@^1.0.2: resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= -is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0: +is-regex@^1.0.4, is-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== @@ -6129,11 +6008,6 @@ is-string@^1.0.5: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== -is-subset@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" - integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= - is-svg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" @@ -6300,6 +6174,16 @@ jest-config@^26.0.1: micromatch "^4.0.2" pretty-format "^26.0.1" +jest-diff@^25.1.0, jest-diff@^25.2.1, jest-diff@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-diff@^26.0.1: version "26.0.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.0.1.tgz#c44ab3cdd5977d466de69c46929e0e57f89aa1de" @@ -6351,6 +6235,11 @@ jest-environment-node@^26.0.1: jest-mock "^26.0.1" jest-util "^26.0.1" +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-get-type@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039" @@ -6427,6 +6316,16 @@ jest-leak-detector@^26.0.1: jest-get-type "^26.0.0" pretty-format "^26.0.1" +jest-matcher-utils@^25.1.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz#fbc98a12d730e5d2453d7f1ed4a4d948e34b7867" + integrity sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw== + dependencies: + chalk "^3.0.0" + jest-diff "^25.5.0" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-matcher-utils@^26.0.1: version "26.0.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.0.1.tgz#12e1fc386fe4f14678f4cc8dbd5ba75a58092911" @@ -6994,16 +6893,6 @@ lodash.defaults@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= -lodash.escape@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" - integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= - -lodash.flattendeep@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" - integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= - lodash.get@^4.0: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -7049,7 +6938,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0, lodash@~4.17.12: +lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0, lodash@~4.17.12: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -7257,6 +7146,11 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + mini-css-extract-plugin@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" @@ -7366,11 +7260,6 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moo@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" - integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== - mousetrap@^1.5.2: version "1.6.5" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" @@ -7453,17 +7342,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -nearley@^2.7.10: - version "2.19.4" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.4.tgz#7518cbdd7d0e8e08b5f82841b9edb0126239c8b1" - integrity sha512-oqj3m4oqwKsN77pETa9IPvxHHHLW68KrDc2KYoWMUOhDlrNUo7finubwffQMBRnwNCOXc4kRxCZO0Rvx4L6Zrw== - dependencies: - commander "^2.19.0" - moo "^0.5.0" - railroad-diagrams "^1.0.0" - randexp "0.4.6" - semver "^5.4.1" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -7616,7 +7494,7 @@ npmlog@^4.1.2: gauge "~2.7.3" set-blocking "~2.0.0" -nth-check@^1.0.2, nth-check@~1.0.1: +nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== @@ -7672,7 +7550,7 @@ object-inspect@^1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== -object-is@^1.0.1, object-is@^1.0.2: +object-is@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== @@ -7702,7 +7580,7 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.entries@^1.1.0, object.entries@^1.1.1: +object.entries@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== @@ -8025,13 +7903,6 @@ parse5@5.1.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== -parse5@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" - integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== - dependencies: - "@types/node" "*" - parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -8654,6 +8525,16 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= +pretty-format@^25.2.1, pretty-format@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" + integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== + dependencies: + "@jest/types" "^25.5.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-format@^26.0.1: version "26.0.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.0.1.tgz#a4fe54fe428ad2fd3413ca6bbd1ec8c2e277e197" @@ -8711,15 +8592,6 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" -prop-types-exact@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" - integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== - dependencies: - has "^1.0.3" - object.assign "^4.1.0" - reflect.ownkeys "^0.2.0" - prop-types-extra@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" @@ -8857,19 +8729,6 @@ raf@^3.1.0, raf@^3.4.1: dependencies: performance-now "^2.1.0" -railroad-diagrams@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" - integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= - -randexp@0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" - integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== - dependencies: - discontinuous-range "1.0.0" - ret "~0.1.10" - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -9128,7 +8987,7 @@ react-swipeable-views@^0.13.9: react-swipeable-views-utils "^0.13.9" warning "^4.0.1" -react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1: +react-test-renderer@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1" integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ== @@ -9232,7 +9091,7 @@ read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -9271,6 +9130,14 @@ realpath-native@^2.0.0: resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866" integrity sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q== +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + redis-commands@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" @@ -9316,11 +9183,6 @@ redux@^4.0.5: loose-envify "^1.4.0" symbol-observable "^1.2.0" -reflect.ownkeys@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" - integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= - regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -9640,14 +9502,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rst-selector-parser@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" - integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= - dependencies: - lodash.flattendeep "^4.4.0" - nearley "^2.7.10" - rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -9824,7 +9678,7 @@ selfsigned@^1.10.7: dependencies: node-forge "0.9.0" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -10099,7 +9953,7 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-resolve@^0.5.0: +source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== @@ -10391,15 +10245,6 @@ string.prototype.matchall@^4.0.2: regexp.prototype.flags "^1.3.0" side-channel "^1.0.2" -string.prototype.trim@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" - integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - string.prototype.trimend@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" @@ -10485,6 +10330,13 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" -- cgit From 1b198d64890de3eed5562c9b485ed8cafbff059f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 29 Jun 2020 17:59:04 +0200 Subject: Fix trying to write non-existent image remote URL attribute on preview cards (#14181) Regression from #14145 --- app/models/concerns/remotable.rb | 8 +- spec/models/concerns/remotable_spec.rb | 241 +++++++++++++++++---------------- 2 files changed, 128 insertions(+), 121 deletions(-) (limited to 'app') diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 6fc1dcc26..53ebc0835 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -7,8 +7,8 @@ module Remotable def remotable_attachment(attachment_name, limit, suppress_errors: true, download_on_assign: true, attribute_name: nil) attribute_name ||= "#{attachment_name}_remote_url".to_sym - define_method("download_#{attachment_name}!") do - url = self[attribute_name] + define_method("download_#{attachment_name}!") do |url = nil| + url ||= self[attribute_name] return if url.blank? @@ -51,9 +51,9 @@ module Remotable define_method("#{attribute_name}=") do |url| return if self[attribute_name] == url && public_send("#{attachment_name}_file_name").present? - self[attribute_name] = url + self[attribute_name] = url if has_attribute?(attribute_name) - public_send("download_#{attachment_name}!") if download_on_assign + public_send("download_#{attachment_name}!", url) if download_on_assign end alias_method("reset_#{attachment_name}!", "download_#{attachment_name}!") diff --git a/spec/models/concerns/remotable_spec.rb b/spec/models/concerns/remotable_spec.rb index 6957b044f..2e6c8a9c6 100644 --- a/spec/models/concerns/remotable_spec.rb +++ b/spec/models/concerns/remotable_spec.rb @@ -29,170 +29,177 @@ RSpec.describe Remotable do end end - context 'Remotable module is included' do - before do - class Foo - include Remotable - remotable_attachment :hoge, 1.kilobyte - end - end + before do + class Foo + include Remotable - let(:attribute_name) { "#{hoge}_remote_url".to_sym } - let(:code) { 200 } - let(:file) { 'filename="foo.txt"' } - let(:foo) { Foo.new } - let(:headers) { { 'content-disposition' => file } } - let(:hoge) { :hoge } - let(:url) { 'https://google.com' } - - let(:request) do - stub_request(:get, url) - .to_return(status: code, headers: headers) + remotable_attachment :hoge, 1.kilobyte end + end - it 'defines a method #hoge_remote_url=' do - expect(foo).to respond_to(:hoge_remote_url=) - end + let(:attribute_name) { "#{hoge}_remote_url".to_sym } + let(:code) { 200 } + let(:file) { 'filename="foo.txt"' } + let(:foo) { Foo.new } + let(:headers) { { 'content-disposition' => file } } + let(:hoge) { :hoge } + let(:url) { 'https://google.com' } + + it 'defines a method #hoge_remote_url=' do + expect(foo).to respond_to(:hoge_remote_url=) + end - it 'defines a method #reset_hoge!' do - expect(foo).to respond_to(:reset_hoge!) + it 'defines a method #reset_hoge!' do + expect(foo).to respond_to(:reset_hoge!) + end + + it 'defines a method #download_hoge!' do + expect(foo).to respond_to(:download_hoge!) + end + + describe '#hoge_remote_url=' do + before do + stub_request(:get, url).to_return(status: code, headers: headers) end - it 'defines a method #download_hoge!' do - expect(foo).to respond_to(:download_hoge!) + it 'always returns its argument' do + [nil, '', [], {}].each do |arg| + expect(foo.hoge_remote_url = arg).to be arg + end end - describe '#hoge_remote_url=' do + context 'with an invalid URL' do before do - request + allow(Addressable::URI).to receive_message_chain(:parse, :normalize).with(url).with(no_args).and_raise(Addressable::URI::InvalidURIError) end - it 'always returns arg' do - [nil, '', [], {}].each do |arg| - expect(foo.hoge_remote_url = arg).to be arg - end + it 'makes no request' do + foo.hoge_remote_url = url + expect(a_request(:get, url)).to_not have_been_made end + end - context 'Addressable::URI::InvalidURIError raised' do - it 'makes no request' do - allow(Addressable::URI).to receive_message_chain(:parse, :normalize) - .with(url).with(no_args).and_raise(Addressable::URI::InvalidURIError) + context 'with scheme that is neither http nor https' do + let(:url) { 'ftp://google.com' } - foo.hoge_remote_url = url - expect(request).not_to have_been_requested - end + it 'makes no request' do + foo.hoge_remote_url = url + expect(a_request(:get, url)).to_not have_been_made end + end - context 'scheme is neither http nor https' do - let(:url) { 'ftp://google.com' } + context 'with relative URL' do + let(:url) { 'https:///path' } - it 'makes no request' do - foo.hoge_remote_url = url - expect(request).not_to have_been_requested - end + it 'makes no request' do + foo.hoge_remote_url = url + expect(a_request(:get, url)).to_not have_been_made end + end - context 'parsed_url.host is empty' do - it 'makes no request' do - parsed_url = double(scheme: 'https', host: double(blank?: true)) - allow(Addressable::URI).to receive_message_chain(:parse, :normalize) - .with(url).with(no_args).and_return(parsed_url) + context 'when URL has not changed' do + it 'makes no request if file is already saved' do + allow(foo).to receive(:[]).with(attribute_name).and_return(url) + allow(foo).to receive(:hoge_file_name).and_return('foo.jpg') - foo.hoge_remote_url = url - expect(request).not_to have_been_requested - end + foo.hoge_remote_url = url + expect(a_request(:get, url)).to_not have_been_made end - context 'parsed_url.host is nil' do - it 'makes no request' do - parsed_url = Addressable::URI.parse('https:https://example.com/path/file.png') - allow(Addressable::URI).to receive_message_chain(:parse, :normalize) - .with(url).with(no_args).and_return(parsed_url) + it 'makes request if file is not already saved' do + allow(foo).to receive(:[]).with(attribute_name).and_return(url) + allow(foo).to receive(:hoge_file_name).and_return(nil) - foo.hoge_remote_url = url - expect(request).not_to have_been_requested - end + foo.hoge_remote_url = url + expect(a_request(:get, url)).to have_been_made end + end - context 'foo[attribute_name] == url' do - it 'makes no request if file is saved' do - allow(foo).to receive(:[]).with(attribute_name).and_return(url) - allow(foo).to receive(:hoge_file_name).and_return('foo.jpg') + context 'when instance has no attribute for URL' do + before do + allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(false) + end - foo.hoge_remote_url = url - expect(request).not_to have_been_requested - end + it 'does not try to write attribute' do + expect(foo).to_not receive('[]=').with(attribute_name, url) + foo.hoge_remote_url = url + end + end - it 'makes request if file is not saved' do - allow(foo).to receive(:[]).with(attribute_name).and_return(url) - allow(foo).to receive(:hoge_file_name).and_return(nil) + context 'when instance has an attribute for URL' do + before do + allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(true) + end - foo.hoge_remote_url = url - expect(request).to have_been_requested - end + it 'does not try to write attribute' do + expect(foo).to receive('[]=').with(attribute_name, url) + foo.hoge_remote_url = url + end + end + + context 'with a valid URL' do + it 'makes a request' do + foo.hoge_remote_url = url + expect(a_request(:get, url)).to have_been_made end - context "scheme is https, parsed_url.host isn't empty, and foo[attribute_name] != url" do - it 'makes a request' do - foo.hoge_remote_url = url - expect(request).to have_been_requested - end + context 'when the response is not successful' do + let(:code) { 500 } - context 'response.code != 200' do - let(:code) { 500 } + it 'does not assign file' do + expect(foo).not_to receive(:public_send).with("#{hoge}=", any_args) + expect(foo).not_to receive(:public_send).with("#{hoge}_file_name=", any_args) - it 'calls not send' do - expect(foo).not_to receive(:public_send).with("#{hoge}=", any_args) - expect(foo).not_to receive(:public_send).with("#{hoge}_file_name=", any_args) - foo.hoge_remote_url = url - end + foo.hoge_remote_url = url end + end - context 'response.code == 200' do - let(:code) { 200 } + context 'when the response is successful' do + let(:code) { 200 } - context 'response contains headers["content-disposition"]' do - let(:file) { 'filename="foo.txt"' } - let(:headers) { { 'content-disposition' => file } } + context 'and contains Content-Disposition header' do + let(:file) { 'filename="foo.txt"' } + let(:headers) { { 'content-disposition' => file } } - it 'calls send' do - string_io = StringIO.new('') - extname = '.txt' - basename = '0123456789abcdef' + it 'assigns file' do + string_io = StringIO.new('') + extname = '.txt' + basename = '0123456789abcdef' - allow(SecureRandom).to receive(:hex).and_return(basename) - allow(StringIO).to receive(:new).with(anything).and_return(string_io) + allow(SecureRandom).to receive(:hex).and_return(basename) + allow(StringIO).to receive(:new).with(anything).and_return(string_io) - expect(foo).to receive(:public_send).with("download_#{hoge}!") + expect(foo).to receive(:public_send).with("download_#{hoge}!", url) - foo.hoge_remote_url = url + foo.hoge_remote_url = url - expect(foo).to receive(:public_send).with("#{hoge}=", string_io) - expect(foo).to receive(:public_send).with("#{hoge}_file_name=", basename + extname) + expect(foo).to receive(:public_send).with("#{hoge}=", string_io) + expect(foo).to receive(:public_send).with("#{hoge}_file_name=", basename + extname) - foo.download_hoge! - end + foo.download_hoge!(url) end end + end - context 'an error raised during the request' do - let(:request) { stub_request(:get, url).to_raise(error_class) } + context 'when an error is raised during the request' do + before do + stub_request(:get, url).to_raise(error_class) + end - error_classes = [ - HTTP::TimeoutError, - HTTP::ConnectionError, - OpenSSL::SSL::SSLError, - Paperclip::Errors::NotIdentifiedByImageMagickError, - Addressable::URI::InvalidURIError, - ] + error_classes = [ + HTTP::TimeoutError, + HTTP::ConnectionError, + OpenSSL::SSL::SSLError, + Paperclip::Errors::NotIdentifiedByImageMagickError, + Addressable::URI::InvalidURIError, + ] - error_classes.each do |error_class| - let(:error_class) { error_class } + error_classes.each do |error_class| + let(:error_class) { error_class } - it 'calls Rails.logger.debug' do - expect(Rails.logger).to receive(:debug).with(/^Error fetching remote #{hoge}: /) - foo.hoge_remote_url = url - end + it 'calls Rails.logger.debug' do + expect(Rails.logger).to receive(:debug).with(/^Error fetching remote #{hoge}: /) + foo.hoge_remote_url = url end end end -- cgit From 65506bac3f3fe233b5b7b3241020bd74eb5c9259 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 30 Jun 2020 19:19:50 +0200 Subject: Add user notes on accounts (#14148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add UserNote model * Add UI for user notes * Put comment in relationships entity * Add API to create user notes * Copy user notes to new account when receiving a Move activity * Address some of the review remarks * Replace modal by inline edition * Please CodeClimate * Button design changes * Change design again * Cancel note edition when pressing Escape * Fixes * Tweak design again * Move “Add note” item, and allow users to add notes to themselves * Rename UserNote into AccountNote, rename “comment” Relationship attribute to “note” --- .../api/v1/accounts/notes_controller.rb | 30 ++++++ app/javascript/mastodon/actions/account_notes.js | 69 ++++++++++++++ .../features/account/components/account_note.js | 103 +++++++++++++++++++++ .../mastodon/features/account/components/header.js | 11 +++ .../account/containers/account_note_container.js | 34 +++++++ .../features/account_timeline/components/header.js | 6 ++ .../containers/header_container.js | 5 + app/javascript/mastodon/reducers/account_notes.js | 44 +++++++++ app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/reducers/relationships.js | 4 + app/javascript/styles/mastodon/components.scss | 62 ++++++++++++- app/models/account_note.rb | 20 ++++ app/models/concerns/account_interactions.rb | 8 ++ app/presenters/account_relationships_presenter.rb | 6 +- app/serializers/rest/relationship_serializer.rb | 6 +- app/workers/move_worker.rb | 17 ++++ config/locales/en.yml | 2 + config/routes.rb | 1 + db/migrate/20200628133322_create_account_notes.rb | 13 +++ db/schema.rb | 14 ++- spec/fabricators/account_note_fabricator.rb | 5 + spec/workers/move_worker_spec.rb | 27 ++++++ 22 files changed, 485 insertions(+), 4 deletions(-) create mode 100644 app/controllers/api/v1/accounts/notes_controller.rb create mode 100644 app/javascript/mastodon/actions/account_notes.js create mode 100644 app/javascript/mastodon/features/account/components/account_note.js create mode 100644 app/javascript/mastodon/features/account/containers/account_note_container.js create mode 100644 app/javascript/mastodon/reducers/account_notes.js create mode 100644 app/models/account_note.rb create mode 100644 db/migrate/20200628133322_create_account_notes.rb create mode 100644 spec/fabricators/account_note_fabricator.rb (limited to 'app') diff --git a/app/controllers/api/v1/accounts/notes_controller.rb b/app/controllers/api/v1/accounts/notes_controller.rb new file mode 100644 index 000000000..032e807d1 --- /dev/null +++ b/app/controllers/api/v1/accounts/notes_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::NotesController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:accounts' } + before_action :require_user! + before_action :set_account + + def create + if params[:comment].blank? + AccountNote.find_by(account: current_account, target_account: @account)&.destroy + else + @note = AccountNote.find_or_initialize_by(account: current_account, target_account: @account) + @note.comment = params[:comment] + @note.save! if @note.changed? + end + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def relationships_presenter + AccountRelationshipsPresenter.new([@account.id], current_user.account_id) + end +end diff --git a/app/javascript/mastodon/actions/account_notes.js b/app/javascript/mastodon/actions/account_notes.js new file mode 100644 index 000000000..059ed9e80 --- /dev/null +++ b/app/javascript/mastodon/actions/account_notes.js @@ -0,0 +1,69 @@ +import api from '../api'; + +export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST'; +export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS'; +export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL'; + +export const ACCOUNT_NOTE_INIT_EDIT = 'ACCOUNT_NOTE_INIT_EDIT'; +export const ACCOUNT_NOTE_CANCEL = 'ACCOUNT_NOTE_CANCEL'; + +export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT'; + +export function submitAccountNote() { + return (dispatch, getState) => { + dispatch(submitAccountNoteRequest()); + + const id = getState().getIn(['account_notes', 'edit', 'account_id']); + + api(getState).post(`/api/v1/accounts/${id}/note`, { + comment: getState().getIn(['account_notes', 'edit', 'comment']), + }).then(response => { + dispatch(submitAccountNoteSuccess(response.data)); + }).catch(error => dispatch(submitAccountNoteFail(error))); + }; +}; + +export function submitAccountNoteRequest() { + return { + type: ACCOUNT_NOTE_SUBMIT_REQUEST, + }; +}; + +export function submitAccountNoteSuccess(relationship) { + return { + type: ACCOUNT_NOTE_SUBMIT_SUCCESS, + relationship, + }; +}; + +export function submitAccountNoteFail(error) { + return { + type: ACCOUNT_NOTE_SUBMIT_FAIL, + error, + }; +}; + +export function initEditAccountNote(account) { + return (dispatch, getState) => { + const comment = getState().getIn(['relationships', account.get('id'), 'note']); + + dispatch({ + type: ACCOUNT_NOTE_INIT_EDIT, + account, + comment, + }); + }; +}; + +export function cancelAccountNote() { + return { + type: ACCOUNT_NOTE_CANCEL, + }; +}; + +export function changeAccountNoteComment(comment) { + return { + type: ACCOUNT_NOTE_CHANGE_COMMENT, + comment, + }; +}; diff --git a/app/javascript/mastodon/features/account/components/account_note.js b/app/javascript/mastodon/features/account/components/account_note.js new file mode 100644 index 000000000..832a96a6a --- /dev/null +++ b/app/javascript/mastodon/features/account/components/account_note.js @@ -0,0 +1,103 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'mastodon/components/icon'; +import Textarea from 'react-textarea-autosize'; + +const messages = defineMessages({ + placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' }, +}); + +export default @injectIntl +class Header extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + isEditing: PropTypes.bool, + isSubmitting: PropTypes.bool, + accountNote: PropTypes.string, + onEditAccountNote: PropTypes.func.isRequired, + onCancelAccountNote: PropTypes.func.isRequired, + onSaveAccountNote: PropTypes.func.isRequired, + onChangeAccountNote: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleChangeAccountNote = (e) => { + this.props.onChangeAccountNote(e.target.value); + }; + + componentWillUnmount () { + if (this.props.isEditing) { + this.props.onCancelAccountNote(); + } + } + + handleKeyDown = e => { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + this.props.onSaveAccountNote(); + } else if (e.keyCode === 27) { + this.props.onCancelAccountNote(); + } + } + + render () { + const { account, accountNote, isEditing, isSubmitting, intl } = this.props; + + if (!account || (!accountNote && !isEditing)) { + return null; + } + + let action_buttons = null; + if (isEditing) { + action_buttons = ( +
+ +
+ +
+ ); + } + + let note_container = null; + if (isEditing) { + note_container = ( +