Browse Source

Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- README.md
- app/helpers/statuses_helper.rb
  Upstream moved account helpers to their own file, we had extra
  helpers there, moved too.
- app/lib/sanitize_config.rb
- app/models/user.rb
- app/serializers/initial_state_serializer.rb
- config/locales/simple_form.en.yml
- spec/lib/sanitize_config_spec.rb
master
Thibaut Girka 2 weeks ago
parent
commit
41a98b6543
68 changed files with 636 additions and 459 deletions
  1. 1
    0
      .env.vagrant
  2. 10
    0
      .github/stale.yml
  3. 1
    1
      Gemfile
  4. 11
    11
      Gemfile.lock
  5. 1
    0
      app/controllers/activitypub/inboxes_controller.rb
  6. 1
    0
      app/controllers/settings/preferences_controller.rb
  7. 112
    0
      app/helpers/accounts_helper.rb
  8. 0
    109
      app/helpers/statuses_helper.rb
  9. 2
    2
      app/javascript/mastodon/components/attachment_list.js
  10. 1
    1
      app/javascript/mastodon/components/dropdown_menu.js
  11. 1
    1
      app/javascript/mastodon/components/error_boundary.js
  12. 38
    46
      app/javascript/mastodon/components/icon_button.js
  13. 10
    9
      app/javascript/mastodon/components/media_gallery.js
  14. 2
    2
      app/javascript/mastodon/components/status.js
  15. 1
    1
      app/javascript/mastodon/components/status_content.js
  16. 3
    3
      app/javascript/mastodon/features/account/components/header.js
  17. 7
    7
      app/javascript/mastodon/features/account_gallery/components/media_item.js
  18. 10
    0
      app/javascript/mastodon/features/audio/index.js
  19. 1
    1
      app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
  20. 3
    3
      app/javascript/mastodon/features/status/components/card.js
  21. 2
    2
      app/javascript/mastodon/features/status/components/detailed_status.js
  22. 2
    2
      app/javascript/mastodon/features/ui/components/actions_modal.js
  23. 1
    1
      app/javascript/mastodon/features/ui/components/boost_modal.js
  24. 1
    1
      app/javascript/mastodon/features/ui/components/columns_area.js
  25. 1
    1
      app/javascript/mastodon/features/ui/components/link_footer.js
  26. 8
    0
      app/javascript/mastodon/features/video/index.js
  27. 1
    0
      app/javascript/mastodon/initial_state.js
  28. 4
    0
      app/javascript/mastodon/locales/defaultMessages.json
  29. 4
    0
      app/javascript/mastodon/reducers/statuses.js
  30. 63
    0
      app/javascript/styles/mastodon/components.scss
  31. 8
    9
      app/javascript/styles/mastodon/tables.scss
  32. 8
    0
      app/lib/activitypub/activity.rb
  33. 5
    4
      app/lib/activitypub/activity/accept.rb
  34. 6
    4
      app/lib/activitypub/activity/reject.rb
  35. 1
    1
      app/lib/formatter.rb
  36. 1
    1
      app/lib/sanitize_config.rb
  37. 5
    0
      app/lib/user_settings_decorator.rb
  38. 1
    1
      app/mailers/admin_mailer.rb
  39. 1
    0
      app/mailers/notification_mailer.rb
  40. 1
    1
      app/mailers/user_mailer.rb
  41. 1
    0
      app/models/media_attachment.rb
  42. 1
    1
      app/models/user.rb
  43. 2
    0
      app/serializers/initial_state_serializer.rb
  44. 1
    1
      app/serializers/rss/account_serializer.rb
  45. 0
    1
      app/serializers/rss/tag_serializer.rb
  46. 1
    1
      app/services/fetch_link_card_service.rb
  47. 1
    1
      app/views/about/show.html.haml
  48. 1
    1
      app/views/accounts/_moved.html.haml
  49. 3
    0
      app/views/admin/accounts/show.html.haml
  50. 1
    1
      app/views/admin/reports/_status.html.haml
  51. 1
    1
      app/views/admin/tags/index.html.haml
  52. 1
    1
      app/views/admin/tags/show.html.haml
  53. 1
    1
      app/views/application/_card.html.haml
  54. 1
    1
      app/views/oauth/authorized_applications/index.html.haml
  55. 5
    0
      app/views/settings/preferences/appearance/show.html.haml
  56. 2
    2
      app/views/statuses/_detailed_status.html.haml
  57. 2
    2
      app/views/statuses/_simple_status.html.haml
  58. 2
    0
      config/locales/en.yml
  59. 2
    1
      config/locales/simple_form.en.yml
  60. 1
    0
      config/settings.yml
  61. 8
    8
      package.json
  62. 2
    2
      spec/fixtures/xml/mastodon.atom
  63. 67
    0
      spec/helpers/accounts_helper_spec.rb
  64. 1
    1
      spec/helpers/admin/account_moderation_notes_helper_spec.rb
  65. 0
    54
      spec/helpers/statuses_helper_spec.rb
  66. 1
    1
      spec/services/fetch_link_card_service_spec.rb
  67. 2
    2
      spec/services/verify_link_service_spec.rb
  68. 185
    150
      yarn.lock

+ 1
- 0
.env.vagrant View File

@@ -1,2 +1,3 @@
VAGRANT=true
LOCAL_DOMAIN=mastodon.local
BIND=0.0.0.0

+ 10
- 0
.github/stale.yml View File

@@ -0,0 +1,10 @@
daysUntilStale: 120
daysUntilClose: 7
exemptLabels:
- security
staleLabel: wontfix
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
only: pulls

+ 1
- 1
Gemfile View File

@@ -137,7 +137,7 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler'
gem 'rubocop', '~> 0.74', require: false
gem 'rubocop', '~> 0.75', require: false
gem 'rubocop-rails', '~> 2.3', require: false
gem 'brakeman', '~> 4.6', require: false
gem 'bundler-audit', '~> 0.6', require: false

+ 11
- 11
Gemfile.lock View File

@@ -184,17 +184,17 @@ GEM
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
crass (1.0.5)
css_parser (1.7.0)
addressable
debug_inspector (0.0.3)
derailed_benchmarks (1.4.0)
derailed_benchmarks (1.4.1)
benchmark-ips (~> 2)
get_process_mem (~> 0)
heapy (~> 0)
memory_profiler (~> 0)
rack (>= 1)
rake (> 10, < 13)
rake (> 10, < 14)
ruby-statistics (>= 2.1)
thor (~> 0.19)
devise (4.7.1)
@@ -358,7 +358,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.2.3)
loofah (2.3.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -399,7 +399,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.9.1)
oj (3.9.2)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3)
@@ -428,7 +428,7 @@ GEM
parallel (1.17.0)
parallel_tests (2.29.2)
parallel
parser (2.6.4.0)
parser (2.6.5.0)
ast (~> 2.4.0)
parslet (1.8.2)
pastel (0.7.3)
@@ -464,7 +464,7 @@ GEM
rack-attack (6.1.0)
rack (>= 1.0, < 3)
rack-cors (1.0.3)
rack-protection (2.0.5)
rack-protection (2.0.7)
rack
rack-proxy (0.6.5)
rack
@@ -560,7 +560,7 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.8.0)
rubocop (0.74.0)
rubocop (0.75.1)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.6)
@@ -593,7 +593,7 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
sidekiq-unique-jobs (6.0.13)
sidekiq-unique-jobs (6.0.15)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0)
thor (~> 0)
@@ -617,7 +617,7 @@ GEM
sshkit (1.20.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.12)
stackprof (0.2.13)
statsd-ruby (1.4.0)
stoplight (2.1.3)
streamio-ffmpeg (3.0.2)
@@ -782,7 +782,7 @@ DEPENDENCIES
rqrcode (~> 0.10)
rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0)
rubocop (~> 0.74)
rubocop (~> 0.75)
rubocop-rails (~> 2.3)
ruby-progressbar (~> 1.10)
sanitize (~> 5.1)

+ 1
- 0
app/controllers/activitypub/inboxes_controller.rb View File

@@ -7,6 +7,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController

before_action :skip_unknown_actor_delete
before_action :require_signature!
skip_before_action :authenticate_user!

def create
upgrade_account

+ 1
- 0
app/controllers/settings/preferences_controller.rb View File

@@ -60,6 +60,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_use_blurhash,
:setting_use_pending_items,
:setting_trends,
:setting_crop_images,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)

+ 112
- 0
app/helpers/accounts_helper.rb View File

@@ -0,0 +1,112 @@
# frozen_string_literal: true

module AccountsHelper
def display_name(account, **options)
if options[:custom_emojify]
Formatter.instance.format_display_name(account, options)
else
account.display_name.presence || account.username
end
end

def acct(account)
if account.local?
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
else
"@#{account.acct}"
end
end

def account_action_button(account)
if user_signed_in?
if account.id == current_user.account_id
link_to settings_profile_url, class: 'button logo-button' do
safe_join([svg_logo, t('settings.edit_profile')])
end
elsif current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
safe_join([svg_logo, t('accounts.unfollow')])
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
safe_join([svg_logo, t('accounts.follow')])
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
safe_join([svg_logo, t('accounts.follow')])
end
end
end

def minimal_account_action_button(account)
if user_signed_in?
return if account.id == current_user.account_id

if current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
fa_icon('user-times fw')
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
end

def account_badge(account, all: false)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all
content_tag(:div, class: 'roles') do
if all && !account.user_staff?
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
elsif account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
end
end

def hide_followers_count?(account)
Setting.hide_followers_count || account.user&.setting_hide_followers_count
end

def account_description(account)
prepend_stats = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),

[
number_to_human(account.following_count, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
]

unless hide_followers_count?(account)
prepend_stats << [
number_to_human(account.followers_count, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' ')
end

[prepend_stats.join(', '), account.note].join(' · ')
end

def svg_logo
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
end

def svg_logo_full
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
end
end

+ 0
- 109
app/helpers/statuses_helper.rb View File

@@ -4,80 +4,6 @@ module StatusesHelper
EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed'

def display_name(account, **options)
if options[:custom_emojify]
Formatter.instance.format_display_name(account, options)
else
account.display_name.presence || account.username
end
end

def account_action_button(account)
if user_signed_in?
if account.id == current_user.account_id
link_to settings_profile_url, class: 'button logo-button' do
safe_join([svg_logo, t('settings.edit_profile')])
end
elsif current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
safe_join([svg_logo, t('accounts.unfollow')])
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
safe_join([svg_logo, t('accounts.follow')])
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
safe_join([svg_logo, t('accounts.follow')])
end
end
end

def minimal_account_action_button(account)
if user_signed_in?
return if account.id == current_user.account_id

if current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
fa_icon('user-times fw')
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
end

def svg_logo
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
end

def svg_logo_full
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
end

def account_badge(account, all: false)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all
content_tag(:div, class: 'roles') do
if all && !account.user_staff?
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
elsif account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
end
end

def link_to_more(url)
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
end
@@ -88,33 +14,6 @@ module StatusesHelper
end
end

def hide_followers_count?(account)
Setting.hide_followers_count || account.user&.setting_hide_followers_count
end

def account_description(account)
prepend_stats = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),

[
number_to_human(account.following_count, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
]

unless hide_followers_count?(account)
prepend_stats << [
number_to_human(account.followers_count, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' ')
end

[prepend_stats.join(', '), account.note].join(' · ')
end

def media_summary(status)
attachments = { image: 0, video: 0 }

@@ -160,14 +59,6 @@ module StatusesHelper
embedded_view? ? '_blank' : nil
end

def acct(account)
if account.local?
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
else
"@#{account.acct}"
end
end

def style_classes(status, is_predecessor, is_successor, include_threads)
classes = ['entry']
classes << 'entry-predecessor' if is_predecessor

+ 2
- 2
app/javascript/mastodon/components/attachment_list.js View File

@@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {

return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a>
</li>
);
})}
@@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent {

return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a>
</li>
);
})}

+ 1
- 1
app/javascript/mastodon/components/dropdown_menu.js View File

@@ -143,7 +143,7 @@ class DropdownMenu extends React.PureComponent {

return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>

+ 1
- 1
app/javascript/mastodon/components/error_boundary.js View File

@@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {
<div>
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div>
</div>
);

+ 38
- 46
app/javascript/mastodon/components/icon_button.js View File

@@ -1,6 +1,4 @@
import React from 'react';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
@@ -37,6 +35,21 @@ export default class IconButton extends React.PureComponent {
tabIndex: '0',
};

state = {
activate: false,
deactivate: false,
}

componentWillReceiveProps (nextProps) {
if (!nextProps.animate) return;

if (this.props.active && !nextProps.active) {
this.setState({ activate: false, deactivate: true });
} else if (!this.props.active && nextProps.active) {
this.setState({ activate: true, deactivate: false });
}
}

handleClick = (e) => {
e.preventDefault();

@@ -75,7 +88,6 @@ export default class IconButton extends React.PureComponent {

const {
active,
animate,
className,
disabled,
expanded,
@@ -87,57 +99,37 @@ export default class IconButton extends React.PureComponent {
title,
} = this.props;

const {
activate,
deactivate,
} = this.state;

const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
activate,
deactivate,
overlayed: overlay,
});

if (!animate) {
// Perf optimization: avoid unnecessary <Motion> components unless
// we actually need to animate.
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}

return (
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) => (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button>
)}
</Motion>
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}


+ 10
- 9
app/javascript/mastodon/components/media_gallery.js View File

@@ -6,7 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
import classNames from 'classnames';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
import { decode } from 'blurhash';

const messages = defineMessages({
@@ -159,7 +159,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') {
return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a>
</div>
@@ -187,6 +187,7 @@ class Item extends React.PureComponent {
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
rel='noopener noreferrer'
>
<img
src={previewUrl}
@@ -280,7 +281,7 @@ class MediaGallery extends React.PureComponent {
}

handleRef = (node) => {
if (node /*&& this.isStandaloneEligible()*/) {
if (node) {
// offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);

@@ -290,13 +291,13 @@ class MediaGallery extends React.PureComponent {
}
}

isStandaloneEligible() {
const { media, standalone } = this.props;
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
isFullSizeEligible() {
const { media } = this.props;
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
}

render () {
const { media, intl, sensitive, height, defaultWidth } = this.props;
const { media, intl, sensitive, height, defaultWidth, standalone } = this.props;
const { visible } = this.state;

const width = this.state.width || defaultWidth;
@@ -305,7 +306,7 @@ class MediaGallery extends React.PureComponent {

const style = {};

if (this.isStandaloneEligible()) {
if (this.isFullSizeEligible() && (standalone || !cropImages)) {
if (width) {
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
}
@@ -318,7 +319,7 @@ class MediaGallery extends React.PureComponent {
const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown');

if (this.isStandaloneEligible()) {
if (standalone && this.isFullSizeEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />);

+ 2
- 2
app/javascript/mastodon/components/status.js View File

@@ -437,9 +437,9 @@ class Status extends ImmutablePureComponent {
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>

<a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>

+ 1
- 1
app/javascript/mastodon/components/status_content.js View File

@@ -59,7 +59,7 @@ export default class StatusContent extends React.PureComponent {
}

link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
link.setAttribute('rel', 'noopener noreferrer');
}

if (

+ 3
- 3
app/javascript/mastodon/features/account/components/header.js View File

@@ -253,7 +253,7 @@ class Header extends ImmutablePureComponent {

<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener' target='_blank'>
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
<Avatar account={account} size={90} />
</a>

@@ -282,10 +282,10 @@ class Header extends ImmutablePureComponent {
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />

<dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' />
</span></a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
</dd>
</dl>
))}

+ 7
- 7
app/javascript/mastodon/features/account_gallery/components/media_item.js View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { decode } from 'blurhash';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
import classNames from 'classnames';
import { decode } from 'blurhash';
import { isIOS } from 'mastodon/is_mobile';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';

export default class MediaItem extends ImmutablePureComponent {

@@ -151,7 +151,7 @@ export default class MediaItem extends ImmutablePureComponent {

return (
<div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
{visible && thumbnail}
{!visible && icon}

+ 10
- 0
app/javascript/mastodon/features/audio/index.js View File

@@ -12,6 +12,7 @@ const messages = defineMessages({
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
download: { id: 'video.download', defaultMessage: 'Download file' },
});

export default @injectIntl
@@ -202,6 +203,7 @@ class Audio extends React.PureComponent {
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>

<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />

<span
@@ -217,6 +219,14 @@ class Audio extends React.PureComponent {
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
</span>
</div>

<div className='video-player__buttons right'>
<button type='button' aria-label={intl.formatMessage(messages.download)}>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</button>
</div>
</div>
</div>
</div>

+ 1
- 1
app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/lib/Async';
import AsyncSelect from 'react-select/async';

const messages = defineMessages({
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },

+ 3
- 3
app/javascript/mastodon/features/status/components/card.js View File

@@ -148,7 +148,7 @@ export default class Card extends React.PureComponent {
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const ratio = card.get('width') / card.get('height');
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);

@@ -180,7 +180,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__actions'>
<div>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>}
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
</div>
</div>
</div>
@@ -208,7 +208,7 @@ export default class Card extends React.PureComponent {
}

return (
<a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
<a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
{embed}
{description}
</a>

+ 2
- 2
app/javascript/mastodon/features/status/components/detailed_status.js View File

@@ -156,7 +156,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
}

if (status.get('application')) {
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>;
}

if (status.get('visibility') === 'direct') {
@@ -220,7 +220,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
{media}

<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{applicationLink} · {reblogLink} · {favouriteLink}
</div>

+ 2
- 2
app/javascript/mastodon/features/ui/components/actions_modal.js View File

@@ -26,7 +26,7 @@ export default class ActionsModal extends ImmutablePureComponent {

return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
@@ -42,7 +42,7 @@ export default class ActionsModal extends ImmutablePureComponent {
<div className='status light'>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<RelativeTimestamp timestamp={this.props.status.get('created_at')} />
</a>
</div>

+ 1
- 1
app/javascript/mastodon/features/ui/components/boost_modal.js View File

@@ -61,7 +61,7 @@ class BoostModal extends ImmutablePureComponent {
<div className='status light'>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>

<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>

+ 1
- 1
app/javascript/mastodon/features/ui/components/columns_area.js View File

@@ -182,7 +182,7 @@ class ColumnsArea extends ImmutablePureComponent {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;

const content = columnIndex !== -1 ? (
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
{links.map(this.renderView)}
</ReactSwipeableViews>
) : (

+ 1
- 1
app/javascript/mastodon/features/ui/components/link_footer.js View File

@@ -62,7 +62,7 @@ class LinkFooter extends React.PureComponent {
<FormattedMessage
id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
values={{ github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }}
/>
</p>
</div>

+ 8
- 0
app/javascript/mastodon/features/video/index.js View File

@@ -19,6 +19,7 @@ const messages = defineMessages({
close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
download: { id: 'video.download', defaultMessage: 'Download file' },
});

export const formatTime = secondsNum => {
@@ -470,6 +471,7 @@ class Video extends React.PureComponent {
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>

<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span
className={classNames('video-player__volume__handle')}
@@ -493,7 +495,13 @@ class Video extends React.PureComponent {
{(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
<button type='button' aria-label={intl.formatMessage(messages.download)}>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</button>
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>

</div>
</div>
</div>

+ 1
- 0
app/javascript/mastodon/initial_state.js View File

@@ -25,5 +25,6 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const showTrends = getMeta('trends');
export const title = getMeta('title');
export const cropImages = getMeta('crop_images');

export default initialState;

+ 4
- 0
app/javascript/mastodon/locales/defaultMessages.json View File

@@ -776,6 +776,10 @@
{
"defaultMessage": "Unmute sound",
"id": "video.unmute"
},
{
"defaultMessage": "Download file",
"id": "video.download"
}
],
"path": "app/javascript/mastodon/features/audio/index.json"

+ 4
- 0
app/javascript/mastodon/reducers/statuses.js View File

@@ -3,6 +3,7 @@ import {
REBLOG_FAIL,
FAVOURITE_REQUEST,
FAVOURITE_FAIL,
UNFAVOURITE_SUCCESS,
} from '../actions/interactions';
import {
STATUS_MUTE_SUCCESS,
@@ -37,6 +38,9 @@ export default function statuses(state = initialState, action) {
return importStatuses(state, action.statuses);
case FAVOURITE_REQUEST:
return state.setIn([action.status.get('id'), 'favourited'], true);
case UNFAVOURITE_SUCCESS:
const favouritesCount = action.status.get('favourites_count');
return state.setIn([action.status.get('id'), 'favourites_count'], favouritesCount - 1);
case FAVOURITE_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
case REBLOG_REQUEST:

+ 63
- 0
app/javascript/styles/mastodon/components.scss View File

@@ -1591,6 +1591,20 @@ a.account__display-name {
color: $gold-star;
}

.no-reduce-motion .icon-button.star-icon {
&.activate {
& > .fa-star {
animation: spring-rotate-in 1s linear;
}
}

&.deactivate {
& > .fa-star {
animation: spring-rotate-out 1s linear;
}
}
}

.notification__display-name {
color: inherit;
font-weight: 500;
@@ -3373,6 +3387,50 @@ a.status-card.compact:hover {
animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}

@keyframes spring-rotate-in {
0% {
transform: rotate(0deg);
}

30% {
transform: rotate(-484.8deg);
}

60% {
transform: rotate(-316.7deg);
}

90% {
transform: rotate(-375deg);
}

100% {
transform: rotate(-360deg);
}
}

@keyframes spring-rotate-out {
0% {
transform: rotate(-360deg);
}

30% {
transform: rotate(124.8deg);
}

60% {
transform: rotate(-43.27deg);
}

90% {
transform: rotate(15deg);
}

100% {
transform: rotate(0deg);
}
}

@keyframes loader-figure {
0% {
width: 0;
@@ -5194,6 +5252,7 @@ a.status-card.compact:hover {
max-height: 100% !important;
width: 100% !important;
height: 100% !important;
outline: 0;
}
}

@@ -5271,6 +5330,10 @@ a.status-card.compact:hover {
display: flex;
justify-content: space-between;
padding-bottom: 10px;

.video-player__download__icon {
color: inherit;
}
}

&__buttons {

+ 8
- 9
app/javascript/styles/mastodon/tables.scss View File

@@ -149,10 +149,6 @@ a.table-action-link {
margin-top: 0;
}
}

@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}

&__actions,
@@ -174,10 +170,6 @@ a.table-action-link {
text-align: right;
padding-right: 16px - 5px;
}

@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}

&__form {
@@ -198,7 +190,7 @@ a.table-action-link {
background: darken($ui-base-color, 4%);

@media screen and (max-width: $no-gap-breakpoint) {
&:first-child {
.optional &:first-child {
border-top: 1px solid darken($ui-base-color, 8%);
}
}
@@ -264,6 +256,13 @@ a.table-action-link {
}
}

&.optional .batch-table__toolbar,
&.optional .batch-table__row__select {
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}

.status__content {
padding-top: 0;


+ 8
- 0
app/lib/activitypub/activity.rb View File

@@ -153,6 +153,14 @@ class ActivityPub::Activity
fetch_remote_original_status
end

def follow_request_from_object
@follow_request ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end

def follow_from_object
@follow ||= Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end

def fetch_remote_original_status
if object_uri.start_with?('http')
return if ActivityPub::TagManager.instance.local_uri?(object_uri)

+ 5
- 4
app/lib/activitypub/activity/accept.rb View File

@@ -2,17 +2,18 @@

class ActivityPub::Activity::Accept < ActivityPub::Activity
def perform
return accept_follow_for_relay if relay_follow?
return follow_request_from_object.authorize! unless follow_request_from_object.nil?

case @object['type']
when 'Follow'
accept_follow
accept_embedded_follow
end
end

private

def accept_follow
return accept_follow_for_relay if relay_follow?

def accept_embedded_follow
target_account = account_from_uri(target_uri)

return if target_account.nil? || !target_account.local?

+ 6
- 4
app/lib/activitypub/activity/reject.rb View File

@@ -2,17 +2,19 @@

class ActivityPub::Activity::Reject < ActivityPub::Activity
def perform
return reject_follow_for_relay if relay_follow?
return follow_request_from_object.reject! unless follow_request_from_object.nil?
return UnfollowService.new.call(follow_from_object.target_account, @account) unless follow_from_object.nil?

case @object['type']
when 'Follow'
reject_follow
reject_embedded_follow
end
end

private

def reject_follow
return reject_follow_for_relay if relay_follow?

def reject_embedded_follow
target_account = account_from_uri(target_uri)

return if target_account.nil? || !target_account.local?

+ 1
- 1
app/lib/formatter.rb View File

@@ -337,7 +337,7 @@ class Formatter

def link_to_url(entity, options = {})
url = Addressable::URI.parse(entity[:url])
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }

html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]


+ 1
- 1
app/lib/sanitize_config.rb View File

@@ -50,7 +50,7 @@ class Sanitize

add_attributes: {
'a' => {
'rel' => 'nofollow noopener tag',
'rel' => 'nofollow noopener tag noreferrer',
'target' => '_blank',
},
},

+ 5
- 0
app/lib/user_settings_decorator.rb View File

@@ -42,6 +42,7 @@ class UserSettingsDecorator
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
user.settings['trends'] = trends_preference if change?('setting_trends')
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
end

def merged_notification_emails
@@ -152,6 +153,10 @@ class UserSettingsDecorator
boolean_cast_setting 'setting_trends'
end

def crop_images_preference
boolean_cast_setting 'setting_crop_images'
end

def boolean_cast_setting(key)
ActiveModel::Type::Boolean.new.cast(settings[key])
end

+ 1
- 1
app/mailers/admin_mailer.rb View File

@@ -3,7 +3,7 @@
class AdminMailer < ApplicationMailer
layout 'plain_mailer'

helper :statuses
helper :accounts

def new_report(recipient, report)
@report = report

+ 1
- 0
app/mailers/notification_mailer.rb View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true

class NotificationMailer < ApplicationMailer
helper :accounts
helper :statuses

add_template_helper RoutingHelper

+ 1
- 1
app/mailers/user_mailer.rb View File

@@ -3,9 +3,9 @@
class UserMailer < Devise::Mailer
layout 'mailer'

helper :accounts
helper :application
helper :instance
helper :statuses

add_template_helper RoutingHelper


+ 1
- 0
app/models/media_attachment.rb View File

@@ -87,6 +87,7 @@ class MediaAttachment < ApplicationRecord
convert_options: {
output: {
'loglevel' => 'fatal',
'map_metadata' => '-1',
'q:a' => 2,
},
},

+ 1
- 1
app/models/user.rb View File

@@ -108,7 +108,7 @@ class User < ApplicationRecord
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
:advanced_layout, :use_blurhash, :use_pending_items, :trends,
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
:default_content_type, :system_emoji_font,
to: :settings, prefix: :setting, allow_nil: false


+ 2
- 0
app/serializers/initial_state_serializer.rb View File

@@ -55,11 +55,13 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:trends] = Setting.trends && object.current_account.user.setting_trends
store[:default_content_type] = object.current_account.user.setting_default_content_type
store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
store[:crop_images] = object.current_account.user.setting_crop_images
else
store[:auto_play_gif] = Setting.auto_play_gif
store[:display_media] = Setting.display_media
store[:reduce_motion] = Setting.reduce_motion
store[:use_blurhash] = Setting.use_blurhash
store[:crop_images] = Setting.crop_images
end

store

+ 1
- 1
app/serializers/rss/account_serializer.rb View File

@@ -2,7 +2,7 @@

class RSS::AccountSerializer
include ActionView::Helpers::NumberHelper
include StatusesHelper
include AccountsHelper
include RoutingHelper

def render(account, statuses, tag)

+ 0
- 1
app/serializers/rss/tag_serializer.rb View File

@@ -3,7 +3,6 @@
class RSS::TagSerializer
include ActionView::Helpers::NumberHelper
include ActionView::Helpers::SanitizeHelper
include StatusesHelper
include RoutingHelper

def render(tag, statuses)

+ 1
- 1
app/services/fetch_link_card_service.rb View File

@@ -84,7 +84,7 @@ class FetchLinkCardService < BaseService

def skip_link?(a)
# Avoid links for hashtags and mentions (microformats)
a['rel']&.include?('tag') || a['class']&.include?('u-url') || mention_link?(a)
a['rel']&.include?('tag') || a['class']&.match?(/u-url|h-card/) || mention_link?(a)
end

def attempt_oembed

+ 1
- 1
app/views/about/show.html.haml View File

@@ -38,7 +38,7 @@
%small= t('about.browse_public_posts')

.directory__tag
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' do
%h4
= fa_icon 'tablet fw'
= t('about.get_apps')

+ 1
- 1
app/views/accounts/_moved.html.haml View File

@@ -6,7 +6,7 @@
= t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention'))

.moved-account-widget__card
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener' do
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
.detailed-status__display-avatar
.account__avatar-overlay
.account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }

+ 3
- 0
app/views/admin/accounts/show.html.haml View File

@@ -143,12 +143,15 @@
%th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip
%td
- if @account.user_current_sign_in_ip
= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: @account.user_current_sign_in_ip)

%tr
%th= t('admin.accounts.most_recent_activity')
%td
- if @account.user_current_sign_in_at
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }= l @account.user_current_sign_in_at
%td

- if @account.user&.invited?
%tr

+ 1
- 1
app/views/admin/reports/_status.html.haml View File

@@ -19,7 +19,7 @@
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }

.detailed-status__meta
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
- if status.discarded?
·

+ 1
- 1
app/views/admin/tags/index.html.haml View File

@@ -45,7 +45,7 @@
- Admin::FilterHelper::TAGS_FILTERS.each do |key|
= hidden_field_tag key, params[key] if params[key].present?

.batch-table
.batch-table.optional
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false

+ 1
- 1
app/views/admin/tags/show.html.haml View File

@@ -3,7 +3,7 @@

.dashboard__counters
%div
= link_to tag_url(@tag), target: '_blank', rel: 'noopener' do
= link_to tag_url(@tag), target: '_blank', rel: 'noopener noreferrer' do
.dashboard__counters__num= number_with_delimiter @accounts_today
.dashboard__counters__label= t 'admin.tags.accounts_today'
%div

+ 1
- 1
app/views/application/_card.html.haml View File

@@ -1,7 +1,7 @@
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account)

.card.h-card
= link_to account_url, target: '_blank', rel: 'noopener' do
= link_to account_url, target: '_blank', rel: 'noopener noreferrer' do
.card__img
= image_tag account.header.url, alt: ''
.card__bar

+ 1
- 1
app/views/oauth/authorized_applications/index.html.haml View File

@@ -16,7 +16,7 @@
- if application.website.blank?
= application.name
- else
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer'
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ')
%td= l application.created_at
%td

+ 5
- 0
app/views/settings/preferences/appearance/show.html.haml View File

@@ -23,6 +23,11 @@
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
= f.input :setting_system_emoji_font, as: :boolean, wrapper: :with_label

%h4= t 'appearance.toot_layout'

.fields-group
= f.input :setting_crop_images, as: :boolean, wrapper: :with_label

%h4= t 'appearance.discovery'

.fields-group

+ 2
- 2
app/views/statuses/_detailed_status.html.haml View File

@@ -44,14 +44,14 @@
.detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 }

= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
·
- if status.application && @account.user&.setting_show_application
- if status.application.website.blank?
%strong.detailed-status__application= status.application.name
- else
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
·
= link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
- if status.in_reply_to_id.nil?

+ 2
- 2
app/views/statuses/_simple_status.html.haml View File

@@ -1,11 +1,11 @@
.status
.status__info
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
%time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
%data.dt-published{ value: status.created_at.to_time.iso8601 }

.p-author.h-card
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
.status__avatar
%div
- if current_account&.user&.setting_auto_play_gif || autoplay

+ 2
- 0
config/locales/en.yml View File

@@ -175,6 +175,7 @@ en:
user: User
salmon_url: Salmon URL
search: Search
search_same_ip: Other users with the same IP
shared_inbox_url: Shared inbox URL
show:
created_reports: Made reports
@@ -588,6 +589,7 @@ en:
confirmation_dialogs: Confirmation dialogs
discovery: Discovery
sensitive_content: Sensitive content
toot_layout: Toot layout
application_mailer:
notification_preferences: Change e-mail preferences
salutation: "%{name},"

+ 2
- 1
config/locales/simple_form.en.yml View File

@@ -83,7 +83,7 @@ en:
text: Custom warning
type: Action
types:
disable: Disable
disable: Disable login
none: Do nothing
silence: Silence
suspend: Suspend and irreversibly delete account data
@@ -118,6 +118,7 @@ en:
setting_aggregate_reblogs: Group boosts in timelines
setting_auto_play_gif: Auto-play animated GIFs
setting_boost_modal: Show confirmation dialog before boosting
setting_crop_images: Crop images in non-expanded toots to 16x9
setting_default_content_type: Default format for toots
setting_default_content_type_html: HTML
setting_default_content_type_markdown: Markdown

+ 1
- 0
config/settings.yml View File

@@ -41,6 +41,7 @@ defaults: &defaults
use_pending_items: false
trends: true
trendable_by_default: false
crop_images: true
notification_emails:
follow: false
reblog: false

+ 8
- 8
package.json View File

@@ -72,7 +72,7 @@
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.5.4",
"@clusterws/cws": "^0.15.0",
"@clusterws/cws": "^0.15.2",
"array-includes": "^3.0.3",
"atrament": "^0.2.3",
"autoprefixer": "^9.6.1",
@@ -87,7 +87,7 @@
"classnames": "^2.2.5",
"compression-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.0.4",
"cross-env": "^5.1.4",
"cross-env": "^6.0.3",
"css-loader": "^3.2.0",
"cssnano": "^4.1.10",
"detect-passive-events": "^1.0.2",
@@ -128,8 +128,8 @@
"prop-types": "^15.5.10",
"punycode": "^2.1.0",
"rails-ujs": "^5.2.3",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react": "^16.10.2",
"react-dom": "^16.10.2",
"react-hotkeys": "^1.1.4",
"react-immutable-proptypes": "^2.1.0",
"react-immutable-pure-component": "^1.1.1",
@@ -142,7 +142,7 @@
"react-redux-loading-bar": "^4.0.8",
"react-router-dom": "^4.1.1",
"react-router-scroll-4": "^1.0.0-beta.1",
"react-select": "^2.4.4",
"react-select": "^3.0.5",
"react-sparklines": "^1.7.0",
"react-swipeable-views": "^0.13.3",
"react-textarea-autosize": "^7.1.0",
@@ -155,7 +155,7 @@
"requestidlecallback": "^0.3.0",
"reselect": "^4.0.0",
"rimraf": "^3.0.0",
"sass": "^1.22.12",
"sass": "^1.23.0",
"sass-loader": "^7.0.3",
"stringz": "^2.0.0",
"substring-trie": "^1.0.2",
@@ -177,7 +177,7 @@
"babel-jest": "^24.9.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"eslint": "^6.5.0",
"eslint": "^6.5.1",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jsx-a11y": "~6.2.3",
"eslint-plugin-promise": "~4.2.1",
@@ -185,7 +185,7 @@
"jest": "^24.9.0",
"raf": "^3.4.1",
"react-intl-translations-manager": "^5.0.3",
"react-test-renderer": "^16.8.6",
"react-test-renderer": "^16.10.2",
"sass-lint": "^1.13.1",
"webpack-dev-server": "^3.8.0",
"yargs": "^13.3.0"

+ 2
- 2
spec/fixtures/xml/mastodon.atom View File

@@ -123,7 +123,7 @@
<published>2016-10-10T00:41:31Z</published>
<updated>2016-10-10T00:41:31Z</updated>
<title>Social media needs MOAR cats! http://kickass.zone/media/3</title>
<content type="html">&lt;p&gt;Social media needs MOAR cats! &lt;a rel="nofollow noopener" href="http://kickass.zone/media/3"&gt;http://kickass.zone/media/3&lt;/a&gt;&lt;/p&gt;</content>
<content type="html">&lt;p&gt;Social media needs MOAR cats! &lt;a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/3"&gt;http://kickass.zone/media/3&lt;/a&gt;&lt;/p&gt;</content>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<link rel="self" type="application/atom+xml" href="http://kickass.zone/users/localhost/updates/9.atom"/>
<link rel="alternate" type="text/html" href="http://kickass.zone/users/localhost/updates/9"/>
@@ -135,7 +135,7 @@
<published>2016-10-10T00:38:39Z</published>
<updated>2016-10-10T00:38:39Z</updated>
<title>http://kickass.zone/media/2</title>
<content type="html">&lt;p&gt;&lt;a rel="nofollow noopener" href="http://kickass.zone/media/2"&gt;http://kickass.zone/media/2&lt;/a&gt;&lt;/p&gt;</content>
<content type="html">&lt;p&gt;&lt;a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/2"&gt;http://kickass.zone/media/2&lt;/a&gt;&lt;/p&gt;</content>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<link rel="self" type="application/atom+xml" href="http://kickass.zone/users/localhost/updates/8.atom"/>
<link rel="alternate" type="text/html" href="http://kickass.zone/users/localhost/updates/8"/>

+ 67
- 0
spec/helpers/accounts_helper_spec.rb View File

@@ -0,0 +1,67 @@
require 'rails_helper'

RSpec.describe AccountsHelper, type: :helper do
def set_not_embedded_view
params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}"
params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}"
end

def set_embedded_view
params[:controller] = StatusesHelper::EMBEDDED_CONTROLLER
params[:action] = StatusesHelper::EMBEDDED_ACTION
end

describe '#display_name' do
it 'uses the display name when it exists' do
account = Account.new(display_name: "Display", username: "Username")

expect(helper.display_name(account)).to eq "Display"
end

it 'uses the username when display name is nil' do
account = Account.new(display_name: nil, username: "Username")

expect(helper.display_name(account)).to eq "Username"
end
end

describe '#acct' do
it 'is fully qualified for embedded local accounts' do
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
set_embedded_view
account = Account.new(domain: nil, username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@local_domain'
end

it 'is fully qualified for embedded foreign accounts' do
set_embedded_view
account = Account.new(domain: 'foreign_server.com', username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@foreign_server.com'
end

it 'is fully qualified for non embedded foreign accounts' do
set_not_embedded_view
account = Account.new(domain: 'foreign_server.com', username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@foreign_server.com'
end

it 'is fully qualified for non embedded local accounts' do
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
set_not_embedded_view
account = Account.new(domain: nil, username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@local_domain'
end
end
end

+ 1
- 1
spec/helpers/admin/account_moderation_notes_helper_spec.rb View File

@@ -3,7 +3,7 @@
require 'rails_helper'

RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do
include StatusesHelper
include AccountsHelper

describe '#admin_account_link_to' do
context 'account is nil' do

+ 0
- 54
spec/helpers/statuses_helper_spec.rb View File

@@ -1,20 +1,6 @@
require 'rails_helper'

RSpec.describe StatusesHelper, type: :helper do
describe '#display_name' do
it 'uses the display name when it exists' do
account = Account.new(display_name: "Display", username: "Username")

expect(helper.display_name(account)).to eq "Display"
end

it 'uses the username when display name is nil' do
account = Account.new(display_name: nil, username: "Username")

expect(helper.display_name(account)).to eq "Username"
end
end

describe '#stream_link_target' do
it 'returns nil if it is not an embedded view' do
set_not_embedded_view
@@ -29,46 +15,6 @@ RSpec.describe StatusesHelper, type: :helper do
end
end

describe '#acct' do
it 'is fully qualified for embedded local accounts' do
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
set_embedded_view
account = Account.new(domain: nil, username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@local_domain'
end

it 'is fully qualified for embedded foreign accounts' do
set_embedded_view
account = Account.new(domain: 'foreign_server.com', username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@foreign_server.com'
end

it 'is fully qualified for non embedded foreign accounts' do
set_not_embedded_view
account = Account.new(domain: 'foreign_server.com', username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@foreign_server.com'
end

it 'is fully qualified for non embedded local accounts' do
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
set_not_embedded_view
account = Account.new(domain: nil, username: 'user')

acct = helper.acct(account)

expect(acct).to eq '@user@local_domain'
end
end

def set_not_embedded_view
params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}"
params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}"

+ 1
- 1
spec/services/fetch_link_card_service_spec.rb View File

@@ -80,7 +80,7 @@ RSpec.describe FetchLinkCardService, type: :service do
end

context 'in a remote status' do
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener noreferrer" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener noreferrer" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener noreferrer" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }

it 'parses out URLs' do
expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once

+ 2
- 2
spec/services/verify_link_service_spec.rb View File

@@ -28,12 +28,12 @@ RSpec.describe VerifyLinkService, type: :service do
end
end

context 'when a link contains an <a rel="noopener"> back' do
context 'when a link contains an <a rel="noopener noreferrer"> back' do
let(:html) do
<<-HTML
<!doctype html>
<body>
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="noopener me" target="_blank">Follow me on Mastodon</a>
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me noopener noreferrer" target="_blank">Follow me on Mastodon</a>
</body>
HTML
end

+ 185
- 150
yarn.lock View File

@@ -782,10 +782,10 @@
dependencies:
regenerator-runtime "^0.12.0"

"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205"
integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==
dependencies:
regenerator-runtime "^0.13.2"

@@ -831,10 +831,10 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"

"@clusterws/cws@^0.15.0":
version "0.15.0"
resolved "https://registry.yarnpkg.com/@clusterws/cws/-/cws-0.15.0.tgz#1d585927252d1cd92e676c952fa6d69df14a0d07"
integrity sha512-41QpCngw86n41hIdU5Nx2QJmmxZuA9FPtDkjONrYpk27L7HjL1kj6J5oWEjbr14yXLfigZit3VY+oACDCGbiHw==
"@clusterws/cws@^0.15.2":
version "0.15.2"
resolved "https://registry.yarnpkg.com/@clusterws/cws/-/cws-0.15.2.tgz#b4cc9d224992d939cf996fca24c655e34a1f5c94"
integrity sha512-nTXrmbv8IK38VBvfhdhigrxC7GvN4fsDrImCGx/b8jb3AP34e5B23cfXQCxZ2H3M3rywWbkeNdX9bGc01nX2rw==

"@cnakazawa/watch@^1.0.3":
version "1.0.3"
@@ -844,52 +844,82 @@
exec-sh "^0.3.2"
minimist "^1.2.0"

"@emotion/babel-utils@^0.6.4":
version "0.6.10"
resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc"
integrity sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==
"@emotion/cache@^10.0.17", "@emotion/cache@^10.0.9":
version "10.0.19"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.19.tgz#d258d94d9c707dcadaf1558def968b86bb87ad71"
integrity sha512-BoiLlk4vEsGBg2dAqGSJu0vJl/PgVtCYLBFJaEO8RmQzPugXewQCXZJNXTDFaRlfCs0W+quesayav4fvaif5WQ==
dependencies:
"@emotion/hash" "^0.6.6"
"@emotion/memoize" "^0.6.6"
"@emotion/serialize" "^0.9.1"
convert-source-map "^1.5.1"
find-root "^1.1.0"
source-map "^0.7.2"

"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44"
integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==

"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b"
integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==

"@emotion/serialize@^0.9.1":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145"
integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==
dependencies:
"@emotion/hash" "^0.6.6"
"@emotion/memoize" "^0.6.6"
"@emotion/unitless" "^0.6.7"
"@emotion/utils" "^0.8.2"

"@emotion/stylis@^0.7.0":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5"
integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==

"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7":
version "0.6.7"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397"
integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==

"@emotion/utils@^0.8.2":
version "0.8.2"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc"
integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==
"@emotion/sheet" "0.9.3"
"@emotion/stylis" "0.8.4"
"@emotion/utils" "0.11.2"
"@emotion/weak-memoize" "0.2.4"

"@emotion/core@^10.0.9":
version "10.0.17"
resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.17.tgz#3367376709721f4ee2068cff54ba581d362789d8"
integrity sha512-gykyjjr0sxzVuZBVTVK4dUmYsorc2qLhdYgSiOVK+m7WXgcYTKZevGWZ7TLAgTZvMelCTvhNq8xnf8FR1IdTbg==
dependencies:
"@babel/runtime" "^7.5.5"
"@emotion/cache" "^10.0.17"
"@emotion/css" "^10.0.14"
"@emotion/serialize" "^0.11.10"
"@emotion/sheet" "0.9.3"
"@emotion/utils" "0.11.2"

"@emotion/css@^10.0.14", "@emotion/css@^10.0.9":
version "10.0.14"
resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.14.tgz#95dacabdd0e22845d1a1b0b5968d9afa34011139"
integrity sha512-MozgPkBEWvorcdpqHZE5x1D/PLEHUitALQCQYt2wayf4UNhpgQs2tN0UwHYS4FMy5ROBH+0ALyCFVYJ/ywmwlg==
dependencies:
"@emotion/serialize" "^0.11.8"
"@emotion/utils" "0.11.2"
babel-plugin-emotion "^10.0.14"

"@emotion/hash@0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f"
integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw==

"@emotion/memoize@0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78"
integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow==

"@emotion/serialize@^0.11.10", "@emotion/serialize@^0.11.11", "@emotion/serialize@^0.11.8":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.11.tgz#c92a5e5b358070a7242d10508143306524e842a4"
integrity sha512-YG8wdCqoWtuoMxhHZCTA+egL0RSGdHEc+YCsmiSBPBEDNuVeMWtjEWtGrhUterSChxzwnWBXvzSxIFQI/3sHLw==
dependencies:
"@emotion/hash" "0.7.3"
"@emotion/memoize" "0.7.3"
"@emotion/unitless" "0.7.4"
"@emotion/utils" "0.11.2"
csstype "^2.5.7"

"@emotion/sheet@0.9.3":
version "0.9.3"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a"
integrity sha512-c3Q6V7Df7jfwSq5AzQWbXHa5soeE4F5cbqi40xn0CzXxWW9/6Mxq48WJEtqfWzbZtW9odZdnRAkwCQwN12ob4A==

"@emotion/stylis@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.4.tgz#6c51afdf1dd0d73666ba09d2eb6c25c220d6fe4c"
integrity sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ==

"@emotion/unitless@0.7.4":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677"
integrity sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ==

"@emotion/utils@0.11.2":
version "0.11.2"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183"
integrity sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA==

"@emotion/weak-memoize@0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc"
integrity sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA==

"@jest/console@^24.7.1":
version "24.7.1"
@@ -1795,23 +1825,21 @@ babel-plugin-dynamic-import-node@^2.3.0:
dependencies:
object.assign "^4.1.0"

babel-plugin-emotion@^9.2.11:
version "9.2.11"
resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728"
integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==
babel-plugin-emotion@^10.0.14:
version "10.0.19"
resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.19.tgz#67b9b213f7505c015f163a387a005c12c502b1de"
integrity sha512-1pJb5uKN/gx6bi3gGr588Krj49sxARI9KmxhtMUa+NRJb6lR3OfC51mh3NlWRsOqdjWlT4cSjnZpnFq5K3T5ZA==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@emotion/babel-utils" "^0.6.4"
"@emotion/hash" "^0.6.2"
"@emotion/memoize" "^0.6.1"
"@emotion/stylis" "^0.7.0"
"@emotion/hash" "0.7.3"
"@emotion/memoize" "0.7.3"
"@emotion/serialize" "^0.11.11"
babel-plugin-macros "^2.0.0"
babel-plugin-syntax-jsx "^6.18.0"
convert-source-map "^1.5.0"
escape-string-regexp "^1.0.5"
find-root "^1.1.0"
mkdirp "^0.5.1"
source-map "^0.5.7"
touch "^2.0.1"

babel-plugin-istanbul@^5.1.0:
version "5.1.1"
@@ -2703,7 +2731,7 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==

convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1:
convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
@@ -2822,19 +2850,6 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0"
elliptic "^6.0.0"

create-emotion@^9.2.12:
version "9.2.12"
resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f"
integrity sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==
dependencies:
"@emotion/hash" "^0.6.2"
"@emotion/memoize" "^0.6.1"
"@emotion/stylis" "^0.7.0"
"@emotion/unitless" "^0.6.2"
csstype "^2.5.2"
stylis "^3.5.0"
stylis-rule-sheet "^0.0.10"

create-hash@^1.1.0, create-hash@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
@@ -2858,13 +2873,12 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"

cross-env@^5.1.4:
version "5.2.0"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==
cross-env@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941"
integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==
dependencies:
cross-spawn "^6.0.5"
is-windows "^1.0.0"
cross-spawn "^7.0.0"

cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
@@ -2877,6 +2891,15 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"

cross-spawn@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14"
integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"

crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@@ -3120,10 +3143,10 @@ csstype@^2.2.0:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.0.tgz#6cf7b2fa7fc32aab3d746802c244d4eda71371a2"
integrity sha512-by8hi8BlLbowQq0qtkx54d9aN73R9oUW20HISpka5kmgsR9F7nnxgfsemuR2sdCKZh+CDNf5egW9UZMm4mgJRg==