~cytrogen/masto-fe

054df2d67e61cdd78afa7b1c15fd61823c421620 — Claire 2 years ago f08f6d2 + f371464
Merge pull request #2216 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
345 files changed, 1836 insertions(+), 1367 deletions(-)

M .eslintrc.js
M .prettierignore
M .prettierrc.js
M .rubocop.yml
M app/javascript/core/mailer.js
M app/javascript/core/public.js
M app/javascript/core/settings.js
M app/javascript/flavours/glitch/actions/app.ts
M app/javascript/flavours/glitch/actions/markers.js
M app/javascript/flavours/glitch/actions/notifications.js
M app/javascript/flavours/glitch/actions/streaming.js
M app/javascript/flavours/glitch/actions/timelines.js
M app/javascript/flavours/glitch/blurhash.ts
M app/javascript/flavours/glitch/compare_id.ts
M app/javascript/flavours/glitch/components/account.jsx
M app/javascript/flavours/glitch/components/animated_number.tsx
M app/javascript/flavours/glitch/components/attachment_list.jsx
M app/javascript/flavours/glitch/components/avatar.tsx
M app/javascript/flavours/glitch/components/blurhash.tsx
M app/javascript/flavours/glitch/components/column_back_button.jsx
M app/javascript/flavours/glitch/components/column_back_button_slim.jsx
M app/javascript/flavours/glitch/components/column_header.jsx
M app/javascript/flavours/glitch/components/dismissable_banner.jsx
D app/javascript/flavours/glitch/components/domain.jsx
A app/javascript/flavours/glitch/components/domain.tsx
M app/javascript/flavours/glitch/components/dropdown_menu.jsx
M app/javascript/flavours/glitch/components/edited_timestamp/index.jsx
M app/javascript/flavours/glitch/components/gifv.tsx
M app/javascript/flavours/glitch/components/icon.tsx
M app/javascript/flavours/glitch/components/icon_button.tsx
M app/javascript/flavours/glitch/components/icon_with_badge.tsx
D app/javascript/flavours/glitch/components/image.jsx
A app/javascript/flavours/glitch/components/image.tsx
M app/javascript/flavours/glitch/components/inline_account.jsx
M app/javascript/flavours/glitch/components/load_gap.jsx
M app/javascript/flavours/glitch/components/media_gallery.jsx
R app/javascript/flavours/glitch/components/{not_signed_in_indicator.jsx => not_signed_in_indicator.tsx}
M app/javascript/flavours/glitch/components/notification_purge_buttons.jsx
M app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx
M app/javascript/flavours/glitch/components/poll.jsx
R app/javascript/flavours/glitch/components/{radio_button.jsx => radio_button.tsx}
M app/javascript/flavours/glitch/components/relative_timestamp.tsx
M app/javascript/flavours/glitch/components/scrollable_list.jsx
M app/javascript/flavours/glitch/components/server_banner.jsx
M app/javascript/flavours/glitch/components/status_action_bar.jsx
M app/javascript/flavours/glitch/components/status_content.jsx
M app/javascript/flavours/glitch/components/status_header.jsx
M app/javascript/flavours/glitch/components/status_icons.jsx
M app/javascript/flavours/glitch/components/status_prepend.jsx
M app/javascript/flavours/glitch/components/status_visibility_icon.jsx
M app/javascript/flavours/glitch/containers/compose_container.jsx
M app/javascript/flavours/glitch/containers/domain_container.jsx
M app/javascript/flavours/glitch/containers/mastodon.jsx
M app/javascript/flavours/glitch/features/about/index.jsx
M app/javascript/flavours/glitch/features/account/components/account_note.jsx
M app/javascript/flavours/glitch/features/account/components/action_bar.jsx
M app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx
M app/javascript/flavours/glitch/features/account/components/header.jsx
M app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
M app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx
M app/javascript/flavours/glitch/features/audio/index.jsx
M app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx
M app/javascript/flavours/glitch/features/compose/components/dropdown.jsx
M app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx
M app/javascript/flavours/glitch/features/compose/components/header.jsx
M app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx
M app/javascript/flavours/glitch/features/compose/components/options.jsx
M app/javascript/flavours/glitch/features/compose/components/poll_form.jsx
M app/javascript/flavours/glitch/features/compose/components/publisher.jsx
M app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx
M app/javascript/flavours/glitch/features/compose/components/search.jsx
M app/javascript/flavours/glitch/features/compose/components/search_results.jsx
M app/javascript/flavours/glitch/features/compose/components/textarea_icons.jsx
M app/javascript/flavours/glitch/features/compose/components/upload.jsx
M app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx
M app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js
M app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js
M app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
M app/javascript/flavours/glitch/features/directory/components/account_card.jsx
M app/javascript/flavours/glitch/features/directory/index.jsx
M app/javascript/flavours/glitch/features/emoji/emoji_compressed.js
M app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js
M app/javascript/flavours/glitch/features/emoji/emoji_mart_search_light.js
M app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.js
M app/javascript/flavours/glitch/features/emoji/emoji_utils.js
M app/javascript/flavours/glitch/features/emoji/unicode_to_filename.js
M app/javascript/flavours/glitch/features/emoji/unicode_to_unified_name.js
M app/javascript/flavours/glitch/features/explore/components/story.jsx
M app/javascript/flavours/glitch/features/favourites/index.jsx
M app/javascript/flavours/glitch/features/filters/select_filter.jsx
M app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx
M app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx
M app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx
M app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx
M app/javascript/flavours/glitch/features/home_timeline/index.jsx
M app/javascript/flavours/glitch/features/interaction_modal/index.jsx
M app/javascript/flavours/glitch/features/list_adder/components/account.jsx
M app/javascript/flavours/glitch/features/list_adder/components/list.jsx
M app/javascript/flavours/glitch/features/list_editor/components/account.jsx
M app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.jsx
M app/javascript/flavours/glitch/features/list_editor/components/search.jsx
M app/javascript/flavours/glitch/features/list_timeline/index.jsx
M app/javascript/flavours/glitch/features/lists/components/new_list_form.jsx
M app/javascript/flavours/glitch/features/local_settings/navigation/item/index.jsx
M app/javascript/flavours/glitch/features/notifications/components/admin_report.jsx
M app/javascript/flavours/glitch/features/notifications/components/admin_signup.jsx
M app/javascript/flavours/glitch/features/notifications/components/clear_column_button.jsx
M app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx
M app/javascript/flavours/glitch/features/notifications/components/follow.jsx
M app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx
M app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx
M app/javascript/flavours/glitch/features/notifications/components/overlay.jsx
M app/javascript/flavours/glitch/features/notifications/components/report.jsx
M app/javascript/flavours/glitch/features/notifications/index.jsx
M app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx
M app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx
M app/javascript/flavours/glitch/features/reblogs/index.jsx
M app/javascript/flavours/glitch/features/report/components/status_check_box.jsx
M app/javascript/flavours/glitch/features/status/components/action_bar.jsx
M app/javascript/flavours/glitch/features/status/components/card.jsx
M app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
M app/javascript/flavours/glitch/features/status/index.jsx
M app/javascript/flavours/glitch/features/subscribed_languages_modal/index.jsx
M app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.jsx
M app/javascript/flavours/glitch/features/ui/components/column_header.jsx
M app/javascript/flavours/glitch/features/ui/components/column_link.jsx
M app/javascript/flavours/glitch/features/ui/components/compare_history_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/doodle_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/follow_requests_column_link.jsx
M app/javascript/flavours/glitch/features/ui/components/header.jsx
M app/javascript/flavours/glitch/features/ui/components/image_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js
M app/javascript/flavours/glitch/features/ui/components/report_modal.jsx
M app/javascript/flavours/glitch/features/ui/components/zoomable_image.jsx
M app/javascript/flavours/glitch/features/ui/index.jsx
M app/javascript/flavours/glitch/features/video/index.jsx
A app/javascript/flavours/glitch/hooks/useHovering.ts
M app/javascript/flavours/glitch/is_mobile.ts
M app/javascript/flavours/glitch/main.jsx
M app/javascript/flavours/glitch/packs/admin.jsx
M app/javascript/flavours/glitch/packs/home.js
M app/javascript/flavours/glitch/packs/public.jsx
M app/javascript/flavours/glitch/packs/settings.js
M app/javascript/flavours/glitch/packs/share.jsx
M app/javascript/flavours/glitch/performance.js
M app/javascript/flavours/glitch/permissions.ts
R app/javascript/flavours/glitch/{base_polyfills.js => polyfills/base_polyfills.ts}
R app/javascript/flavours/glitch/{extra_polyfills.js => polyfills/extra_polyfills.ts}
R app/javascript/flavours/glitch/{load_polyfills.js => polyfills/index.ts}
M app/javascript/flavours/glitch/reducers/compose.js
M app/javascript/flavours/glitch/reducers/contexts.js
M app/javascript/flavours/glitch/reducers/conversations.js
R app/javascript/flavours/glitch/reducers/{index.js => index.ts}
M app/javascript/flavours/glitch/reducers/notifications.js
M app/javascript/flavours/glitch/reducers/settings.js
M app/javascript/flavours/glitch/reducers/timelines.js
M app/javascript/flavours/glitch/scroll.ts
D app/javascript/flavours/glitch/store/configureStore.js
A app/javascript/flavours/glitch/store/index.ts
R app/javascript/flavours/glitch/{middleware/errors.js => store/middlewares/errors.ts}
R app/javascript/flavours/glitch/{middleware/loading_bar.js => store/middlewares/loading_bar.ts}
R app/javascript/flavours/glitch/{middleware/sounds.js => store/middlewares/sounds.ts}
M app/javascript/flavours/glitch/types/resources.ts
M app/javascript/flavours/glitch/utils/filters.ts
M app/javascript/flavours/glitch/utils/numbers.ts
M app/javascript/flavours/glitch/uuid.ts
M app/javascript/mastodon/actions/app.ts
M app/javascript/mastodon/actions/markers.js
M app/javascript/mastodon/actions/notifications.js
M app/javascript/mastodon/actions/streaming.js
M app/javascript/mastodon/actions/timelines.js
M app/javascript/mastodon/blurhash.ts
M app/javascript/mastodon/common.js
M app/javascript/mastodon/compare_id.ts
M app/javascript/mastodon/components/__tests__/avatar-test.jsx
M app/javascript/mastodon/components/__tests__/avatar_overlay-test.jsx
M app/javascript/mastodon/components/account.jsx
M app/javascript/mastodon/components/animated_number.tsx
M app/javascript/mastodon/components/attachment_list.jsx
M app/javascript/mastodon/components/avatar.tsx
M app/javascript/mastodon/components/avatar_composite.jsx
M app/javascript/mastodon/components/avatar_overlay.tsx
M app/javascript/mastodon/components/blurhash.tsx
D app/javascript/mastodon/components/check.jsx
A app/javascript/mastodon/components/check.tsx
M app/javascript/mastodon/components/column_back_button.jsx
M app/javascript/mastodon/components/column_back_button_slim.jsx
M app/javascript/mastodon/components/column_header.jsx
M app/javascript/mastodon/components/dismissable_banner.jsx
D app/javascript/mastodon/components/domain.jsx
A app/javascript/mastodon/components/domain.tsx
M app/javascript/mastodon/components/dropdown_menu.jsx
M app/javascript/mastodon/components/edited_timestamp/index.jsx
M app/javascript/mastodon/components/gifv.tsx
M app/javascript/mastodon/components/hashtag.jsx
M app/javascript/mastodon/components/icon.tsx
M app/javascript/mastodon/components/icon_button.tsx
M app/javascript/mastodon/components/icon_with_badge.tsx
D app/javascript/mastodon/components/image.jsx
A app/javascript/mastodon/components/image.tsx
M app/javascript/mastodon/components/inline_account.jsx
M app/javascript/mastodon/components/load_gap.jsx
M app/javascript/mastodon/components/media_gallery.jsx
R app/javascript/mastodon/components/{not_signed_in_indicator.jsx => not_signed_in_indicator.tsx}
M app/javascript/mastodon/components/picture_in_picture_placeholder.jsx
M app/javascript/mastodon/components/poll.jsx
R app/javascript/mastodon/components/{radio_button.jsx => radio_button.tsx}
M app/javascript/mastodon/components/relative_timestamp.tsx
M app/javascript/mastodon/components/scrollable_list.jsx
M app/javascript/mastodon/components/server_banner.jsx
M app/javascript/mastodon/components/status.jsx
M app/javascript/mastodon/components/status_action_bar.jsx
M app/javascript/mastodon/components/status_content.jsx
D app/javascript/mastodon/components/verified_badge.jsx
A app/javascript/mastodon/components/verified_badge.tsx
M app/javascript/mastodon/containers/compose_container.jsx
M app/javascript/mastodon/containers/domain_container.jsx
M app/javascript/mastodon/containers/mastodon.jsx
M app/javascript/mastodon/features/about/index.jsx
M app/javascript/mastodon/features/account/components/follow_request_note.jsx
M app/javascript/mastodon/features/account/components/header.jsx
M app/javascript/mastodon/features/account_gallery/components/media_item.jsx
M app/javascript/mastodon/features/account_timeline/components/moved_note.jsx
M app/javascript/mastodon/features/audio/index.jsx
M app/javascript/mastodon/features/compose/components/autosuggest_account.jsx
M app/javascript/mastodon/features/compose/components/compose_form.jsx
M app/javascript/mastodon/features/compose/components/navigation_bar.jsx
M app/javascript/mastodon/features/compose/components/poll_button.jsx
M app/javascript/mastodon/features/compose/components/poll_form.jsx
M app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx
M app/javascript/mastodon/features/compose/components/reply_indicator.jsx
M app/javascript/mastodon/features/compose/components/search.jsx
M app/javascript/mastodon/features/compose/components/search_results.jsx
M app/javascript/mastodon/features/compose/components/upload.jsx
M app/javascript/mastodon/features/compose/components/upload_button.jsx
M app/javascript/mastodon/features/compose/components/upload_progress.jsx
M app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
M app/javascript/mastodon/features/compose/containers/language_dropdown_container.js
M app/javascript/mastodon/features/compose/index.jsx
M app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
M app/javascript/mastodon/features/directory/components/account_card.jsx
M app/javascript/mastodon/features/directory/index.jsx
M app/javascript/mastodon/features/emoji/emoji_compressed.js
M app/javascript/mastodon/features/emoji/emoji_mart_data_light.js
M app/javascript/mastodon/features/emoji/emoji_mart_search_light.js
M app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.js
M app/javascript/mastodon/features/emoji/emoji_utils.js
M app/javascript/mastodon/features/emoji/unicode_to_filename.js
M app/javascript/mastodon/features/emoji/unicode_to_unified_name.js
M app/javascript/mastodon/features/explore/components/story.jsx
M app/javascript/mastodon/features/favourites/index.jsx
M app/javascript/mastodon/features/filters/select_filter.jsx
M app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx
M app/javascript/mastodon/features/getting_started/components/announcements.jsx
M app/javascript/mastodon/features/hashtag_timeline/index.jsx
M app/javascript/mastodon/features/home_timeline/index.jsx
M app/javascript/mastodon/features/interaction_modal/index.jsx
M app/javascript/mastodon/features/list_adder/components/account.jsx
M app/javascript/mastodon/features/list_adder/components/list.jsx
M app/javascript/mastodon/features/list_editor/components/account.jsx
M app/javascript/mastodon/features/list_editor/components/edit_list_form.jsx
M app/javascript/mastodon/features/list_editor/components/search.jsx
M app/javascript/mastodon/features/list_timeline/index.jsx
M app/javascript/mastodon/features/notifications/components/clear_column_button.jsx
M app/javascript/mastodon/features/notifications/components/filter_bar.jsx
M app/javascript/mastodon/features/notifications/components/follow_request.jsx
M app/javascript/mastodon/features/notifications/components/notification.jsx
M app/javascript/mastodon/features/notifications/components/notifications_permission_banner.jsx
M app/javascript/mastodon/features/notifications/components/report.jsx
M app/javascript/mastodon/features/notifications/index.jsx
M app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx
M app/javascript/mastodon/features/onboarding/components/step.jsx
M app/javascript/mastodon/features/onboarding/share.jsx
M app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
M app/javascript/mastodon/features/picture_in_picture/components/header.jsx
M app/javascript/mastodon/features/reblogs/index.jsx
M app/javascript/mastodon/features/report/components/option.jsx
M app/javascript/mastodon/features/report/components/status_check_box.jsx
M app/javascript/mastodon/features/status/components/action_bar.jsx
M app/javascript/mastodon/features/status/components/card.jsx
M app/javascript/mastodon/features/status/components/detailed_status.jsx
M app/javascript/mastodon/features/status/index.jsx
M app/javascript/mastodon/features/subscribed_languages_modal/index.jsx
M app/javascript/mastodon/features/ui/components/actions_modal.jsx
M app/javascript/mastodon/features/ui/components/boost_modal.jsx
M app/javascript/mastodon/features/ui/components/bundle_modal_error.jsx
M app/javascript/mastodon/features/ui/components/column_header.jsx
M app/javascript/mastodon/features/ui/components/column_link.jsx
M app/javascript/mastodon/features/ui/components/compare_history_modal.jsx
M app/javascript/mastodon/features/ui/components/embed_modal.jsx
M app/javascript/mastodon/features/ui/components/filter_modal.jsx
M app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
M app/javascript/mastodon/features/ui/components/follow_requests_column_link.jsx
M app/javascript/mastodon/features/ui/components/header.jsx
M app/javascript/mastodon/features/ui/components/image_modal.jsx
M app/javascript/mastodon/features/ui/components/media_modal.jsx
M app/javascript/mastodon/features/ui/components/notifications_counter_icon.js
M app/javascript/mastodon/features/ui/components/report_modal.jsx
M app/javascript/mastodon/features/ui/components/zoomable_image.jsx
M app/javascript/mastodon/features/video/index.jsx
M app/javascript/mastodon/is_mobile.ts
M app/javascript/mastodon/main.jsx
M app/javascript/mastodon/performance.js
M app/javascript/mastodon/permissions.ts
R app/javascript/mastodon/{base_polyfills.js => polyfills/base_polyfills.ts}
R app/javascript/mastodon/{extra_polyfills.js => polyfills/extra_polyfills.ts}
R app/javascript/mastodon/{load_polyfills.js => polyfills/index.ts}
M app/javascript/mastodon/reducers/compose.js
M app/javascript/mastodon/reducers/contexts.js
M app/javascript/mastodon/reducers/conversations.js
R app/javascript/mastodon/reducers/{index.js => index.ts}
M app/javascript/mastodon/reducers/missed_updates.ts
M app/javascript/mastodon/reducers/notifications.js
M app/javascript/mastodon/reducers/settings.js
M app/javascript/mastodon/reducers/timelines.js
M app/javascript/mastodon/scroll.ts
M app/javascript/mastodon/service_worker/web_push_locales.js
D app/javascript/mastodon/store/configureStore.js
A app/javascript/mastodon/store/index.ts
R app/javascript/mastodon/{middleware/errors.js => store/middlewares/errors.ts}
R app/javascript/mastodon/{middleware/loading_bar.js => store/middlewares/loading_bar.ts}
R app/javascript/mastodon/{middleware/sounds.js => store/middlewares/sounds.ts}
M app/javascript/mastodon/utils/filters.ts
M app/javascript/mastodon/utils/hashtags.ts
M app/javascript/mastodon/utils/numbers.ts
M app/javascript/mastodon/uuid.ts
M app/javascript/packs/admin.jsx
M app/javascript/packs/application.js
M app/javascript/packs/public.jsx
M app/javascript/packs/share.jsx
A app/javascript/types/image.d.ts
M app/javascript/types/resources.ts
M config/webpack/shared.js
M lib/mastodon/ip_blocks_cli.rb
M package.json
M tsconfig.json
M yarn.lock
M .eslintrc.js => .eslintrc.js +27 -27
@@ 4,10 4,12 @@ module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
    'plugin:import/recommended',
    'plugin:promise/recommended',
    'plugin:jsdoc/recommended',
    'plugin:prettier/recommended',
  ],

  env: {


@@ 61,20 63,9 @@ module.exports = {
  },

  rules: {
    'brace-style': 'warn',
    'comma-dangle': ['error', 'always-multiline'],
    'comma-spacing': [
      'warn',
      {
        before: false,
        after: true,
      },
    ],
    'comma-style': ['warn', 'last'],
    'consistent-return': 'error',
    'dot-notation': 'error',
    eqeqeq: ['error', 'always', { 'null': 'ignore' }],
    indent: ['warn', 2],
    'jsx-quotes': ['error', 'prefer-single'],
    'no-case-declarations': 'off',
    'no-catch-shadow': 'error',


@@ 94,7 85,6 @@ module.exports = {
      { property: 'substr', message: 'Use .slice instead of .substr.' },
    ],
    'no-self-assign': 'off',
    'no-trailing-spaces': 'warn',
    'no-unused-expressions': 'error',
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': [


@@ 102,32 92,18 @@ module.exports = {
      {
        vars: 'all',
        args: 'after-used',
        destructuredArrayIgnorePattern: '^_',
        ignoreRestSiblings: true,
      },
    ],
    'object-curly-spacing': ['error', 'always'],
    'padded-blocks': [
      'error',
      {
        classes: 'always',
      },
    ],
    quotes: ['error', 'single'],
    semi: 'error',
    'valid-typeof': 'error',

    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }],
    'react/jsx-boolean-value': 'error',
    'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
    'react/jsx-curly-spacing': 'error',
    'react/display-name': 'off',
    'react/jsx-equals-spacing': 'error',
    'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
    'react/jsx-indent': ['error', 2],
    'react/jsx-no-bind': 'error',
    'react/jsx-no-target-blank': 'off',
    'react/jsx-tag-spacing': 'error',
    'react/jsx-wrap-multilines': 'error',
    'react/no-deprecated': 'off',
    'react/no-unknown-property': 'off',
    'react/self-closing-comp': 'error',


@@ 208,6 184,9 @@ module.exports = {
        ],
      },
    ],
    'import/no-amd': 'error',
    'import/no-commonjs': 'error',
    'import/no-import-module-exports': 'error',
    'import/no-webpack-loader-syntax': 'error',

    'promise/always-return': 'off',


@@ 255,6 234,7 @@ module.exports = {
        '*.config.js',
        '.*rc.js',
        'ide-helper.js',
        'config/webpack/**/*',
      ],

      env: {


@@ 264,6 244,10 @@ module.exports = {
      parserOptions: {
        sourceType: 'script',
      },

      rules: {
        'import/no-commonjs': 'off',
      },
    },
    {
      files: [


@@ 275,17 259,25 @@ module.exports = {
        'eslint:recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:react/recommended',
        'plugin:react-hooks/recommended',
        'plugin:jsx-a11y/recommended',
        'plugin:import/recommended',
        'plugin:import/typescript',
        'plugin:promise/recommended',
        'plugin:jsdoc/recommended',
        'plugin:prettier/recommended',
      ],

      rules: {
        '@typescript-eslint/no-explicit-any': 'off',

        'jsdoc/require-jsdoc': 'off',

        // Those rules set stricter rules for TS files
        // to enforce better practices when converting from JS
        'import/no-default-export': 'warn',
        'react/prefer-stateless-function': 'warn',
        'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }],
      },
    },
    {


@@ 298,5 290,13 @@ module.exports = {
        jest: true,
      },
    },
    {
      files: [
        'streaming/**/*',
      ],
      rules: {
        'import/no-commonjs': 'off',
      },
    },
  ],
};

M .prettierignore => .prettierignore +0 -2
@@ 70,8 70,6 @@ app/javascript/styles/mastodon/reset.scss
# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631
*.js
*.jsx
*.ts
*.tsx

# Ignore HTML till cleaned and included in CI
*.html

M .prettierrc.js => .prettierrc.js +2 -1
@@ 1,3 1,4 @@
module.exports = {
  singleQuote: true
  singleQuote: true,
  jsxSingleQuote: true
}

M .rubocop.yml => .rubocop.yml +1 -1
@@ 157,7 157,7 @@ Metrics/MethodLength:
    - 'lib/mastodon/*_cli.rb'

# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
Metrics/ModuleLength:
  CountAsOne: [array, heredoc]


M app/javascript/core/mailer.js => app/javascript/core/mailer.js +1 -1
@@ 1,3 1,3 @@
require('../styles/mailer.scss');
import '../styles/mailer.scss';

require.context('../icons');

M app/javascript/core/public.js => app/javascript/core/public.js +1 -1
@@ 2,7 2,7 @@

import 'packs/public-path';

const { delegate } = require('@rails/ujs');
import { delegate } from '@rails/ujs';

const getProfileAvatarAnimationHandler = (swapTo) => {
  //animate avatar gifs on the profile page when moused over

M app/javascript/core/settings.js => app/javascript/core/settings.js +1 -1
@@ 3,7 3,7 @@
import 'packs/public-path';
import escapeTextContentForBrowser from 'escape-html';

const { delegate } = require('@rails/ujs');
import { delegate } from '@rails/ujs';

import emojify from '../mastodon/features/emoji/emoji';


M app/javascript/flavours/glitch/actions/app.ts => app/javascript/flavours/glitch/actions/app.ts +2 -1
@@ 1,7 1,8 @@
import { createAction } from '@reduxjs/toolkit';
import type { LayoutType } from '../is_mobile';

type ChangeLayoutPayload = {
  layout: 'mobile' | 'single-column' | 'multi-column';
  layout: LayoutType;
};
export const changeLayout =
  createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');

M app/javascript/flavours/glitch/actions/markers.js => app/javascript/flavours/glitch/actions/markers.js +1 -1
@@ 1,6 1,6 @@
import api from '../api';
import { debounce } from 'lodash';
import compareId from '../compare_id';
import { compareId } from '../compare_id';
import { List as ImmutableList } from 'immutable';

export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';

M app/javascript/flavours/glitch/actions/notifications.js => app/javascript/flavours/glitch/actions/notifications.js +1 -1
@@ 13,7 13,7 @@ import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from 'flavours/glitch/utils/html';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import compareId from 'flavours/glitch/compare_id';
import { compareId } from 'flavours/glitch/compare_id';
import { requestNotificationPermission } from 'flavours/glitch/utils/notifications';

export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';

M app/javascript/flavours/glitch/actions/streaming.js => app/javascript/flavours/glitch/actions/streaming.js +2 -0
@@ 52,8 52,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
    /**
     * @param {function(Function, Function): void} fallback
     */

    const useFallback = fallback => {
      fallback(dispatch, () => {
        // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
        pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
      });
    };

M app/javascript/flavours/glitch/actions/timelines.js => app/javascript/flavours/glitch/actions/timelines.js +1 -1
@@ 2,7 2,7 @@ import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers';
import api, { getLinks } from 'flavours/glitch/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/compare_id';
import { compareId } from 'flavours/glitch/compare_id';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import { toServerSideType } from 'flavours/glitch/utils/filters';


M app/javascript/flavours/glitch/blurhash.ts => app/javascript/flavours/glitch/blurhash.ts +2 -2
@@ 98,9 98,9 @@ export const decode83 = (str: string) => {
};

export const intToRGB = (int: number) => ({
  r: Math.max(0, (int >> 16)),
  r: Math.max(0, int >> 16),
  g: Math.max(0, (int >> 8) & 255),
  b: Math.max(0, (int & 255)),
  b: Math.max(0, int & 255),
});

export const getAverageFromBlurhash = (blurhash: string) => {

M app/javascript/flavours/glitch/compare_id.ts => app/javascript/flavours/glitch/compare_id.ts +1 -1
@@ 1,4 1,4 @@
export default function compareId (id1: string, id2: string) {
export function compareId(id1: string, id2: string) {
  if (id1 === id2) {
    return 0;
  }

M app/javascript/flavours/glitch/components/account.jsx => app/javascript/flavours/glitch/components/account.jsx +3 -3
@@ 1,14 1,14 @@
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import { Avatar } from './avatar';
import DisplayName from './display_name';
import Permalink from './permalink';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'flavours/glitch/initial_state';
import RelativeTimestamp from './relative_timestamp';
import { RelativeTimestamp } from './relative_timestamp';
import Skeleton from 'flavours/glitch/components/skeleton';

const messages = defineMessages({

M app/javascript/flavours/glitch/components/animated_number.tsx => app/javascript/flavours/glitch/components/animated_number.tsx +34 -18
@@ 16,13 16,10 @@ const obfuscatedCount = (count: number) => {
type Props = {
  value: number;
  obfuscate?: boolean;
}
export const AnimatedNumber: React.FC<Props> = ({
  value,
  obfuscate,
})=> {
};
export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
  const [previousValue, setPreviousValue] = useState(value);
  const [direction, setDirection] = useState<1|-1>(1);
  const [direction, setDirection] = useState<1 | -1>(1);

  if (previousValue !== value) {
    setPreviousValue(value);


@@ 30,29 27,48 @@ export const AnimatedNumber: React.FC<Props> = ({
  }

  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
  const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]);
  const willLeave = useCallback(
    () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
    [direction]
  );

  if (reduceMotion) {
    return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />;
    return obfuscate ? (
      <>{obfuscatedCount(value)}</>
    ) : (
      <ShortNumber value={value} />
    );
  }

  const styles = [{
    key: `${value}`,
    data: value,
    style: { y: spring(0, { damping: 35, stiffness: 400 }) },
  }];
  const styles = [
    {
      key: `${value}`,
      data: value,
      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
    },
  ];

  return (
    <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
      {items => (
    <TransitionMotion
      styles={styles}
      willEnter={willEnter}
      willLeave={willLeave}
    >
      {(items) => (
        <span className='animated-number'>
          {items.map(({ key, data, style }) => (
            <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
            <span
              key={key}
              style={{
                position: direction * style.y > 0 ? 'absolute' : 'static',
                transform: `translateY(${style.y * 100}%)`,
              }}
            >
              {obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}
            </span>
          ))}
        </span>
      )}
    </TransitionMotion>
  );
};

export default AnimatedNumber;

M app/javascript/flavours/glitch/components/attachment_list.jsx => app/javascript/flavours/glitch/components/attachment_list.jsx +1 -1
@@ 4,7 4,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const filename = url => url.split('/').pop().split('#')[0].split('?')[0];


M app/javascript/flavours/glitch/components/avatar.tsx => app/javascript/flavours/glitch/components/avatar.tsx +12 -7
@@ 1,7 1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import { useHovering } from 'hooks/useHovering';
import { useHovering } from 'flavours/glitch/hooks/useHovering';
import type { Account } from 'flavours/glitch/types/resources';

type Props = {


@@ 10,7 10,7 @@ type Props = {
  size: number;
  style?: React.CSSProperties;
  inline?: boolean;
}
};

export const Avatar: React.FC<Props> = ({
  account,


@@ 19,7 19,8 @@ export const Avatar: React.FC<Props> = ({
  inline = false,
  style: styleFromParent,
}) => {
  const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif);
  const { hovering, handleMouseEnter, handleMouseLeave } =
    useHovering(autoPlayGif);

  const style = {
    ...styleFromParent,


@@ 29,12 30,18 @@ export const Avatar: React.FC<Props> = ({
  };

  if (account) {
    style.backgroundImage = `url(${account.get(hovering ? 'avatar' : 'avatar_static')})`;
    style.backgroundImage = `url(${account.get(
      hovering ? 'avatar' : 'avatar_static'
    )})`;
  }

  return (
    <div
      className={classNames('account__avatar', { 'account__avatar-inline': inline }, className)}
      className={classNames(
        'account__avatar',
        { 'account__avatar-inline': inline },
        className
      )}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      style={style}


@@ 44,5 51,3 @@ export const Avatar: React.FC<Props> = ({
    />
  );
};

export default Avatar;

M app/javascript/flavours/glitch/components/blurhash.tsx => app/javascript/flavours/glitch/components/blurhash.tsx +7 -5
@@ 8,14 8,14 @@ type Props = {
  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
  children?: never;
  [key: string]: any;
}
function Blurhash({
};
const Blurhash: React.FC<Props> = ({
  hash,
  width = 32,
  height = width,
  dummy = false,
  ...canvasProps
}: Props) {
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {


@@ 40,6 40,8 @@ function Blurhash({
  return (
    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
  );
}
};

export default React.memo(Blurhash);
const MemoizedBlurhash = React.memo(Blurhash);

export { MemoizedBlurhash as Blurhash };

M app/javascript/flavours/glitch/components/column_back_button.jsx => app/javascript/flavours/glitch/components/column_back_button.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { createPortal } from 'react-dom';

export default class ColumnBackButton extends React.PureComponent {

M app/javascript/flavours/glitch/components/column_back_button_slim.jsx => app/javascript/flavours/glitch/components/column_back_button_slim.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class ColumnBackButtonSlim extends React.PureComponent {


M app/javascript/flavours/glitch/components/column_header.jsx => app/javascript/flavours/glitch/components/column_header.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const messages = defineMessages({
  show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },

M app/javascript/flavours/glitch/components/dismissable_banner.jsx => app/javascript/flavours/glitch/components/dismissable_banner.jsx +1 -1
@@ 1,5 1,5 @@
import React from 'react';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import { bannerSettings } from 'flavours/glitch/settings';

D app/javascript/flavours/glitch/components/domain.jsx => app/javascript/flavours/glitch/components/domain.jsx +0 -43
@@ 1,43 0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';

const messages = defineMessages({
  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});

class Account extends ImmutablePureComponent {

  static propTypes = {
    domain: PropTypes.string,
    onUnblockDomain: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };

  handleDomainUnblock = () => {
    this.props.onUnblockDomain(this.props.domain);
  };

  render () {
    const { domain, intl } = this.props;

    return (
      <div className='domain'>
        <div className='domain__wrapper'>
          <span className='domain__domain-name'>
            <strong>{domain}</strong>
          </span>

          <div className='domain__buttons'>
            <IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
          </div>
        </div>
      </div>
    );
  }

}

export default injectIntl(Account);

A app/javascript/flavours/glitch/components/domain.tsx => app/javascript/flavours/glitch/components/domain.tsx +42 -0
@@ 0,0 1,42 @@
import React, { useCallback } from 'react';
import { IconButton } from './icon_button';
import { InjectedIntl, defineMessages, injectIntl } from 'react-intl';

const messages = defineMessages({
  unblockDomain: {
    id: 'account.unblock_domain',
    defaultMessage: 'Unblock domain {domain}',
  },
});

type Props = {
  domain: string;
  onUnblockDomain: (domain: string) => void;
  intl: InjectedIntl;
};
const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
  const handleDomainUnblock = useCallback(() => {
    onUnblockDomain(domain);
  }, [domain, onUnblockDomain]);

  return (
    <div className='domain'>
      <div className='domain__wrapper'>
        <span className='domain__domain-name'>
          <strong>{domain}</strong>
        </span>

        <div className='domain__buttons'>
          <IconButton
            active
            icon='unlock'
            title={intl.formatMessage(messages.unblockDomain, { domain })}
            onClick={handleDomainUnblock}
          />
        </div>
      </div>
    </div>
  );
};

export const Domain = injectIntl(_Domain);

M app/javascript/flavours/glitch/components/dropdown_menu.jsx => app/javascript/flavours/glitch/components/dropdown_menu.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import Overlay from 'react-overlays/Overlay';
import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';

M app/javascript/flavours/glitch/components/edited_timestamp/index.jsx => app/javascript/flavours/glitch/components/edited_timestamp/index.jsx +2 -2
@@ 1,11 1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import DropdownMenu from './containers/dropdown_menu_container';
import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import InlineAccount from 'flavours/glitch/components/inline_account';

const mapDispatchToProps = (dispatch, { statusId }) => ({

M app/javascript/flavours/glitch/components/gifv.tsx => app/javascript/flavours/glitch/components/gifv.tsx +15 -13
@@ 8,7 8,7 @@ type Props = {
  width: number;
  height: number;
  onClick?: () => void;
}
};

export const GIFV: React.FC<Props> = ({
  src,


@@ 17,19 17,23 @@ export const GIFV: React.FC<Props> = ({
  width,
  height,
  onClick,
})=> {
}) => {
  const [loading, setLoading] = useState(true);

  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
    setLoading(false);
  }, [setLoading]);
  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
    useCallback(() => {
      setLoading(false);
    }, [setLoading]);

  const handleClick: React.MouseEventHandler = useCallback((e) => {
    if (onClick) {
      e.stopPropagation();
      onClick();
    }
  }, [onClick]);
  const handleClick: React.MouseEventHandler = useCallback(
    (e) => {
      if (onClick) {
        e.stopPropagation();
        onClick();
      }
    },
    [onClick]
  );

  return (
    <div className='gifv' style={{ position: 'relative' }}>


@@ 64,5 68,3 @@ export const GIFV: React.FC<Props> = ({
    </div>
  );
};

export default GIFV;

M app/javascript/flavours/glitch/components/icon.tsx => app/javascript/flavours/glitch/components/icon.tsx +12 -5
@@ 7,8 7,15 @@ type Props = {
  fixedWidth?: boolean;
  children?: never;
  [key: string]: any;
}
export const Icon: React.FC<Props> = ({ id, className, fixedWidth, ...other }) =>
  <i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />;

export default Icon;
};
export const Icon: React.FC<Props> = ({
  id,
  className,
  fixedWidth,
  ...other
}) => (
  <i
    className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
    {...other}
  />
);

M app/javascript/flavours/glitch/components/icon_button.tsx => app/javascript/flavours/glitch/components/icon_button.tsx +16 -16
@@ 21,18 21,17 @@ type Props = {
  animate: boolean;
  overlay: boolean;
  tabIndex: number;
  label: string;
  label?: string;
  counter?: number;
  obfuscateCount?: boolean;
  href?: string;
  ariaHidden: boolean;
}
};
type States = {
  activate: boolean,
  deactivate: boolean,
}
export default class IconButton extends React.PureComponent<Props, States> {

  activate: boolean;
  deactivate: boolean;
};
export class IconButton extends React.PureComponent<Props, States> {
  static defaultProps = {
    size: 18,
    active: false,


@@ 48,7 47,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
    deactivate: false,
  };

  UNSAFE_componentWillReceiveProps (nextProps: Props) {
  UNSAFE_componentWillReceiveProps(nextProps: Props) {
    if (!nextProps.animate) return;

    if (this.props.active && !nextProps.active) {


@@ 58,7 57,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
    }
  }

  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) =>  {
  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
    e.preventDefault();

    if (!this.props.disabled && this.props.onClick != null) {


@@ 84,7 83,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
    }
  };

  render () {
  render() {
    // Hack required for some icons which have an overriden size
    let containerSize = '1.28571429em';
    if (this.props.style?.fontSize) {


@@ 120,10 119,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
      ariaHidden,
    } = this.props;

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

    const classes = classNames(className, 'icon-button', {
      active,


@@ 141,7 137,12 @@ export default class IconButton extends React.PureComponent<Props, States> {

    let contents = (
      <React.Fragment>
        <Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
        <Icon id={icon} fixedWidth aria-hidden='true' />{' '}
        {typeof counter !== 'undefined' && (
          <span className='icon-button__counter'>
            <AnimatedNumber value={counter} obfuscate={obfuscateCount} />
          </span>
        )}
        {this.props.label}
      </React.Fragment>
    );


@@ 174,5 175,4 @@ export default class IconButton extends React.PureComponent<Props, States> {
      </button>
    );
  }

}

M app/javascript/flavours/glitch/components/icon_with_badge.tsx => app/javascript/flavours/glitch/components/icon_with_badge.tsx +11 -6
@@ 1,20 1,25 @@
import React from 'react';
import { Icon } from './icon';

const formatNumber = (num: number): number | string => num > 40 ? '40+' : num;
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);

type Props = {
  id: string;
  count: number;
  issueBadge: boolean;
  className: string;
}
const IconWithBadge: React.FC<Props> = ({ id, count, issueBadge, className }) => (
};
export const IconWithBadge: React.FC<Props> = ({
  id,
  count,
  issueBadge,
  className,
}) => (
  <i className='icon-with-badge'>
    <Icon id={id} fixedWidth className={className} />
    {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
    {count > 0 && (
      <i className='icon-with-badge__badge'>{formatNumber(count)}</i>
    )}
    {issueBadge && <i className='icon-with-badge__issue-badge' />}
  </i>
);

export default IconWithBadge;

D app/javascript/flavours/glitch/components/image.jsx => app/javascript/flavours/glitch/components/image.jsx +0 -33
@@ 1,33 0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Blurhash from './blurhash';
import classNames from 'classnames';

export default class Image extends React.PureComponent {

  static propTypes = {
    src: PropTypes.string,
    srcSet: PropTypes.string,
    blurhash: PropTypes.string,
    className: PropTypes.string,
  };

  state = {
    loaded: false,
  };

  handleLoad = () => this.setState({ loaded: true });

  render () {
    const { src, srcSet, blurhash, className } = this.props;
    const { loaded } = this.state;

    return (
      <div className={classNames('image', { loaded }, className)} role='presentation'>
        {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
        <img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} />
      </div>
    );
  }

}

A app/javascript/flavours/glitch/components/image.tsx => app/javascript/flavours/glitch/components/image.tsx +33 -0
@@ 0,0 1,33 @@
import React, { useCallback, useState } from 'react';
import { Blurhash } from './blurhash';
import classNames from 'classnames';

type Props = {
  src: string;
  srcSet?: string;
  blurhash?: string;
  className?: string;
};

export const Image: React.FC<Props> = ({
  src,
  srcSet,
  blurhash,
  className,
}) => {
  const [loaded, setLoaded] = useState(false);

  const handleLoad = useCallback(() => {
    setLoaded(true);
  }, [setLoaded]);

  return (
    <div
      className={classNames('image', { loaded }, className)}
      role='presentation'
    >
      {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
      <img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
    </div>
  );
};

M app/javascript/flavours/glitch/components/inline_account.jsx => app/javascript/flavours/glitch/components/inline_account.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { makeGetAccount } from 'flavours/glitch/selectors';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';

const makeMapStateToProps = () => {
  const getAccount = makeGetAccount();

M app/javascript/flavours/glitch/components/load_gap.jsx => app/javascript/flavours/glitch/components/load_gap.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const messages = defineMessages({
  load_more: { id: 'status.load_more', defaultMessage: 'Load more' },

M app/javascript/flavours/glitch/components/media_gallery.jsx => app/javascript/flavours/glitch/components/media_gallery.jsx +2 -2
@@ 2,12 2,12 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { is } from 'immutable';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import { debounce } from 'lodash';
import Blurhash from 'flavours/glitch/components/blurhash';
import { Blurhash } from 'flavours/glitch/components/blurhash';

const messages = defineMessages({
  hidden: {

R app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx => app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx +5 -4
@@ 1,12 1,13 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';

const NotSignedInIndicator = () => (
export const NotSignedInIndicator: React.FC = () => (
  <div className='scrollable scrollable--flex'>
    <div className='empty-column-indicator'>
      <FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' />
      <FormattedMessage
        id='not_signed_in_indicator.not_signed_in'
        defaultMessage='You need to sign in to access this resource.'
      />
    </div>
  </div>
);

export default NotSignedInIndicator;

M app/javascript/flavours/glitch/components/notification_purge_buttons.jsx => app/javascript/flavours/glitch/components/notification_purge_buttons.jsx +1 -1
@@ 10,7 10,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';

const messages = defineMessages({

M app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx => app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx +1 -1
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';

M app/javascript/flavours/glitch/components/poll.jsx => app/javascript/flavours/glitch/components/poll.jsx +2 -2
@@ 8,8 8,8 @@ import Motion from 'flavours/glitch/features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'flavours/glitch/features/emoji/emoji';
import RelativeTimestamp from './relative_timestamp';
import Icon from 'flavours/glitch/components/icon';
import { RelativeTimestamp } from './relative_timestamp';
import { Icon } from 'flavours/glitch/components/icon';

const messages = defineMessages({
  closed: {

R app/javascript/flavours/glitch/components/radio_button.jsx => app/javascript/flavours/glitch/components/radio_button.tsx +28 -29
@@ 1,35 1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

export default class RadioButton extends React.PureComponent {
type Props = {
  value: string;
  checked: boolean;
  name: string;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  label: React.ReactNode;
};

  static propTypes = {
    value: PropTypes.string.isRequired,
    checked: PropTypes.bool,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    label: PropTypes.node.isRequired,
  };
export const RadioButton: React.FC<Props> = ({
  name,
  value,
  checked,
  onChange,
  label,
}) => {
  return (
    <label className='radio-button'>
      <input
        name={name}
        type='radio'
        value={value}
        checked={checked}
        onChange={onChange}
      />

  render () {
    const { name, value, checked, onChange, label } = this.props;
      <span className={classNames('radio-button__input', { checked })} />

    return (
      <label className='radio-button'>
        <input
          name={name}
          type='radio'
          value={value}
          checked={checked}
          onChange={onChange}
        />

        <span className={classNames('radio-button__input', { checked })} />

        <span>{label}</span>
      </label>
    );
  }

}
      <span>{label}</span>
    </label>
  );
};

M app/javascript/flavours/glitch/components/relative_timestamp.tsx => app/javascript/flavours/glitch/components/relative_timestamp.tsx +135 -58
@@ 4,20 4,50 @@ import { injectIntl, defineMessages, InjectedIntl } from 'react-intl';
const messages = defineMessages({
  today: { id: 'relative_time.today', defaultMessage: 'today' },
  just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
  just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
  just_now_full: {
    id: 'relative_time.full.just_now',
    defaultMessage: 'just now',
  },
  seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
  seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
  seconds_full: {
    id: 'relative_time.full.seconds',
    defaultMessage: '{number, plural, one {# second} other {# seconds}} ago',
  },
  minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
  minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
  minutes_full: {
    id: 'relative_time.full.minutes',
    defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago',
  },
  hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
  hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
  hours_full: {
    id: 'relative_time.full.hours',
    defaultMessage: '{number, plural, one {# hour} other {# hours}} ago',
  },
  days: { id: 'relative_time.days', defaultMessage: '{number}d' },
  days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
  moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
  seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
  minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
  hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
  days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
  days_full: {
    id: 'relative_time.full.days',
    defaultMessage: '{number, plural, one {# day} other {# days}} ago',
  },
  moments_remaining: {
    id: 'time_remaining.moments',
    defaultMessage: 'Moments remaining',
  },
  seconds_remaining: {
    id: 'time_remaining.seconds',
    defaultMessage: '{number, plural, one {# second} other {# seconds}} left',
  },
  minutes_remaining: {
    id: 'time_remaining.minutes',
    defaultMessage: '{number, plural, one {# minute} other {# minutes}} left',
  },
  hours_remaining: {
    id: 'time_remaining.hours',
    defaultMessage: '{number, plural, one {# hour} other {# hours}} left',
  },
  days_remaining: {
    id: 'time_remaining.days',
    defaultMessage: '{number, plural, one {# day} other {# days}} left',
  },
});

const dateFormatOptions = {


@@ 36,8 66,8 @@ const shortDateFormatOptions = {

const SECOND = 1000;
const MINUTE = 1000 * 60;
const HOUR   = 1000 * 60 * 60;
const DAY    = 1000 * 60 * 60 * 24;
const HOUR = 1000 * 60 * 60;
const DAY = 1000 * 60 * 60 * 24;

const MAX_DELAY = 2147483647;



@@ 57,20 87,27 @@ const selectUnits = (delta: number) => {

const getUnitDelay = (units: string) => {
  switch (units) {
  case 'second':
    return SECOND;
  case 'minute':
    return MINUTE;
  case 'hour':
    return HOUR;
  case 'day':
    return DAY;
  default:
    return MAX_DELAY;
    case 'second':
      return SECOND;
    case 'minute':
      return MINUTE;
    case 'hour':
      return HOUR;
    case 'day':
      return DAY;
    default:
      return MAX_DELAY;
  }
};

export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: number, timeGiven: boolean, short?: boolean) => {
export const timeAgoString = (
  intl: InjectedIntl,
  date: Date,
  now: number,
  year: number,
  timeGiven: boolean,
  short?: boolean
) => {
  const delta = now - date.getTime();

  let relativeTime;


@@ 78,27 115,49 @@ export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year:
  if (delta < DAY && !timeGiven) {
    relativeTime = intl.formatMessage(messages.today);
  } else if (delta < 10 * SECOND) {
    relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
    relativeTime = intl.formatMessage(
      short ? messages.just_now : messages.just_now_full
    );
  } else if (delta < 7 * DAY) {
    if (delta < MINUTE) {
      relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
      relativeTime = intl.formatMessage(
        short ? messages.seconds : messages.seconds_full,
        { number: Math.floor(delta / SECOND) }
      );
    } else if (delta < HOUR) {
      relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
      relativeTime = intl.formatMessage(
        short ? messages.minutes : messages.minutes_full,
        { number: Math.floor(delta / MINUTE) }
      );
    } else if (delta < DAY) {
      relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
      relativeTime = intl.formatMessage(
        short ? messages.hours : messages.hours_full,
        { number: Math.floor(delta / HOUR) }
      );
    } else {
      relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
      relativeTime = intl.formatMessage(
        short ? messages.days : messages.days_full,
        { number: Math.floor(delta / DAY) }
      );
    }
  } else if (date.getFullYear() === year) {
    relativeTime = intl.formatDate(date, shortDateFormatOptions);
  } else {
    relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
    relativeTime = intl.formatDate(date, {
      ...shortDateFormatOptions,
      year: 'numeric',
    });
  }

  return relativeTime;
};

const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGiven = true) => {
const timeRemainingString = (
  intl: InjectedIntl,
  date: Date,
  now: number,
  timeGiven = true
) => {
  const delta = date.getTime() - now;

  let relativeTime;


@@ 108,13 167,21 @@ const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGi
  } else if (delta < 10 * SECOND) {
    relativeTime = intl.formatMessage(messages.moments_remaining);
  } else if (delta < MINUTE) {
    relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
    relativeTime = intl.formatMessage(messages.seconds_remaining, {
      number: Math.floor(delta / SECOND),
    });
  } else if (delta < HOUR) {
    relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
    relativeTime = intl.formatMessage(messages.minutes_remaining, {
      number: Math.floor(delta / MINUTE),
    });
  } else if (delta < DAY) {
    relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
    relativeTime = intl.formatMessage(messages.hours_remaining, {
      number: Math.floor(delta / HOUR),
    });
  } else {
    relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
    relativeTime = intl.formatMessage(messages.days_remaining, {
      number: Math.floor(delta / DAY),
    });
  }

  return relativeTime;


@@ 126,78 193,88 @@ type Props = {
  year: number;
  futureDate?: boolean;
  short?: boolean;
}
};
type States = {
  now: number;
}
};
class RelativeTimestamp extends React.Component<Props, States> {

  state = {
    now: this.props.intl.now(),
  };

  static defaultProps = {
    year: (new Date()).getFullYear(),
    year: new Date().getFullYear(),
    short: true,
  };

  _timer: number | undefined;

  shouldComponentUpdate (nextProps: Props, nextState: States) {
  shouldComponentUpdate(nextProps: Props, nextState: States) {
    // As of right now the locale doesn't change without a new page load,
    // but we might as well check in case that ever changes.
    return this.props.timestamp !== nextProps.timestamp ||
    return (
      this.props.timestamp !== nextProps.timestamp ||
      this.props.intl.locale !== nextProps.intl.locale ||
      this.state.now !== nextState.now;
      this.state.now !== nextState.now
    );
  }

  UNSAFE_componentWillReceiveProps (nextProps: Props) {
  UNSAFE_componentWillReceiveProps(nextProps: Props) {
    if (this.props.timestamp !== nextProps.timestamp) {
      this.setState({ now: this.props.intl.now() });
    }
  }

  componentDidMount () {
  componentDidMount() {
    this._scheduleNextUpdate(this.props, this.state);
  }

  UNSAFE_componentWillUpdate (nextProps: Props, nextState: States) {
  UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) {
    this._scheduleNextUpdate(nextProps, nextState);
  }

  componentWillUnmount () {
  componentWillUnmount() {
    window.clearTimeout(this._timer);
  }

  _scheduleNextUpdate (props: Props, state: States) {
  _scheduleNextUpdate(props: Props, state: States) {
    window.clearTimeout(this._timer);

    const { timestamp }  = props;
    const delta          = (new Date(timestamp)).getTime() - state.now;
    const unitDelay      = getUnitDelay(selectUnits(delta));
    const unitRemainder  = Math.abs(delta % unitDelay);
    const { timestamp } = props;
    const delta = new Date(timestamp).getTime() - state.now;
    const unitDelay = getUnitDelay(selectUnits(delta));
    const unitRemainder = Math.abs(delta % unitDelay);
    const updateInterval = 1000 * 10;
    const delay          = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
    const delay =
      delta < 0
        ? Math.max(updateInterval, unitDelay - unitRemainder)
        : Math.max(updateInterval, unitRemainder);

    this._timer = window.setTimeout(() => {
      this.setState({ now: this.props.intl.now() });
    }, delay);
  }

  render () {
  render() {
    const { timestamp, intl, year, futureDate, short } = this.props;

    const timeGiven    = timestamp.includes('T');
    const date         = new Date(timestamp);
    const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
    const timeGiven = timestamp.includes('T');
    const date = new Date(timestamp);
    const relativeTime = futureDate
      ? timeRemainingString(intl, date, this.state.now, timeGiven)
      : timeAgoString(intl, date, this.state.now, year, timeGiven, short);

    return (
      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
      <time
        dateTime={timestamp}
        title={intl.formatDate(date, dateFormatOptions)}
      >
        {relativeTime}
      </time>
    );
  }

}

export default injectIntl(RelativeTimestamp);
const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);

export { RelativeTimestampWithIntl as RelativeTimestamp };

M app/javascript/flavours/glitch/components/scrollable_list.jsx => app/javascript/flavours/glitch/components/scrollable_list.jsx +3 -2
@@ 8,6 8,7 @@ import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/inters
import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
import LoadingIndicator from './loading_indicator';
import { connect } from 'react-redux';


@@ 236,10 237,10 @@ class ScrollableList extends PureComponent {
  attachScrollListener () {
    if (this.props.bindToDocument) {
      document.addEventListener('scroll', this.handleScroll);
      document.addEventListener('wheel', this.handleWheel);
      document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined);
    } else {
      this.node.addEventListener('scroll', this.handleScroll);
      this.node.addEventListener('wheel', this.handleWheel);
      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined);
    }
  }


M app/javascript/flavours/glitch/components/server_banner.jsx => app/javascript/flavours/glitch/components/server_banner.jsx +1 -1
@@ 7,7 7,7 @@ import ShortNumber from 'flavours/glitch/components/short_number';
import Skeleton from 'flavours/glitch/components/skeleton';
import Account from 'flavours/glitch/containers/account_container';
import { domain } from 'flavours/glitch/initial_state';
import Image from 'flavours/glitch/components/image';
import { Image } from 'flavours/glitch/components/image';
import { Link } from 'react-router-dom';

const messages = defineMessages({

M app/javascript/flavours/glitch/components/status_action_bar.jsx => app/javascript/flavours/glitch/components/status_action_bar.jsx +2 -2
@@ 1,12 1,12 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'flavours/glitch/initial_state';
import RelativeTimestamp from './relative_timestamp';
import { RelativeTimestamp } from './relative_timestamp';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';

M app/javascript/flavours/glitch/components/status_content.jsx => app/javascript/flavours/glitch/components/status_content.jsx +1 -1
@@ 5,7 5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import Permalink from './permalink';
import { connect } from 'react-redux';
import classnames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';


M app/javascript/flavours/glitch/components/status_header.jsx => app/javascript/flavours/glitch/components/status_header.jsx +1 -1
@@ 4,7 4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';

//  Mastodon imports.
import Avatar from './avatar';
import { Avatar } from './avatar';
import AvatarOverlay from './avatar_overlay';
import DisplayName from './display_name';


M app/javascript/flavours/glitch/components/status_icons.jsx => app/javascript/flavours/glitch/components/status_icons.jsx +2 -2
@@ 5,9 5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';

//  Mastodon imports.
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import VisibilityIcon from './status_visibility_icon';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { languages } from 'flavours/glitch/initial_state';

//  Messages for use with internationalization stuff.

M app/javascript/flavours/glitch/components/status_prepend.jsx => app/javascript/flavours/glitch/components/status_prepend.jsx +1 -1
@@ 3,7 3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { me } from 'flavours/glitch/initial_state';

export default class StatusPrepend extends React.PureComponent {

M app/javascript/flavours/glitch/components/status_visibility_icon.jsx => app/javascript/flavours/glitch/components/status_visibility_icon.jsx +1 -1
@@ 3,7 3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const messages = defineMessages({
  public: { id: 'privacy.public.short', defaultMessage: 'Public' },

M app/javascript/flavours/glitch/containers/compose_container.jsx => app/javascript/flavours/glitch/containers/compose_container.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import { store } from 'flavours/glitch/store/configureStore';
import { store } from 'flavours/glitch/store';
import { hydrateStore } from 'flavours/glitch/actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from 'mastodon/locales';

M app/javascript/flavours/glitch/containers/domain_container.jsx => app/javascript/flavours/glitch/containers/domain_container.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { blockDomain, unblockDomain } from '../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Domain from '../components/domain';
import { Domain } from '../components/domain';
import { openModal } from '../actions/modal';

const messages = defineMessages({

M app/javascript/flavours/glitch/containers/mastodon.jsx => app/javascript/flavours/glitch/containers/mastodon.jsx +1 -1
@@ 5,7 5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { Provider as ReduxProvider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import { store } from 'flavours/glitch/store/configureStore';
import { store } from 'flavours/glitch/store';
import UI from 'flavours/glitch/features/ui';
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
import { hydrateStore } from 'flavours/glitch/actions/store';

M app/javascript/flavours/glitch/features/about/index.jsx => app/javascript/flavours/glitch/features/about/index.jsx +2 -2
@@ 9,9 9,9 @@ import { Helmet } from 'react-helmet';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
import Account from 'flavours/glitch/containers/account_container';
import Skeleton from 'flavours/glitch/components/skeleton';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';
import Image from 'flavours/glitch/components/image';
import { Image } from 'flavours/glitch/components/image';

const messages = defineMessages({
  title: { id: 'column.about', defaultMessage: 'About' },

M app/javascript/flavours/glitch/features/account/components/account_note.jsx => app/javascript/flavours/glitch/features/account/components/account_note.jsx +1 -1
@@ 3,7 3,7 @@ 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 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import Textarea from 'react-textarea-autosize';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/account/components/action_bar.jsx => app/javascript/flavours/glitch/features/account/components/action_bar.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { NavLink } from 'react-router-dom';
import { FormattedMessage, FormattedNumber } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

class ActionBar extends React.PureComponent {


M app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx => app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class FollowRequestNote extends ImmutablePureComponent {


M app/javascript/flavours/glitch/features/account/components/header.jsx => app/javascript/flavours/glitch/features/account/components/header.jsx +3 -3
@@ 6,9 6,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import IconButton from 'flavours/glitch/components/icon_button';
import Avatar from 'flavours/glitch/components/avatar';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Avatar } from 'flavours/glitch/components/avatar';
import Button from 'flavours/glitch/components/button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';

M app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx => app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx +2 -2
@@ 1,6 1,6 @@
import Blurhash from 'flavours/glitch/components/blurhash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import PropTypes from 'prop-types';
import React from 'react';

M app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx => app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx +1 -1
@@ 5,7 5,7 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from '../../../components/avatar_overlay';
import DisplayName from '../../../components/display_name';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class MovedNote extends ImmutablePureComponent {


M app/javascript/flavours/glitch/features/audio/index.jsx => app/javascript/flavours/glitch/features/audio/index.jsx +2 -2
@@ 2,12 2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';
import { throttle, debounce } from 'lodash';
import Visualizer from './visualizer';
import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import Blurhash from 'flavours/glitch/components/blurhash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { is } from 'immutable';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx => app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +1 -1
@@ 1,5 1,5 @@
import React from 'react';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';

M app/javascript/flavours/glitch/features/compose/components/dropdown.jsx => app/javascript/flavours/glitch/features/compose/components/dropdown.jsx +1 -1
@@ 5,7 5,7 @@ import React from 'react';
import Overlay from 'react-overlays/Overlay';

//  Components.
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import DropdownMenu from './dropdown_menu';

//  The component.

M app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx => app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx +1 -1
@@ 4,7 4,7 @@ import React from 'react';
import classNames from 'classnames';

//  Components.
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

//  Utils.
import { withPassive } from 'flavours/glitch/utils/dom_helpers';

M app/javascript/flavours/glitch/features/compose/components/header.jsx => app/javascript/flavours/glitch/features/compose/components/header.jsx +1 -1
@@ 7,7 7,7 @@ import { Link } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component';

//  Components.
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

//  Utils.
import { conditionalRender } from 'flavours/glitch/utils/react_helpers';

M app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx => app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ActionBar from './action_bar';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import Permalink from 'flavours/glitch/components/permalink';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';

M app/javascript/flavours/glitch/features/compose/components/options.jsx => app/javascript/flavours/glitch/features/compose/components/options.jsx +1 -1
@@ 7,7 7,7 @@ import Toggle from 'react-toggle';
import { connect } from 'react-redux';

//  Components.
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import TextIconButton from './text_icon_button';
import DropdownContainer from '../containers/dropdown_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';

M app/javascript/flavours/glitch/features/compose/components/poll_form.jsx => app/javascript/flavours/glitch/features/compose/components/poll_form.jsx +2 -2
@@ 3,8 3,8 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button';
import Icon from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Icon } from 'flavours/glitch/components/icon';
import AutosuggestInput from 'flavours/glitch/components/autosuggest_input';
import classNames from 'classnames';
import { pollLimits } from 'flavours/glitch/initial_state';

M app/javascript/flavours/glitch/features/compose/components/publisher.jsx => app/javascript/flavours/glitch/features/compose/components/publisher.jsx +1 -1
@@ 8,7 8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';

//  Components.
import Button from 'flavours/glitch/components/button';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

//  Utils.
import { maxChars } from 'flavours/glitch/initial_state';

M app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx => app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx +1 -1
@@ 7,7 7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';

//  Components.
import AccountContainer from 'flavours/glitch/containers/account_container';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import AttachmentList from 'flavours/glitch/components/attachment_list';

//  Messages.

M app/javascript/flavours/glitch/features/compose/components/search.jsx => app/javascript/flavours/glitch/features/compose/components/search.jsx +1 -1
@@ 9,7 9,7 @@ import {
import Overlay from 'react-overlays/Overlay';

//  Components.
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

//  Utils.
import { focusRoot } from 'flavours/glitch/utils/dom_helpers';

M app/javascript/flavours/glitch/features/compose/components/search_results.jsx => app/javascript/flavours/glitch/features/compose/components/search_results.jsx +1 -1
@@ 6,7 6,7 @@ import AccountContainer from 'flavours/glitch/containers/account_container';
import StatusContainer from 'flavours/glitch/containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { searchEnabled } from 'flavours/glitch/initial_state';
import LoadMore from 'flavours/glitch/components/load_more';


M app/javascript/flavours/glitch/features/compose/components/textarea_icons.jsx => app/javascript/flavours/glitch/features/compose/components/textarea_icons.jsx +1 -1
@@ 6,7 6,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';

//  Components.
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

//  Messages.
const messages = defineMessages({

M app/javascript/flavours/glitch/features/compose/components/upload.jsx => app/javascript/flavours/glitch/features/compose/components/upload.jsx +1 -1
@@ 5,7 5,7 @@ import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class Upload extends ImmutablePureComponent {


M app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx => app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { FormattedMessage } from 'react-intl';

export default class UploadProgress extends React.PureComponent {

M app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js => app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js +1 -0
@@ 72,6 72,7 @@ const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
  },

  onPickEmoji: emoji => {
    // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
    dispatch(useEmoji(emoji));

    if (onPickEmoji) {

M app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js => app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js +1 -0
@@ 26,6 26,7 @@ const mapDispatchToProps = dispatch => ({
  },

  onClose (value) {
    // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
    dispatch(useLanguage(value));
  },


M app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx => app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +2 -2
@@ 8,8 8,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import AvatarComposite from 'flavours/glitch/components/avatar_composite';
import Permalink from 'flavours/glitch/components/permalink';
import IconButton from 'flavours/glitch/components/icon_button';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import { HotKeys } from 'react-hotkeys';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import classNames from 'classnames';

M app/javascript/flavours/glitch/features/directory/components/account_card.jsx => app/javascript/flavours/glitch/features/directory/components/account_card.jsx +2 -2
@@ 4,10 4,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { makeGetAccount } from 'flavours/glitch/selectors';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import Permalink from 'flavours/glitch/components/permalink';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import Button from 'flavours/glitch/components/button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/initial_state';

M app/javascript/flavours/glitch/features/directory/index.jsx => app/javascript/flavours/glitch/features/directory/index.jsx +1 -1
@@ 9,7 9,7 @@ import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavour
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';
import { List as ImmutableList } from 'immutable';
import AccountCard from './components/account_card';
import RadioButton from 'flavours/glitch/components/radio_button';
import { RadioButton } from 'flavours/glitch/components/radio_button';
import LoadMore from 'flavours/glitch/components/load_more';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';

M app/javascript/flavours/glitch/features/emoji/emoji_compressed.js => app/javascript/flavours/glitch/features/emoji/emoji_compressed.js +2 -0
@@ 1,3 1,5 @@
/* eslint-disable import/no-commonjs --
   We need to use CommonJS here due to preval */
// @preval
// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
// This file contains the compressed version of the emoji data from

M app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js => app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js +5 -3
@@ 1,8 1,10 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed');
import { unicodeToUnifiedName } from './unicode_to_unified_name';
import emojiCompressed from './emoji_compressed';

const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;

const emojis = {};



@@ 33,7 35,7 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
  };
});

module.exports = {
export {
  emojis,
  skins,
  categories,

M app/javascript/flavours/glitch/features/emoji/emoji_mart_search_light.js => app/javascript/flavours/glitch/features/emoji/emoji_mart_search_light.js +1 -1
@@ 1,7 1,7 @@
// This code is largely borrowed from:
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js

import data from './emoji_mart_data_light';
import * as data from './emoji_mart_data_light';
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';

let originalPool = {};

M app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.js => app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.js +9 -6
@@ 2,14 2,17 @@
// (i.e. the svg filename) and a shortCode intended to be shown
// as a "title" attribute in an HTML element (aka tooltip).

import emojiCompressed from './emoji_compressed';

import { unicodeToFilename } from './unicode_to_filename';

const [
  shortCodesToEmojiData,
  skins, // eslint-disable-line @typescript-eslint/no-unused-vars
  categories, // eslint-disable-line @typescript-eslint/no-unused-vars
  short_names, // eslint-disable-line @typescript-eslint/no-unused-vars
  _skins,
  _categories,
  _short_names,
  emojisWithoutShortCodes,
] = require('./emoji_compressed');
const { unicodeToFilename } = require('./unicode_to_filename');
] = emojiCompressed;

// decompress
const unicodeMapping = {};


@@ 32,4 35,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
});
emojisWithoutShortCodes.forEach(emojiMapData => processEmojiMapData(emojiMapData));

module.exports = unicodeMapping;
export default unicodeMapping;

M app/javascript/flavours/glitch/features/emoji/emoji_utils.js => app/javascript/flavours/glitch/features/emoji/emoji_utils.js +1 -1
@@ 1,7 1,7 @@
// This code is largely borrowed from:
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js

import data from './emoji_mart_data_light';
import * as data from './emoji_mart_data_light';

const buildSearch = (data) => {
  const search = [];

M app/javascript/flavours/glitch/features/emoji/unicode_to_filename.js => app/javascript/flavours/glitch/features/emoji/unicode_to_filename.js +3 -0
@@ 1,3 1,6 @@
/* eslint-disable import/no-commonjs --
   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */

// taken from:
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
exports.unicodeToFilename = (str) => {

M app/javascript/flavours/glitch/features/emoji/unicode_to_unified_name.js => app/javascript/flavours/glitch/features/emoji/unicode_to_unified_name.js +3 -0
@@ 1,3 1,6 @@
/* eslint-disable import/no-commonjs --
   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */

function padLeft(str, num) {
  while (str.length < num) {
    str = '0' + str;

M app/javascript/flavours/glitch/features/explore/components/story.jsx => app/javascript/flavours/glitch/features/explore/components/story.jsx +1 -1
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Blurhash from 'flavours/glitch/components/blurhash';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { accountsCountRenderer } from 'flavours/glitch/components/hashtag';
import ShortNumber from 'flavours/glitch/components/short_number';
import Skeleton from 'flavours/glitch/components/skeleton';

M app/javascript/flavours/glitch/features/favourites/index.jsx => app/javascript/flavours/glitch/features/favourites/index.jsx +1 -1
@@ 5,7 5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import ColumnHeader from 'flavours/glitch/components/column_header';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { fetchFavourites } from 'flavours/glitch/actions/interactions';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import ScrollableList from 'flavours/glitch/components/scrollable_list';

M app/javascript/flavours/glitch/features/filters/select_filter.jsx => app/javascript/flavours/glitch/features/filters/select_filter.jsx +1 -1
@@ 4,7 4,7 @@ import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { toServerSideType } from 'flavours/glitch/utils/filters';
import { loupeIcon, deleteIcon } from 'flavours/glitch/utils/icons';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import fuzzysort from 'fuzzysort';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx => app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx +2 -2
@@ 4,10 4,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { makeGetAccount } from 'flavours/glitch/selectors';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import Permalink from 'flavours/glitch/components/permalink';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { injectIntl, defineMessages } from 'react-intl';
import { followAccount, unfollowAccount } from 'flavours/glitch/actions/accounts';


M app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx => app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx +2 -2
@@ 2,9 2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from 'flavours/glitch/components/permalink';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';


M app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx => app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx +3 -3
@@ 3,15 3,15 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'flavours/glitch/components/icon_button';
import Icon from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Icon } from 'flavours/glitch/components/icon';
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'flavours/glitch/initial_state';
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light';
import classNames from 'classnames';
import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emoji_picker_dropdown_container';
import AnimatedNumber from 'flavours/glitch/components/animated_number';
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { assetHost } from 'flavours/glitch/utils/config';

M app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx => app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx +1 -1
@@ 12,7 12,7 @@ import { connectHashtagStream } from 'flavours/glitch/actions/streaming';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { isEqual } from 'lodash';
import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';


M app/javascript/flavours/glitch/features/home_timeline/index.jsx => app/javascript/flavours/glitch/features/home_timeline/index.jsx +2 -2
@@ 12,8 12,8 @@ import { Link } from 'react-router-dom';
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
import classNames from 'classnames';
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator';
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
import { Helmet } from 'react-helmet';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/interaction_modal/index.jsx => app/javascript/flavours/glitch/features/interaction_modal/index.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { registrationsOpen } from 'flavours/glitch/initial_state';
import { connect } from 'react-redux';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';
import { openModal, closeModal } from 'flavours/glitch/actions/modal';


M app/javascript/flavours/glitch/features/list_adder/components/account.jsx => app/javascript/flavours/glitch/features/list_adder/components/account.jsx +1 -1
@@ 3,7 3,7 @@ import { connect } from 'react-redux';
import { makeGetAccount } from '../../../selectors';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import { injectIntl } from 'react-intl';


M app/javascript/flavours/glitch/features/list_adder/components/list.jsx => app/javascript/flavours/glitch/features/list_adder/components/list.jsx +2 -2
@@ 3,10 3,10 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const messages = defineMessages({
  remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },

M app/javascript/flavours/glitch/features/list_editor/components/account.jsx => app/javascript/flavours/glitch/features/list_editor/components/account.jsx +2 -2
@@ 2,9 2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { defineMessages } from 'react-intl';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.jsx => app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { changeListEditorTitle, submitListEditor } from 'flavours/glitch/actions/lists';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/list_editor/components/search.jsx => app/javascript/flavours/glitch/features/list_editor/components/search.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages } from 'react-intl';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const messages = defineMessages({
  search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },

M app/javascript/flavours/glitch/features/list_timeline/index.jsx => app/javascript/flavours/glitch/features/list_timeline/index.jsx +2 -2
@@ 11,9 11,9 @@ import { connectListStream } from 'flavours/glitch/actions/streaming';
import { expandListTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import RadioButton from 'flavours/glitch/components/radio_button';
import { RadioButton } from 'flavours/glitch/components/radio_button';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';


M app/javascript/flavours/glitch/features/lists/components/new_list_form.jsx => app/javascript/flavours/glitch/features/lists/components/new_list_form.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { changeListEditorTitle, submitListEditor } from 'flavours/glitch/actions/lists';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/local_settings/navigation/item/index.jsx => app/javascript/flavours/glitch/features/local_settings/navigation/item/index.jsx +1 -1
@@ 3,7 3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *


M app/javascript/flavours/glitch/features/notifications/components/admin_report.jsx => app/javascript/flavours/glitch/features/notifications/components/admin_report.jsx +1 -1
@@ 10,7 10,7 @@ import classNames from 'classnames';
// Our imports.
import Permalink from 'flavours/glitch/components/permalink';
import NotificationOverlayContainer from '../containers/overlay_container';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import Report from './report';

export default class AdminReport extends ImmutablePureComponent {

M app/javascript/flavours/glitch/features/notifications/components/admin_signup.jsx => app/javascript/flavours/glitch/features/notifications/components/admin_signup.jsx +1 -1
@@ 11,7 11,7 @@ import classNames from 'classnames';
import Permalink from 'flavours/glitch/components/permalink';
import AccountContainer from 'flavours/glitch/containers/account_container';
import NotificationOverlayContainer from '../containers/overlay_container';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class NotificationFollow extends ImmutablePureComponent {


M app/javascript/flavours/glitch/features/notifications/components/clear_column_button.jsx => app/javascript/flavours/glitch/features/notifications/components/clear_column_button.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class ClearColumnButton extends React.Component {


M app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx => app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const tooltips = defineMessages({
  mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },

M app/javascript/flavours/glitch/features/notifications/components/follow.jsx => app/javascript/flavours/glitch/features/notifications/components/follow.jsx +1 -1
@@ 11,7 11,7 @@ import classNames from 'classnames';
import Permalink from 'flavours/glitch/components/permalink';
import AccountContainer from 'flavours/glitch/containers/account_container';
import NotificationOverlayContainer from '../containers/overlay_container';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class NotificationFollow extends ImmutablePureComponent {


M app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx => app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx +3 -3
@@ 1,15 1,15 @@
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import Permalink from 'flavours/glitch/components/permalink';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import NotificationOverlayContainer from '../containers/overlay_container';
import { HotKeys } from 'react-hotkeys';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx => app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx +2 -2
@@ 1,7 1,7 @@
import React from 'react';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import Button from 'flavours/glitch/components/button';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { connect } from 'react-redux';

M app/javascript/flavours/glitch/features/notifications/components/overlay.jsx => app/javascript/flavours/glitch/features/notifications/components/overlay.jsx +1 -1
@@ 9,7 9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

const messages = defineMessages({
  markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' },

M app/javascript/flavours/glitch/features/notifications/components/report.jsx => app/javascript/flavours/glitch/features/notifications/components/report.jsx +1 -1
@@ 4,7 4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from 'flavours/glitch/components/avatar_overlay';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';

const messages = defineMessages({
  openReport: { id: 'report_notification.open', defaultMessage: 'Open report' },

M app/javascript/flavours/glitch/features/notifications/index.jsx => app/javascript/flavours/glitch/features/notifications/index.jsx +3 -3
@@ 25,10 25,10 @@ import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import LoadGap from 'flavours/glitch/components/load_gap';
import Icon from 'flavours/glitch/components/icon';
import compareId from 'flavours/glitch/compare_id';
import { Icon } from 'flavours/glitch/components/icon';
import { compareId } from 'flavours/glitch/compare_id';
import NotificationsPermissionBanner from './components/notifications_permission_banner';
import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator';
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
import { Helmet } from 'react-helmet';

import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container';

M app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx => app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx +1 -1
@@ 3,7 3,7 @@ import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import classNames from 'classnames';
import { me, boostModal } from 'flavours/glitch/initial_state';
import { defineMessages, injectIntl } from 'react-intl';

M app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx => app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx +2 -2
@@ 3,9 3,9 @@ import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Link } from 'react-router-dom';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import { defineMessages, injectIntl } from 'react-intl';


M app/javascript/flavours/glitch/features/reblogs/index.jsx => app/javascript/flavours/glitch/features/reblogs/index.jsx +1 -1
@@ 6,7 6,7 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import { fetchReblogs } from 'flavours/glitch/actions/interactions';
import AccountContainer from 'flavours/glitch/containers/account_container';
import Column from 'flavours/glitch/features/ui/components/column';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';

M app/javascript/flavours/glitch/features/report/components/status_check_box.jsx => app/javascript/flavours/glitch/features/report/components/status_check_box.jsx +2 -2
@@ 2,9 2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContent from 'flavours/glitch/components/status_content';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import Option from './option';
import MediaAttachments from 'flavours/glitch/components/media_attachments';
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';

M app/javascript/flavours/glitch/features/status/components/action_bar.jsx => app/javascript/flavours/glitch/features/status/components/action_bar.jsx +1 -1
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';

M app/javascript/flavours/glitch/features/status/components/card.jsx => app/javascript/flavours/glitch/features/status/components/card.jsx +2 -2
@@ 5,9 5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import classnames from 'classnames';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { useBlurhash } from 'flavours/glitch/initial_state';
import Blurhash from 'flavours/glitch/components/blurhash';
import { Blurhash } from 'flavours/glitch/components/blurhash';

const getHostname = url => {
  const parser = document.createElement('a');

M app/javascript/flavours/glitch/features/status/components/detailed_status.jsx => app/javascript/flavours/glitch/features/status/components/detailed_status.jsx +3 -3
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import StatusContent from 'flavours/glitch/components/status_content';
import MediaGallery from 'flavours/glitch/components/media_gallery';


@@ 16,8 16,8 @@ import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames';
import PollContainer from 'flavours/glitch/containers/poll_container';
import Icon from 'flavours/glitch/components/icon';
import AnimatedNumber from 'flavours/glitch/components/animated_number';
import { Icon } from 'flavours/glitch/components/icon';
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';


M app/javascript/flavours/glitch/features/status/index.jsx => app/javascript/flavours/glitch/features/status/index.jsx +1 -1
@@ 52,7 52,7 @@ import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { Helmet } from 'react-helmet';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';


M app/javascript/flavours/glitch/features/subscribed_languages_modal/index.jsx => app/javascript/flavours/glitch/features/subscribed_languages_modal/index.jsx +1 -1
@@ 8,7 8,7 @@ import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable';
import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import Option from 'flavours/glitch/features/report/components/option';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import Button from 'flavours/glitch/components/button';
import { followAccount } from 'flavours/glitch/actions/accounts';


M app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx => app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx +3 -3
@@ 3,11 3,11 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusContent from 'flavours/glitch/components/status_content';
import Avatar from 'flavours/glitch/components/avatar';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { Avatar } from 'flavours/glitch/components/avatar';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import DisplayName from 'flavours/glitch/components/display_name';
import classNames from 'classnames';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';

export default class ActionsModal extends ImmutablePureComponent {


M app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx => app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx +3 -3
@@ 5,11 5,11 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'flavours/glitch/components/button';
import StatusContent from 'flavours/glitch/components/status_content';
import Avatar from 'flavours/glitch/components/avatar';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { Avatar } from 'flavours/glitch/components/avatar';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import DisplayName from 'flavours/glitch/components/display_name';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import ImmutablePureComponent from 'react-immutable-pure-component';
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
import classNames from 'classnames';

M app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.jsx => app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';

import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';

const messages = defineMessages({
  error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },

M app/javascript/flavours/glitch/features/ui/components/column_header.jsx => app/javascript/flavours/glitch/features/ui/components/column_header.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';

export default class ColumnHeader extends React.PureComponent {


M app/javascript/flavours/glitch/features/ui/components/column_link.jsx => app/javascript/flavours/glitch/features/ui/components/column_link.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import classNames from 'classnames';

const ColumnLink = ({ icon, text, to, onClick, href, method, badge, transparent, ...other }) => {

M app/javascript/flavours/glitch/features/ui/components/compare_history_modal.jsx => app/javascript/flavours/glitch/features/ui/components/compare_history_modal.jsx +2 -2
@@ 7,8 7,8 @@ import { closeModal } from 'flavours/glitch/actions/modal';
import emojify from 'flavours/glitch/features/emoji/emoji';
import escapeTextContentForBrowser from 'escape-html';
import InlineAccount from 'flavours/glitch/components/inline_account';
import IconButton from 'flavours/glitch/components/icon_button';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import MediaAttachments from 'flavours/glitch/components/media_attachments';

const mapStateToProps = (state, { statusId }) => ({

M app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.jsx => app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.jsx +1 -1
@@ 4,7 4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { preferenceLink } from 'flavours/glitch/utils/backend_links';
import Button from 'flavours/glitch/components/button';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import illustration from 'flavours/glitch/images/logo_warn_glitch.svg';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/ui/components/doodle_modal.jsx => app/javascript/flavours/glitch/features/ui/components/doodle_modal.jsx +1 -1
@@ 6,7 6,7 @@ import Atrament from 'atrament'; // the doodling library
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { doodleSet, uploadCompose } from 'flavours/glitch/actions/compose';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { debounce, mapValues } from 'lodash';
import classNames from 'classnames';


M app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx => app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import api from 'flavours/glitch/api';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';

const messages = defineMessages({
  close: { id: 'lightbox.close', defaultMessage: 'Close' },

M app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx => app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx +3 -3
@@ 4,11 4,11 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'flavours/glitch/components/button';
import StatusContent from 'flavours/glitch/components/status_content';
import Avatar from 'flavours/glitch/components/avatar';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { Avatar } from 'flavours/glitch/components/avatar';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import DisplayName from 'flavours/glitch/components/display_name';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import Icon from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';

M app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx => app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx +1 -1
@@ 5,7 5,7 @@ import { fetchFilters, createFilter, createFilterStatus } from 'flavours/glitch/
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import SelectFilter from 'flavours/glitch/features/filters/select_filter';
import AddedToFilter from 'flavours/glitch/features/filters/added_to_filter';


M app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx => app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx +2 -2
@@ 7,7 7,7 @@ import classNames from 'classnames';
import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from 'flavours/glitch/actions/compose';
import Video, { getPointerPosition } from 'flavours/glitch/features/video';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import Button from 'flavours/glitch/components/button';
import Audio from 'flavours/glitch/features/audio';
import Textarea from 'react-textarea-autosize';


@@ 15,7 15,7 @@ import UploadProgress from 'flavours/glitch/features/compose/components/upload_p
import CharacterCounter from 'flavours/glitch/features/compose/components/character_counter';
import { length } from 'stringz';
import { Tesseract as fetchTesseract } from 'flavours/glitch/features/ui/util/async-components';
import GIFV from 'flavours/glitch/components/gifv';
import { GIFV } from 'flavours/glitch/components/gifv';
import { me } from 'flavours/glitch/initial_state';
// eslint-disable-next-line import/no-extraneous-dependencies
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';

M app/javascript/flavours/glitch/features/ui/components/follow_requests_column_link.jsx => app/javascript/flavours/glitch/features/ui/components/follow_requests_column_link.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
import { connect } from 'react-redux';
import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
import { List as ImmutableList } from 'immutable';
import { injectIntl, defineMessages } from 'react-intl';


M app/javascript/flavours/glitch/features/ui/components/header.jsx => app/javascript/flavours/glitch/features/ui/components/header.jsx +1 -1
@@ 3,7 3,7 @@ import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
import { Link, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
import Avatar from 'flavours/glitch/components/avatar';
import { Avatar } from 'flavours/glitch/components/avatar';
import Permalink from 'flavours/glitch/components/permalink';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

M app/javascript/flavours/glitch/features/ui/components/image_modal.jsx => app/javascript/flavours/glitch/features/ui/components/image_modal.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import ImageLoader from './image_loader';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/ui/components/media_modal.jsx => app/javascript/flavours/glitch/features/ui/components/media_modal.jsx +3 -3
@@ 6,11 6,11 @@ import Video from 'flavours/glitch/features/video';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImageLoader from './image_loader';
import Icon from 'flavours/glitch/components/icon';
import GIFV from 'flavours/glitch/components/gifv';
import { Icon } from 'flavours/glitch/components/icon';
import { GIFV } from 'flavours/glitch/components/gifv';
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
import { disableSwiping } from 'flavours/glitch/initial_state';

M app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js => app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js +1 -1
@@ 1,5 1,5 @@
import { connect } from 'react-redux';
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';

const mapStateToProps = state => ({
  count: state.getIn(['local_settings', 'notifications', 'tab_badge']) ? state.getIn(['notifications', 'unread']) : 0,

M app/javascript/flavours/glitch/features/ui/components/report_modal.jsx => app/javascript/flavours/glitch/features/ui/components/report_modal.jsx +1 -1
@@ 10,7 10,7 @@ import { makeGetAccount } from 'flavours/glitch/selectors';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { OrderedSet } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import Category from 'flavours/glitch/features/report/category';
import Statuses from 'flavours/glitch/features/report/statuses';
import Rules from 'flavours/glitch/features/report/rules';

M app/javascript/flavours/glitch/features/ui/components/zoomable_image.jsx => app/javascript/flavours/glitch/features/ui/components/zoomable_image.jsx +1 -1
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from 'flavours/glitch/components/icon_button';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';

const messages = defineMessages({

M app/javascript/flavours/glitch/features/ui/index.jsx => app/javascript/flavours/glitch/features/ui/index.jsx +3 -3
@@ 58,7 58,7 @@ import {
} from './util/async-components';
import { HotKeys } from 'react-hotkeys';
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
// TODO: import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Header from './components/header';



@@ 82,7 82,7 @@ const mapStateToProps = state => ({
  showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
  hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
  moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]),
  firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
  firstLaunch: false, // TODO: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
  username: state.getIn(['accounts', me, 'username']),
});



@@ 405,7 405,7 @@ class UI extends React.Component {
    // On first launch, redirect to the follow recommendations page
    if (signedIn && this.props.firstLaunch) {
      this.context.router.history.replace('/start');
      this.props.dispatch(closeOnboarding());
      // TODO: this.props.dispatch(closeOnboarding());
    }

    if (signedIn) {

M app/javascript/flavours/glitch/features/video/index.jsx => app/javascript/flavours/glitch/features/video/index.jsx +2 -2
@@ 6,8 6,8 @@ import { throttle } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import Icon from 'flavours/glitch/components/icon';
import Blurhash from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { Blurhash } from 'flavours/glitch/components/blurhash';

const messages = defineMessages({
  play: { id: 'video.play', defaultMessage: 'Play' },

A app/javascript/flavours/glitch/hooks/useHovering.ts => app/javascript/flavours/glitch/hooks/useHovering.ts +17 -0
@@ 0,0 1,17 @@
import { useCallback, useState } from 'react';

export const useHovering = (animate?: boolean) => {
  const [hovering, setHovering] = useState<boolean>(animate ?? false);

  const handleMouseEnter = useCallback(() => {
    if (animate) return;
    setHovering(true);
  }, [animate]);

  const handleMouseLeave = useCallback(() => {
    if (animate) return;
    setHovering(false);
  }, [animate]);

  return { hovering, handleMouseEnter, handleMouseLeave };
};

M app/javascript/flavours/glitch/is_mobile.ts => app/javascript/flavours/glitch/is_mobile.ts +16 -22
@@ 6,31 6,27 @@ const LAYOUT_BREAKPOINT = 630;
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;

export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
export const layoutFromWindow = (layout_local_setting : string): LayoutType => {
export const layoutFromWindow = (layout_local_setting: string): LayoutType => {
  switch (layout_local_setting) {
  case 'multiple':
    return 'multi-column';
  case 'single':
    if (isMobile(window.innerWidth)) {
      return 'mobile';
    } else {
      return 'single-column';
    }
  default:
    if (isMobile(window.innerWidth)) {
      return 'mobile';
    } else if (forceSingleColumn) {
      return 'single-column';
    } else {
    case 'multiple':
      return 'multi-column';
    }
    case 'single':
      if (isMobile(window.innerWidth)) {
        return 'mobile';
      } else {
        return 'single-column';
      }
    default:
      if (isMobile(window.innerWidth)) {
        return 'mobile';
      } else if (forceSingleColumn) {
        return 'single-column';
      } else {
        return 'multi-column';
      }
  }
};

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null;

const listenerOptions = supportsPassiveEvents ? { passive: true } : false;

let userTouching = false;


@@ 44,5 40,3 @@ const touchListener = () => {
window.addEventListener('touchstart', touchListener, listenerOptions);

export const isUserTouching = () => userTouching;

export const isIOS = () => iOS;

M app/javascript/flavours/glitch/main.jsx => app/javascript/flavours/glitch/main.jsx +2 -2
@@ 2,11 2,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
import Mastodon from 'flavours/glitch/containers/mastodon';
import { store } from 'flavours/glitch/store/configureStore';
import { store } from 'flavours/glitch/store';
import { me } from 'flavours/glitch/initial_state';
import ready from 'flavours/glitch/ready';

const perf = require('flavours/glitch/performance');
import * as perf from 'flavours/glitch/performance';

/**
 * @returns {Promise<void>}

M app/javascript/flavours/glitch/packs/admin.jsx => app/javascript/flavours/glitch/packs/admin.jsx +2 -3
@@ 1,10 1,9 @@
import 'packs/public-path';
import ready from 'flavours/glitch/ready';
import React from 'react';
import ReactDOM from 'react-dom';

ready(() => {
  const React    = require('react');
  const ReactDOM = require('react-dom');

  [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
    const componentName  = element.getAttribute('data-admin-component');
    const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));

M app/javascript/flavours/glitch/packs/home.js => app/javascript/flavours/glitch/packs/home.js +1 -1
@@ 1,5 1,5 @@
import 'packs/public-path';
import loadPolyfills from 'flavours/glitch/load_polyfills';
import { loadPolyfills } from 'flavours/glitch/polyfills';

loadPolyfills().then(async () => {
  const { default: main } = await import('flavours/glitch/main');

M app/javascript/flavours/glitch/packs/public.jsx => app/javascript/flavours/glitch/packs/public.jsx +9 -9
@@ 1,10 1,18 @@
import 'packs/public-path';
import loadPolyfills from 'flavours/glitch/load_polyfills';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
import axios from 'axios';
import { throttle } from 'lodash';
import { defineMessages } from 'react-intl';
import * as IntlMessageFormat  from 'intl-messageformat';
import { timeAgoString }  from 'flavours/glitch/components/relative_timestamp';
import { delegate }  from '@rails/ujs';
import emojify  from 'flavours/glitch/features/emoji/emoji';
import { getLocale }  from 'locales';
import React  from 'react';
import ReactDOM  from 'react-dom';
import { createBrowserHistory }  from 'history';

const messages = defineMessages({
  usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },


@@ 13,15 21,7 @@ const messages = defineMessages({
});

function main() {
  const IntlMessageFormat = require('intl-messageformat').default;
  const { timeAgoString } = require('flavours/glitch/components/relative_timestamp');
  const { delegate } = require('@rails/ujs');
  const emojify = require('flavours/glitch/features/emoji/emoji').default;
  const { getLocale } = require('locales');
  const { localeData } = getLocale();
  const React = require('react');
  const ReactDOM = require('react-dom');
  const { createBrowserHistory } = require('history');

  const scrollToDetailedStatus = () => {
    const history = createBrowserHistory();

M app/javascript/flavours/glitch/packs/settings.js => app/javascript/flavours/glitch/packs/settings.js +2 -3
@@ 1,11 1,10 @@
import 'packs/public-path';
import loadPolyfills from 'flavours/glitch/load_polyfills';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
import { delegate }  from '@rails/ujs';
import 'cocoon-js-vanilla';

function main() {
  const { delegate } = require('@rails/ujs');

  const toggleSidebar = () => {
    const sidebar = document.querySelector('.sidebar ul');
    const toggleButton = document.querySelector('.sidebar__toggle__icon');

M app/javascript/flavours/glitch/packs/share.jsx => app/javascript/flavours/glitch/packs/share.jsx +10 -7
@@ 1,20 1,23 @@
import 'packs/public-path';
import loadPolyfills from 'flavours/glitch/load_polyfills';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ComposeContainer from 'flavours/glitch/containers/compose_container';
import React from 'react';
import ReactDOM from 'react-dom';
import ready from 'flavours/glitch/ready';

function loaded() {
  const ComposeContainer = require('flavours/glitch/containers/compose_container').default;
  const React = require('react');
  const ReactDOM = require('react-dom');
  const mountNode = document.getElementById('mastodon-compose');

  if (mountNode !== null) {
    const props = JSON.parse(mountNode.getAttribute('data-props'));
  if (mountNode) {
    const attr = mountNode.getAttribute('data-props');
    if(!attr) return;

    const props = JSON.parse(attr);
    ReactDOM.render(<ComposeContainer {...props} />, mountNode);
  }
}

function main() {
  const ready = require('flavours/glitch/ready').default;
  ready(loaded);
}


M app/javascript/flavours/glitch/performance.js => app/javascript/flavours/glitch/performance.js +2 -4
@@ 2,9 2,8 @@
// Tools for performance debugging, only enabled in development mode.
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
// Also see config/webpack/loaders/mark.js for the webpack loader marks.
//

let marky;
import * as marky from 'marky';

if (process.env.NODE_ENV === 'development') {
  if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {


@@ 12,8 11,7 @@ if (process.env.NODE_ENV === 'development') {
    // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
    performance.setResourceTimingBufferSize(Infinity);
  }
  // eslint-disable-next-line import/no-extraneous-dependencies
  marky = require('marky');

  // allows us to easily do e.g. ReactPerf.printWasted() while debugging
  //window.ReactPerf = require('react-addons-perf');
  //window.ReactPerf.start();

M app/javascript/flavours/glitch/permissions.ts => app/javascript/flavours/glitch/permissions.ts +3 -3
@@ 1,4 1,4 @@
export const PERMISSION_INVITE_USERS      = 0x0000000000010000;
export const PERMISSION_MANAGE_USERS      = 0x0000000000000400;
export const PERMISSION_INVITE_USERS = 0x0000000000010000;
export const PERMISSION_MANAGE_USERS = 0x0000000000000400;
export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020;
export const PERMISSION_MANAGE_REPORTS    = 0x0000000000000010;
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010;

R app/javascript/flavours/glitch/base_polyfills.js => app/javascript/flavours/glitch/polyfills/base_polyfills.ts +6 -16
@@ 1,26 1,16 @@
import 'intl';
import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
import assign from 'object-assign';
import values from 'object.values';
import { decode as decodeBase64 } from './utils/base64';
import promiseFinally from 'promise.prototype.finally';

if (!Object.assign) {
  Object.assign = assign;
}

if (!Object.values) {
  values.shim();
}

promiseFinally.shim();
import 'core-js/features/object/assign';
import 'core-js/features/object/values';
import 'core-js/features/symbol';
import 'core-js/features/promise/finally';
import { decode as decodeBase64 } from '../utils/base64';

if (!HTMLCanvasElement.prototype.toBlob) {
  const BASE64_MARKER = ';base64,';

  Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
    value(callback, type = 'image/png', quality) {
    value(callback: BlobCallback, type = 'image/png', quality: any) {
      const dataURL = this.toDataURL(type, quality);
      let data;


R app/javascript/flavours/glitch/extra_polyfills.js => app/javascript/flavours/glitch/polyfills/extra_polyfills.ts +0 -0
R app/javascript/flavours/glitch/load_polyfills.js => app/javascript/flavours/glitch/polyfills/index.ts +7 -9
@@ 10,14 10,14 @@ function importExtraPolyfills() {
  return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
}

function loadPolyfills() {
export function loadPolyfills() {
  const needsBasePolyfills = !(
    HTMLCanvasElement.prototype.toBlob &&
    window.Intl &&
    Object.assign &&
    Object.values &&
    window.Symbol &&
    Promise.prototype.finally
    'toBlob' in HTMLCanvasElement.prototype &&
    'Intl' in window &&
    'assign' in Object &&
    'values' in Object &&
    'Symbol' in window &&
    'finally' in Promise.prototype
  );

  // Latest version of Firefox and Safari do not have IntersectionObserver.


@@ 36,5 36,3 @@ function loadPolyfills() {
    needsExtraPolyfills && importExtraPolyfills(),
  ]);
}

export default loadPolyfills;

M app/javascript/flavours/glitch/reducers/compose.js => app/javascript/flavours/glitch/reducers/compose.js +1 -1
@@ 54,7 54,7 @@ import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
import { REDRAFT } from 'flavours/glitch/actions/statuses';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from '../uuid';
import { uuid } from '../uuid';
import { privacyPreference } from 'flavours/glitch/utils/privacy_preference';
import { me, defaultContentType } from 'flavours/glitch/initial_state';
import { overwrite } from 'flavours/glitch/utils/js_helpers';

M app/javascript/flavours/glitch/reducers/contexts.js => app/javascript/flavours/glitch/reducers/contexts.js +1 -1
@@ 5,7 5,7 @@ import {
import { CONTEXT_FETCH_SUCCESS } from 'flavours/glitch/actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from 'flavours/glitch/actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap({
  inReplyTos: ImmutableMap(),

M app/javascript/flavours/glitch/reducers/conversations.js => app/javascript/flavours/glitch/reducers/conversations.js +1 -1
@@ 11,7 11,7 @@ import {
} from '../actions/conversations';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'flavours/glitch/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap({
  items: ImmutableList(),

R app/javascript/flavours/glitch/reducers/index.js => app/javascript/flavours/glitch/reducers/index.ts +3 -1
@@ 91,4 91,6 @@ const reducers = {
  followed_tags,
};

export default combineReducers(reducers);
const rootReducer = combineReducers(reducers);

export { rootReducer };

M app/javascript/flavours/glitch/reducers/notifications.js => app/javascript/flavours/glitch/reducers/notifications.js +1 -1
@@ 32,7 32,7 @@ import {
import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap({
  pendingItems: ImmutableList(),

M app/javascript/flavours/glitch/reducers/settings.js => app/javascript/flavours/glitch/reducers/settings.js +1 -1
@@ 6,7 6,7 @@ import { EMOJI_USE } from 'flavours/glitch/actions/emojis';
import { LANGUAGE_USE } from 'flavours/glitch/actions/languages';
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
import { Map as ImmutableMap, fromJS } from 'immutable';
import uuid from '../uuid';
import { uuid } from '../uuid';

const initialState = ImmutableMap({
  saved: true,

M app/javascript/flavours/glitch/reducers/timelines.js => app/javascript/flavours/glitch/reducers/timelines.js +1 -1
@@ 17,7 17,7 @@ import {
  ACCOUNT_UNFOLLOW_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap();


M app/javascript/flavours/glitch/scroll.ts => app/javascript/flavours/glitch/scroll.ts +27 -10
@@ 1,13 1,23 @@
const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
const easingOutQuint = (
  x: number,
  t: number,
  b: number,
  c: number,
  d: number
) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
const scroll = (
  node: Element,
  key: 'scrollTop' | 'scrollLeft',
  target: number
) => {
  const startTime = Date.now();
  const offset    = node[key];
  const gap       = target - offset;
  const duration  = 1000;
  let interrupt   = false;
  const offset = node[key];
  const gap = target - offset;
  const duration = 1000;
  let interrupt = false;

  const step = () => {
    const elapsed    = Date.now() - startTime;
    const elapsed = Date.now() - startTime;
    const percentage = elapsed / duration;

    if (percentage > 1 || interrupt) {


@@ 25,7 35,14 @@ const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) 
  };
};

const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
const isScrollBehaviorSupported =
  'scrollBehavior' in document.documentElement.style;

export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
export const scrollRight = (node: Element, position: number) =>
  isScrollBehaviorSupported
    ? node.scrollTo({ left: position, behavior: 'smooth' })
    : scroll(node, 'scrollLeft', position);
export const scrollTop = (node: Element) =>
  isScrollBehaviorSupported
    ? node.scrollTo({ top: 0, behavior: 'smooth' })
    : scroll(node, 'scrollTop', 0);

D app/javascript/flavours/glitch/store/configureStore.js => app/javascript/flavours/glitch/store/configureStore.js +0 -16
@@ 1,16 0,0 @@
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import appReducer from '../reducers';
import loadingBarMiddleware from '../middleware/loading_bar';
import errorsMiddleware from '../middleware/errors';
import soundsMiddleware from '../middleware/sounds';

export const store = configureStore({
  reducer: appReducer,
  middleware: [
    thunk,
    loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
    errorsMiddleware(),
    soundsMiddleware(),
  ],
});

A app/javascript/flavours/glitch/store/index.ts => app/javascript/flavours/glitch/store/index.ts +27 -0
@@ 0,0 1,27 @@
import { configureStore } from '@reduxjs/toolkit';
import { rootReducer } from '../reducers';
import { loadingBarMiddleware } from './middlewares/loading_bar';
import { errorsMiddleware } from './middlewares/errors';
import { soundsMiddleware } from './middlewares/sounds';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware()
      .concat(
        loadingBarMiddleware({
          promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
        })
      )
      .concat(errorsMiddleware)
      .concat(soundsMiddleware()),
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

R app/javascript/flavours/glitch/middleware/errors.js => app/javascript/flavours/glitch/store/middlewares/errors.ts +6 -3
@@ 1,9 1,13 @@
import { Middleware } from 'redux';
import { showAlertForError } from 'flavours/glitch/actions/alerts';
import { RootState } from '..';

const defaultFailSuffix = 'FAIL';

export default function errorsMiddleware() {
  return ({ dispatch }) => next => action => {
export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
  ({ dispatch }) =>
  (next) =>
  (action) => {
    if (action.type && !action.skipAlert) {
      const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');



@@ 14,4 18,3 @@ export default function errorsMiddleware() {

    return next(action);
  };
}

R app/javascript/flavours/glitch/middleware/loading_bar.js => app/javascript/flavours/glitch/store/middlewares/loading_bar.ts +33 -16
@@ 1,25 1,42 @@
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { Middleware } from 'redux';
import { RootState } from '..';

const defaultTypeSuffixes = ['PENDING', 'FULFILLED', 'REJECTED'];
interface Config {
  promiseTypeSuffixes?: string[];
}

const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
  'PENDING',
  'FULFILLED',
  'REJECTED',
];

export default function loadingBarMiddleware(config = {}) {
export const loadingBarMiddleware = (
  config: Config = {}
): Middleware<Record<string, never>, RootState> => {
  const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;

  return ({ dispatch }) => next => (action) => {
    if (action.type && !action.skipLoading) {
      const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
  return ({ dispatch }) =>
    (next) =>
    (action) => {
      if (action.type && !action.skipLoading) {
        const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;

      const isPending = new RegExp(`${PENDING}$`, 'g');
      const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
      const isRejected = new RegExp(`${REJECTED}$`, 'g');
        const isPending = new RegExp(`${PENDING}$`, 'g');
        const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
        const isRejected = new RegExp(`${REJECTED}$`, 'g');

      if (action.type.match(isPending)) {
        dispatch(showLoading());
      } else if (action.type.match(isFulfilled) || action.type.match(isRejected)) {
        dispatch(hideLoading());
        if (action.type.match(isPending)) {
          dispatch(showLoading());
        } else if (
          action.type.match(isFulfilled) ||
          action.type.match(isRejected)
        ) {
          dispatch(hideLoading());
        }
      }
    }

    return next(action);
  };
}
      return next(action);
    };
};

R app/javascript/flavours/glitch/middleware/sounds.js => app/javascript/flavours/glitch/store/middlewares/sounds.ts +21 -8
@@ 1,4 1,12 @@
const createAudio = sources => {
import { Middleware, AnyAction } from 'redux';
import { RootState } from '..';

interface AudioSource {
  src: string;
  type: string;
}

const createAudio = (sources: AudioSource[]) => {
  const audio = new Audio();
  sources.forEach(({ type, src }) => {
    const source = document.createElement('source');


@@ 9,7 17,7 @@ const createAudio = sources => {
  return audio;
};

const play = audio => {
const play = (audio: HTMLAudioElement) => {
  if (!audio.paused) {
    audio.pause();
    if (typeof audio.fastSeek === 'function') {


@@ 22,8 30,11 @@ const play = audio => {
  audio.play();
};

export default function soundsMiddleware() {
  const soundCache = {
export const soundsMiddleware = (): Middleware<
  Record<string, never>,
  RootState
> => {
  const soundCache: { [key: string]: HTMLAudioElement } = {
    boop: createAudio([
      {
        src: '/sounds/boop.ogg',


@@ 36,11 47,13 @@ export default function soundsMiddleware() {
    ]),
  };

  return () => next => action => {
    if (action.meta && action.meta.sound && soundCache[action.meta.sound]) {
      play(soundCache[action.meta.sound]);
  return () => (next) => (action: AnyAction) => {
    const sound = action?.meta?.sound;

    if (sound && soundCache[sound]) {
      play(soundCache[sound]);
    }

    return next(action);
  };
}
};

M app/javascript/flavours/glitch/types/resources.ts => app/javascript/flavours/glitch/types/resources.ts +48 -4
@@ 1,10 1,54 @@
import type { Record } from 'immutable';

type AccountValues = {
  id: number;
type CustomEmoji = Record<{
  shortcode: string;
  static_url: string;
  url: string;
}>;

type AccountField = Record<{
  name: string;
  value: string;
  verified_at: string | null;
}>;

type AccountApiResponseValues = {
  acct: string;
  avatar: string;
  avatar_static: string;
  [key: string]: any;
  bot: boolean;
  created_at: string;
  discoverable: boolean;
  display_name: string;
  emojis: CustomEmoji[];
  fields: AccountField[];
  followers_count: number;
  following_count: number;
  group: boolean;
  header: string;
  header_static: string;
  id: string;
  last_status_at: string;
  locked: boolean;
  note: string;
  statuses_count: number;
  url: string;
  username: string;
};

type NormalizedAccountField = Record<{
  name_emojified: string;
  value_emojified: string;
  value_plain: string;
}>;

type NormalizedAccountValues = {
  display_name_html: string;
  fields: NormalizedAccountField[];
  note_emojified: string;
  note_plain: string;
};

export type Account = Record<AccountValues>;
export type Account = Record<
  AccountApiResponseValues & NormalizedAccountValues
>;

M app/javascript/flavours/glitch/utils/filters.ts => app/javascript/flavours/glitch/utils/filters.ts +12 -12
@@ 1,16 1,16 @@
export const toServerSideType = (columnType: string) => {
  switch (columnType) {
  case 'home':
  case 'notifications':
  case 'public':
  case 'thread':
  case 'account':
    return columnType;
  default:
    if (columnType.indexOf('list:') > -1) {
      return 'home';
    } else {
      return 'public'; // community, account, hashtag
    }
    case 'home':
    case 'notifications':
    case 'public':
    case 'thread':
    case 'account':
      return columnType;
    default:
      if (columnType.indexOf('list:') > -1) {
        return 'home';
      } else {
        return 'public'; // community, account, hashtag
      }
  }
};

M app/javascript/flavours/glitch/utils/numbers.ts => app/javascript/flavours/glitch/utils/numbers.ts +6 -7
@@ 21,7 21,7 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 * shortNumber(5936);
 * // => [5.936, 1000, 1]
 */
export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits
export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
export function toShortNumber(sourceNumber: number): ShortNumber {
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];


@@ 38,11 38,7 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
      sourceNumber < TEN_MILLIONS ? 1 : 0,
    ];
  } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
    return [
      sourceNumber / DECIMAL_UNITS.BILLION,
      DECIMAL_UNITS.BILLION,
      0,
    ];
    return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
  }

  return [sourceNumber, DECIMAL_UNITS.ONE, 0];


@@ 56,7 52,10 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 * // => 1790
 */
export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
export function pluralReady(
  sourceNumber: number,
  division: DecimalUnits
): number {
  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
    return sourceNumber;
  }

M app/javascript/flavours/glitch/uuid.ts => app/javascript/flavours/glitch/uuid.ts +7 -2
@@ 1,3 1,8 @@
export default function uuid(a?: string): string {
  return a ? ((a as any as number) ^ Math.random() * 16 >> (a as any as number) / 4).toString(16) : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
export function uuid(a?: string): string {
  return a
    ? (
        (a as any as number) ^
        ((Math.random() * 16) >> ((a as any as number) / 4))
      ).toString(16)
    : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
}

M app/javascript/mastodon/actions/app.ts => app/javascript/mastodon/actions/app.ts +2 -1
@@ 1,10 1,11 @@
import { createAction } from '@reduxjs/toolkit';
import type { LayoutType } from '../is_mobile';

export const focusApp = createAction('APP_FOCUS');
export const unfocusApp = createAction('APP_UNFOCUS');

type ChangeLayoutPayload = {
  layout: 'mobile' | 'single-column' | 'multi-column';
  layout: LayoutType;
};
export const changeLayout =
  createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');

M app/javascript/mastodon/actions/markers.js => app/javascript/mastodon/actions/markers.js +1 -1
@@ 1,6 1,6 @@
import api from '../api';
import { debounce } from 'lodash';
import compareId from '../compare_id';
import { compareId } from '../compare_id';
import { List as ImmutableList } from 'immutable';

export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';

M app/javascript/mastodon/actions/notifications.js => app/javascript/mastodon/actions/notifications.js +1 -1
@@ 13,7 13,7 @@ import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from '../utils/html';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import compareId from 'mastodon/compare_id';
import { compareId } from 'mastodon/compare_id';
import { requestNotificationPermission } from '../utils/notifications';

export const NOTIFICATIONS_UPDATE      = 'NOTIFICATIONS_UPDATE';

M app/javascript/mastodon/actions/streaming.js => app/javascript/mastodon/actions/streaming.js +2 -0
@@ 52,8 52,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
    /**
     * @param {function(Function, Function): void} fallback
     */

    const useFallback = fallback => {
      fallback(dispatch, () => {
        // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
        pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
      });
    };

M app/javascript/mastodon/actions/timelines.js => app/javascript/mastodon/actions/timelines.js +1 -1
@@ 2,7 2,7 @@ import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers';
import api, { getLinks } from 'mastodon/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'mastodon/compare_id';
import { compareId } from 'mastodon/compare_id';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';

export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';

M app/javascript/mastodon/blurhash.ts => app/javascript/mastodon/blurhash.ts +2 -2
@@ 98,9 98,9 @@ export const decode83 = (str: string) => {
};

export const intToRGB = (int: number) => ({
  r: Math.max(0, (int >> 16)),
  r: Math.max(0, int >> 16),
  g: Math.max(0, (int >> 8) & 255),
  b: Math.max(0, (int & 255)),
  b: Math.max(0, int & 255),
});

export const getAverageFromBlurhash = (blurhash: string) => {

M app/javascript/mastodon/common.js => app/javascript/mastodon/common.js +1 -1
@@ 1,7 1,7 @@
import Rails from '@rails/ujs';
import 'font-awesome/css/font-awesome.css';

export function start() {
  require('font-awesome/css/font-awesome.css');
  require.context('../images/', true);

  try {

M app/javascript/mastodon/compare_id.ts => app/javascript/mastodon/compare_id.ts +1 -1
@@ 1,4 1,4 @@
export default function compareId (id1: string, id2: string) {
export function compareId(id1: string, id2: string) {
  if (id1 === id2) {
    return 0;
  }

M app/javascript/mastodon/components/__tests__/avatar-test.jsx => app/javascript/mastodon/components/__tests__/avatar-test.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import Avatar from '../avatar';
import { Avatar } from '../avatar';

describe('<Avatar />', () => {
  const account = fromJS({

M app/javascript/mastodon/components/__tests__/avatar_overlay-test.jsx => app/javascript/mastodon/components/__tests__/avatar_overlay-test.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import AvatarOverlay from '../avatar_overlay';
import { AvatarOverlay } from '../avatar_overlay';

describe('<AvatarOverlay', () => {
  const account = fromJS({

M app/javascript/mastodon/components/account.jsx => app/javascript/mastodon/components/account.jsx +5 -5
@@ 1,19 1,19 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import { Avatar } from './avatar';
import DisplayName from './display_name';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
import RelativeTimestamp from './relative_timestamp';
import { RelativeTimestamp } from './relative_timestamp';
import Skeleton from 'mastodon/components/skeleton';
import { Link } from 'react-router-dom';
import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
import classNames from 'classnames';
import VerifiedBadge from 'mastodon/components/verified_badge';
import { VerifiedBadge } from 'mastodon/components/verified_badge';

const messages = defineMessages({
  follow: { id: 'account.follow', defaultMessage: 'Follow' },


@@ 151,7 151,7 @@ class Account extends ImmutablePureComponent {
    const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));

    if (firstVerifiedField) {
      verification = <>· <VerifiedBadge link={firstVerifiedField.get('value')} verifiedAt={firstVerifiedField.get('verified_at')} /></>;
      verification = <>· <VerifiedBadge link={firstVerifiedField.get('value')} /></>;
    }

    return (

M app/javascript/mastodon/components/animated_number.tsx => app/javascript/mastodon/components/animated_number.tsx +34 -18
@@ 16,13 16,10 @@ const obfuscatedCount = (count: number) => {
type Props = {
  value: number;
  obfuscate?: boolean;
}
export const AnimatedNumber: React.FC<Props> = ({
  value,
  obfuscate,
})=> {
};
export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
  const [previousValue, setPreviousValue] = useState(value);
  const [direction, setDirection] = useState<1|-1>(1);
  const [direction, setDirection] = useState<1 | -1>(1);

  if (previousValue !== value) {
    setPreviousValue(value);


@@ 30,29 27,48 @@ export const AnimatedNumber: React.FC<Props> = ({
  }

  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
  const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]);
  const willLeave = useCallback(
    () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
    [direction]
  );

  if (reduceMotion) {
    return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />;
    return obfuscate ? (
      <>{obfuscatedCount(value)}</>
    ) : (
      <ShortNumber value={value} />
    );
  }

  const styles = [{
    key: `${value}`,
    data: value,
    style: { y: spring(0, { damping: 35, stiffness: 400 }) },
  }];
  const styles = [
    {
      key: `${value}`,
      data: value,
      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
    },
  ];

  return (
    <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
      {items => (
    <TransitionMotion
      styles={styles}
      willEnter={willEnter}
      willLeave={willLeave}
    >
      {(items) => (
        <span className='animated-number'>
          {items.map(({ key, data, style }) => (
            <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
            <span
              key={key}
              style={{
                position: direction * style.y > 0 ? 'absolute' : 'static',
                transform: `translateY(${style.y * 100}%)`,
              }}
            >
              {obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}
            </span>
          ))}
        </span>
      )}
    </TransitionMotion>
  );
};

export default AnimatedNumber;

M app/javascript/mastodon/components/attachment_list.jsx => app/javascript/mastodon/components/attachment_list.jsx +1 -1
@@ 4,7 4,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const filename = url => url.split('/').pop().split('#')[0].split('?')[0];


M app/javascript/mastodon/components/avatar.tsx => app/javascript/mastodon/components/avatar.tsx +0 -2
@@ 45,5 45,3 @@ export const Avatar: React.FC<Props> = ({
    </div>
  );
};

export default Avatar;

M app/javascript/mastodon/components/avatar_composite.jsx => app/javascript/mastodon/components/avatar_composite.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
import Avatar from './avatar';
import { Avatar } from './avatar';

export default class AvatarComposite extends React.PureComponent {


M app/javascript/mastodon/components/avatar_overlay.tsx => app/javascript/mastodon/components/avatar_overlay.tsx +10 -6
@@ 18,13 18,19 @@ export const AvatarOverlay: React.FC<Props> = ({
  baseSize = 36,
  overlaySize = 24,
}) => {
  const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif);
  const accountSrc = hovering ? account?.get('avatar') : account?.get('avatar_static');
  const friendSrc = hovering ? friend?.get('avatar') : friend?.get('avatar_static');
  const { hovering, handleMouseEnter, handleMouseLeave } =
    useHovering(autoPlayGif);
  const accountSrc = hovering
    ? account?.get('avatar')
    : account?.get('avatar_static');
  const friendSrc = hovering
    ? friend?.get('avatar')
    : friend?.get('avatar_static');

  return (
    <div
      className='account__avatar-overlay' style={{ width: size, height: size }}
      className='account__avatar-overlay'
      style={{ width: size, height: size }}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
    >


@@ 47,5 53,3 @@ export const AvatarOverlay: React.FC<Props> = ({
    </div>
  );
};

export default AvatarOverlay;

M app/javascript/mastodon/components/blurhash.tsx => app/javascript/mastodon/components/blurhash.tsx +7 -5
@@ 8,14 8,14 @@ type Props = {
  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
  children?: never;
  [key: string]: any;
}
function Blurhash({
};
const Blurhash: React.FC<Props> = ({
  hash,
  width = 32,
  height = width,
  dummy = false,
  ...canvasProps
}: Props) {
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {


@@ 40,6 40,8 @@ function Blurhash({
  return (
    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
  );
}
};

export default React.memo(Blurhash);
const MemoizedBlurhash = React.memo(Blurhash);

export { MemoizedBlurhash as Blurhash };

D app/javascript/mastodon/components/check.jsx => app/javascript/mastodon/components/check.jsx +0 -9
@@ 1,9 0,0 @@
import React from 'react';

const Check = () => (
  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
    <path fillRule='evenodd' d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z' clipRule='evenodd' />
  </svg>
);

export default Check;

A app/javascript/mastodon/components/check.tsx => app/javascript/mastodon/components/check.tsx +15 -0
@@ 0,0 1,15 @@
import React from 'react';

export const Check: React.FC = () => (
  <svg
    xmlns='http://www.w3.org/2000/svg'
    viewBox='0 0 20 20'
    fill='currentColor'
  >
    <path
      fillRule='evenodd'
      d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'
      clipRule='evenodd'
    />
  </svg>
);

M app/javascript/mastodon/components/column_back_button.jsx => app/javascript/mastodon/components/column_back_button.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { createPortal } from 'react-dom';

export default class ColumnBackButton extends React.PureComponent {

M app/javascript/mastodon/components/column_back_button_slim.jsx => app/javascript/mastodon/components/column_back_button_slim.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import ColumnBackButton from './column_back_button';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

export default class ColumnBackButtonSlim extends ColumnBackButton {


M app/javascript/mastodon/components/column_header.jsx => app/javascript/mastodon/components/column_header.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const messages = defineMessages({
  show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },

M app/javascript/mastodon/components/dismissable_banner.jsx => app/javascript/mastodon/components/dismissable_banner.jsx +1 -1
@@ 1,5 1,5 @@
import React from 'react';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import { bannerSettings } from 'mastodon/settings';

D app/javascript/mastodon/components/domain.jsx => app/javascript/mastodon/components/domain.jsx +0 -43
@@ 1,43 0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';

const messages = defineMessages({
  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});

class Account extends ImmutablePureComponent {

  static propTypes = {
    domain: PropTypes.string,
    onUnblockDomain: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };

  handleDomainUnblock = () => {
    this.props.onUnblockDomain(this.props.domain);
  };

  render () {
    const { domain, intl } = this.props;

    return (
      <div className='domain'>
        <div className='domain__wrapper'>
          <span className='domain__domain-name'>
            <strong>{domain}</strong>
          </span>

          <div className='domain__buttons'>
            <IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
          </div>
        </div>
      </div>
    );
  }

}

export default injectIntl(Account);

A app/javascript/mastodon/components/domain.tsx => app/javascript/mastodon/components/domain.tsx +42 -0
@@ 0,0 1,42 @@
import React, { useCallback } from 'react';
import { IconButton } from './icon_button';
import { InjectedIntl, defineMessages, injectIntl } from 'react-intl';

const messages = defineMessages({
  unblockDomain: {
    id: 'account.unblock_domain',
    defaultMessage: 'Unblock domain {domain}',
  },
});

type Props = {
  domain: string;
  onUnblockDomain: (domain: string) => void;
  intl: InjectedIntl;
};
const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
  const handleDomainUnblock = useCallback(() => {
    onUnblockDomain(domain);
  }, [domain, onUnblockDomain]);

  return (
    <div className='domain'>
      <div className='domain__wrapper'>
        <span className='domain__domain-name'>
          <strong>{domain}</strong>
        </span>

        <div className='domain__buttons'>
          <IconButton
            active
            icon='unlock'
            title={intl.formatMessage(messages.unblockDomain, { domain })}
            onClick={handleDomainUnblock}
          />
        </div>
      </div>
    </div>
  );
};

export const Domain = injectIntl(_Domain);

M app/javascript/mastodon/components/dropdown_menu.jsx => app/javascript/mastodon/components/dropdown_menu.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import Overlay from 'react-overlays/Overlay';
import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';

M app/javascript/mastodon/components/edited_timestamp/index.jsx => app/javascript/mastodon/components/edited_timestamp/index.jsx +2 -2
@@ 1,11 1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import DropdownMenu from './containers/dropdown_menu_container';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import InlineAccount from 'mastodon/components/inline_account';

const mapDispatchToProps = (dispatch, { statusId }) => ({

M app/javascript/mastodon/components/gifv.tsx => app/javascript/mastodon/components/gifv.tsx +15 -13
@@ 8,7 8,7 @@ type Props = {
  width: number;
  height: number;
  onClick?: () => void;
}
};

export const GIFV: React.FC<Props> = ({
  src,


@@ 17,19 17,23 @@ export const GIFV: React.FC<Props> = ({
  width,
  height,
  onClick,
})=> {
}) => {
  const [loading, setLoading] = useState(true);

  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
    setLoading(false);
  }, [setLoading]);
  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
    useCallback(() => {
      setLoading(false);
    }, [setLoading]);

  const handleClick: React.MouseEventHandler = useCallback((e) => {
    if (onClick) {
      e.stopPropagation();
      onClick();
    }
  }, [onClick]);
  const handleClick: React.MouseEventHandler = useCallback(
    (e) => {
      if (onClick) {
        e.stopPropagation();
        onClick();
      }
    },
    [onClick]
  );

  return (
    <div className='gifv' style={{ position: 'relative' }}>


@@ 64,5 68,3 @@ export const GIFV: React.FC<Props> = ({
    </div>
  );
};

export default GIFV;

M app/javascript/mastodon/components/hashtag.jsx => app/javascript/mastodon/components/hashtag.jsx +0 -2
@@ 5,9 5,7 @@ import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
// @ts-expect-error
import ShortNumber from 'mastodon/components/short_number';
// @ts-expect-error
import Skeleton from 'mastodon/components/skeleton';
import classNames from 'classnames';


M app/javascript/mastodon/components/icon.tsx => app/javascript/mastodon/components/icon.tsx +12 -5
@@ 7,8 7,15 @@ type Props = {
  fixedWidth?: boolean;
  children?: never;
  [key: string]: any;
}
export const Icon: React.FC<Props> = ({ id, className, fixedWidth, ...other }) =>
  <i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />;

export default Icon;
};
export const Icon: React.FC<Props> = ({
  id,
  className,
  fixedWidth,
  ...other
}) => (
  <i
    className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
    {...other}
  />
);

M app/javascript/mastodon/components/icon_button.tsx => app/javascript/mastodon/components/icon_button.tsx +15 -15
@@ 25,13 25,12 @@ type Props = {
  obfuscateCount?: boolean;
  href?: string;
  ariaHidden: boolean;
}
};
type States = {
  activate: boolean,
  deactivate: boolean,
}
export default class IconButton extends React.PureComponent<Props, States> {

  activate: boolean;
  deactivate: boolean;
};
export class IconButton extends React.PureComponent<Props, States> {
  static defaultProps = {
    size: 18,
    active: false,


@@ 47,7 46,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
    deactivate: false,
  };

  UNSAFE_componentWillReceiveProps (nextProps: Props) {
  UNSAFE_componentWillReceiveProps(nextProps: Props) {
    if (!nextProps.animate) return;

    if (this.props.active && !nextProps.active) {


@@ 57,7 56,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
    }
  }

  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) =>  {
  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
    e.preventDefault();

    if (!this.props.disabled && this.props.onClick != null) {


@@ 83,7 82,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
    }
  };

  render () {
  render() {
    const style = {
      fontSize: `${this.props.size}px`,
      width: `${this.props.size * 1.28571429}px`,


@@ 109,10 108,7 @@ export default class IconButton extends React.PureComponent<Props, States> {
      ariaHidden,
    } = this.props;

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

    const classes = classNames(className, 'icon-button', {
      active,


@@ 130,7 126,12 @@ export default class IconButton extends React.PureComponent<Props, States> {

    let contents = (
      <React.Fragment>
        <Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
        <Icon id={icon} fixedWidth aria-hidden='true' />{' '}
        {typeof counter !== 'undefined' && (
          <span className='icon-button__counter'>
            <AnimatedNumber value={counter} obfuscate={obfuscateCount} />
          </span>
        )}
      </React.Fragment>
    );



@@ 162,5 163,4 @@ export default class IconButton extends React.PureComponent<Props, States> {
      </button>
    );
  }

}

M app/javascript/mastodon/components/icon_with_badge.tsx => app/javascript/mastodon/components/icon_with_badge.tsx +11 -6
@@ 1,20 1,25 @@
import React from 'react';
import { Icon } from './icon';

const formatNumber = (num: number): number | string => num > 40 ? '40+' : num;
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);

type Props = {
  id: string;
  count: number;
  issueBadge: boolean;
  className: string;
}
const IconWithBadge: React.FC<Props> = ({ id, count, issueBadge, className }) => (
};
export const IconWithBadge: React.FC<Props> = ({
  id,
  count,
  issueBadge,
  className,
}) => (
  <i className='icon-with-badge'>
    <Icon id={id} fixedWidth className={className} />
    {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
    {count > 0 && (
      <i className='icon-with-badge__badge'>{formatNumber(count)}</i>
    )}
    {issueBadge && <i className='icon-with-badge__issue-badge' />}
  </i>
);

export default IconWithBadge;

D app/javascript/mastodon/components/image.jsx => app/javascript/mastodon/components/image.jsx +0 -33
@@ 1,33 0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Blurhash from './blurhash';
import classNames from 'classnames';

export default class Image extends React.PureComponent {

  static propTypes = {
    src: PropTypes.string,
    srcSet: PropTypes.string,
    blurhash: PropTypes.string,
    className: PropTypes.string,
  };

  state = {
    loaded: false,
  };

  handleLoad = () => this.setState({ loaded: true });

  render () {
    const { src, srcSet, blurhash, className } = this.props;
    const { loaded } = this.state;

    return (
      <div className={classNames('image', { loaded }, className)} role='presentation'>
        {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
        <img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} />
      </div>
    );
  }

}

A app/javascript/mastodon/components/image.tsx => app/javascript/mastodon/components/image.tsx +33 -0
@@ 0,0 1,33 @@
import React, { useCallback, useState } from 'react';
import { Blurhash } from './blurhash';
import classNames from 'classnames';

type Props = {
  src: string;
  srcSet?: string;
  blurhash?: string;
  className?: string;
};

export const Image: React.FC<Props> = ({
  src,
  srcSet,
  blurhash,
  className,
}) => {
  const [loaded, setLoaded] = useState(false);

  const handleLoad = useCallback(() => {
    setLoaded(true);
  }, [setLoaded]);

  return (
    <div
      className={classNames('image', { loaded }, className)}
      role='presentation'
    >
      {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
      <img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
    </div>
  );
};

M app/javascript/mastodon/components/inline_account.jsx => app/javascript/mastodon/components/inline_account.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import Avatar from 'mastodon/components/avatar';
import { Avatar } from 'mastodon/components/avatar';

const makeMapStateToProps = () => {
  const getAccount = makeGetAccount();

M app/javascript/mastodon/components/load_gap.jsx => app/javascript/mastodon/components/load_gap.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const messages = defineMessages({
  load_more: { id: 'status.load_more', defaultMessage: 'Load more' },

M app/javascript/mastodon/components/media_gallery.jsx => app/javascript/mastodon/components/media_gallery.jsx +2 -2
@@ 2,12 2,12 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { is } from 'immutable';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
import { debounce } from 'lodash';
import Blurhash from 'mastodon/components/blurhash';
import { Blurhash } from 'mastodon/components/blurhash';

const messages = defineMessages({
  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },

R app/javascript/mastodon/components/not_signed_in_indicator.jsx => app/javascript/mastodon/components/not_signed_in_indicator.tsx +5 -4
@@ 1,12 1,13 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';

const NotSignedInIndicator = () => (
export const NotSignedInIndicator: React.FC = () => (
  <div className='scrollable scrollable--flex'>
    <div className='empty-column-indicator'>
      <FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' />
      <FormattedMessage
        id='not_signed_in_indicator.not_signed_in'
        defaultMessage='You need to sign in to access this resource.'
      />
    </div>
  </div>
);

export default NotSignedInIndicator;

M app/javascript/mastodon/components/picture_in_picture_placeholder.jsx => app/javascript/mastodon/components/picture_in_picture_placeholder.jsx +1 -1
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';

M app/javascript/mastodon/components/poll.jsx => app/javascript/mastodon/components/poll.jsx +2 -2
@@ 8,8 8,8 @@ import Motion from 'mastodon/features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'mastodon/features/emoji/emoji';
import RelativeTimestamp from './relative_timestamp';
import Icon from 'mastodon/components/icon';
import { RelativeTimestamp } from './relative_timestamp';
import { Icon }  from 'mastodon/components/icon';

const messages = defineMessages({
  closed: {

R app/javascript/mastodon/components/radio_button.jsx => app/javascript/mastodon/components/radio_button.tsx +28 -29
@@ 1,35 1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

export default class RadioButton extends React.PureComponent {
type Props = {
  value: string;
  checked: boolean;
  name: string;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  label: React.ReactNode;
};

  static propTypes = {
    value: PropTypes.string.isRequired,
    checked: PropTypes.bool,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    label: PropTypes.node.isRequired,
  };
export const RadioButton: React.FC<Props> = ({
  name,
  value,
  checked,
  onChange,
  label,
}) => {
  return (
    <label className='radio-button'>
      <input
        name={name}
        type='radio'
        value={value}
        checked={checked}
        onChange={onChange}
      />

  render () {
    const { name, value, checked, onChange, label } = this.props;
      <span className={classNames('radio-button__input', { checked })} />

    return (
      <label className='radio-button'>
        <input
          name={name}
          type='radio'
          value={value}
          checked={checked}
          onChange={onChange}
        />

        <span className={classNames('radio-button__input', { checked })} />

        <span>{label}</span>
      </label>
    );
  }

}
      <span>{label}</span>
    </label>
  );
};

M app/javascript/mastodon/components/relative_timestamp.tsx => app/javascript/mastodon/components/relative_timestamp.tsx +135 -58
@@ 4,20 4,50 @@ import { injectIntl, defineMessages, InjectedIntl } from 'react-intl';
const messages = defineMessages({
  today: { id: 'relative_time.today', defaultMessage: 'today' },
  just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
  just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
  just_now_full: {
    id: 'relative_time.full.just_now',
    defaultMessage: 'just now',
  },
  seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
  seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
  seconds_full: {
    id: 'relative_time.full.seconds',
    defaultMessage: '{number, plural, one {# second} other {# seconds}} ago',
  },
  minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
  minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
  minutes_full: {
    id: 'relative_time.full.minutes',
    defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago',
  },
  hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
  hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
  hours_full: {
    id: 'relative_time.full.hours',
    defaultMessage: '{number, plural, one {# hour} other {# hours}} ago',
  },
  days: { id: 'relative_time.days', defaultMessage: '{number}d' },
  days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
  moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
  seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
  minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
  hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
  days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
  days_full: {
    id: 'relative_time.full.days',
    defaultMessage: '{number, plural, one {# day} other {# days}} ago',
  },
  moments_remaining: {
    id: 'time_remaining.moments',
    defaultMessage: 'Moments remaining',
  },
  seconds_remaining: {
    id: 'time_remaining.seconds',
    defaultMessage: '{number, plural, one {# second} other {# seconds}} left',
  },
  minutes_remaining: {
    id: 'time_remaining.minutes',
    defaultMessage: '{number, plural, one {# minute} other {# minutes}} left',
  },
  hours_remaining: {
    id: 'time_remaining.hours',
    defaultMessage: '{number, plural, one {# hour} other {# hours}} left',
  },
  days_remaining: {
    id: 'time_remaining.days',
    defaultMessage: '{number, plural, one {# day} other {# days}} left',
  },
});

const dateFormatOptions = {


@@ 36,8 66,8 @@ const shortDateFormatOptions = {

const SECOND = 1000;
const MINUTE = 1000 * 60;
const HOUR   = 1000 * 60 * 60;
const DAY    = 1000 * 60 * 60 * 24;
const HOUR = 1000 * 60 * 60;
const DAY = 1000 * 60 * 60 * 24;

const MAX_DELAY = 2147483647;



@@ 57,20 87,27 @@ const selectUnits = (delta: number) => {

const getUnitDelay = (units: string) => {
  switch (units) {
  case 'second':
    return SECOND;
  case 'minute':
    return MINUTE;
  case 'hour':
    return HOUR;
  case 'day':
    return DAY;
  default:
    return MAX_DELAY;
    case 'second':
      return SECOND;
    case 'minute':
      return MINUTE;
    case 'hour':
      return HOUR;
    case 'day':
      return DAY;
    default:
      return MAX_DELAY;
  }
};

export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: number, timeGiven: boolean, short?: boolean) => {
export const timeAgoString = (
  intl: InjectedIntl,
  date: Date,
  now: number,
  year: number,
  timeGiven: boolean,
  short?: boolean
) => {
  const delta = now - date.getTime();

  let relativeTime;


@@ 78,27 115,49 @@ export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year:
  if (delta < DAY && !timeGiven) {
    relativeTime = intl.formatMessage(messages.today);
  } else if (delta < 10 * SECOND) {
    relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
    relativeTime = intl.formatMessage(
      short ? messages.just_now : messages.just_now_full
    );
  } else if (delta < 7 * DAY) {
    if (delta < MINUTE) {
      relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
      relativeTime = intl.formatMessage(
        short ? messages.seconds : messages.seconds_full,
        { number: Math.floor(delta / SECOND) }
      );
    } else if (delta < HOUR) {
      relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
      relativeTime = intl.formatMessage(
        short ? messages.minutes : messages.minutes_full,
        { number: Math.floor(delta / MINUTE) }
      );
    } else if (delta < DAY) {
      relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
      relativeTime = intl.formatMessage(
        short ? messages.hours : messages.hours_full,
        { number: Math.floor(delta / HOUR) }
      );
    } else {
      relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
      relativeTime = intl.formatMessage(
        short ? messages.days : messages.days_full,
        { number: Math.floor(delta / DAY) }
      );
    }
  } else if (date.getFullYear() === year) {
    relativeTime = intl.formatDate(date, shortDateFormatOptions);
  } else {
    relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
    relativeTime = intl.formatDate(date, {
      ...shortDateFormatOptions,
      year: 'numeric',
    });
  }

  return relativeTime;
};

const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGiven = true) => {
const timeRemainingString = (
  intl: InjectedIntl,
  date: Date,
  now: number,
  timeGiven = true
) => {
  const delta = date.getTime() - now;

  let relativeTime;


@@ 108,13 167,21 @@ const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGi
  } else if (delta < 10 * SECOND) {
    relativeTime = intl.formatMessage(messages.moments_remaining);
  } else if (delta < MINUTE) {
    relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
    relativeTime = intl.formatMessage(messages.seconds_remaining, {
      number: Math.floor(delta / SECOND),
    });
  } else if (delta < HOUR) {
    relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
    relativeTime = intl.formatMessage(messages.minutes_remaining, {
      number: Math.floor(delta / MINUTE),
    });
  } else if (delta < DAY) {
    relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
    relativeTime = intl.formatMessage(messages.hours_remaining, {
      number: Math.floor(delta / HOUR),
    });
  } else {
    relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
    relativeTime = intl.formatMessage(messages.days_remaining, {
      number: Math.floor(delta / DAY),
    });
  }

  return relativeTime;


@@ 126,78 193,88 @@ type Props = {
  year: number;
  futureDate?: boolean;
  short?: boolean;
}
};
type States = {
  now: number;
}
};
class RelativeTimestamp extends React.Component<Props, States> {

  state = {
    now: this.props.intl.now(),
  };

  static defaultProps = {
    year: (new Date()).getFullYear(),
    year: new Date().getFullYear(),
    short: true,
  };

  _timer: number | undefined;

  shouldComponentUpdate (nextProps: Props, nextState: States) {
  shouldComponentUpdate(nextProps: Props, nextState: States) {
    // As of right now the locale doesn't change without a new page load,
    // but we might as well check in case that ever changes.
    return this.props.timestamp !== nextProps.timestamp ||
    return (
      this.props.timestamp !== nextProps.timestamp ||
      this.props.intl.locale !== nextProps.intl.locale ||
      this.state.now !== nextState.now;
      this.state.now !== nextState.now
    );
  }

  UNSAFE_componentWillReceiveProps (nextProps: Props) {
  UNSAFE_componentWillReceiveProps(nextProps: Props) {
    if (this.props.timestamp !== nextProps.timestamp) {
      this.setState({ now: this.props.intl.now() });
    }
  }

  componentDidMount () {
  componentDidMount() {
    this._scheduleNextUpdate(this.props, this.state);
  }

  UNSAFE_componentWillUpdate (nextProps: Props, nextState: States) {
  UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) {
    this._scheduleNextUpdate(nextProps, nextState);
  }

  componentWillUnmount () {
  componentWillUnmount() {
    window.clearTimeout(this._timer);
  }

  _scheduleNextUpdate (props: Props, state: States) {
  _scheduleNextUpdate(props: Props, state: States) {
    window.clearTimeout(this._timer);

    const { timestamp }  = props;
    const delta          = (new Date(timestamp)).getTime() - state.now;
    const unitDelay      = getUnitDelay(selectUnits(delta));
    const unitRemainder  = Math.abs(delta % unitDelay);
    const { timestamp } = props;
    const delta = new Date(timestamp).getTime() - state.now;
    const unitDelay = getUnitDelay(selectUnits(delta));
    const unitRemainder = Math.abs(delta % unitDelay);
    const updateInterval = 1000 * 10;
    const delay          = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
    const delay =
      delta < 0
        ? Math.max(updateInterval, unitDelay - unitRemainder)
        : Math.max(updateInterval, unitRemainder);

    this._timer = window.setTimeout(() => {
      this.setState({ now: this.props.intl.now() });
    }, delay);
  }

  render () {
  render() {
    const { timestamp, intl, year, futureDate, short } = this.props;

    const timeGiven    = timestamp.includes('T');
    const date         = new Date(timestamp);
    const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
    const timeGiven = timestamp.includes('T');
    const date = new Date(timestamp);
    const relativeTime = futureDate
      ? timeRemainingString(intl, date, this.state.now, timeGiven)
      : timeAgoString(intl, date, this.state.now, year, timeGiven, short);

    return (
      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
      <time
        dateTime={timestamp}
        title={intl.formatDate(date, dateFormatOptions)}
      >
        {relativeTime}
      </time>
    );
  }

}

export default injectIntl(RelativeTimestamp);
const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);

export { RelativeTimestampWithIntl as RelativeTimestamp };

M app/javascript/mastodon/components/scrollable_list.jsx => app/javascript/mastodon/components/scrollable_list.jsx +3 -2
@@ 8,6 8,7 @@ import IntersectionObserverWrapper from '../features/ui/util/intersection_observ
import { throttle } from 'lodash';
import { List as ImmutableList } from 'immutable';
import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
import LoadingIndicator from './loading_indicator';
import { connect } from 'react-redux';


@@ 236,10 237,10 @@ class ScrollableList extends PureComponent {
  attachScrollListener () {
    if (this.props.bindToDocument) {
      document.addEventListener('scroll', this.handleScroll);
      document.addEventListener('wheel', this.handleWheel);
      document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined);
    } else {
      this.node.addEventListener('scroll', this.handleScroll);
      this.node.addEventListener('wheel', this.handleWheel);
      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined);
    }
  }


M app/javascript/mastodon/components/server_banner.jsx => app/javascript/mastodon/components/server_banner.jsx +1 -1
@@ 7,7 7,7 @@ import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state';
import Image from 'mastodon/components/image';
import { Image } from 'mastodon/components/image';
import { Link } from 'react-router-dom';

const messages = defineMessages({

M app/javascript/mastodon/components/status.jsx => app/javascript/mastodon/components/status.jsx +4 -4
@@ 1,9 1,9 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay';
import RelativeTimestamp from './relative_timestamp';
import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay';
import { RelativeTimestamp } from './relative_timestamp';
import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';


@@ 14,7 14,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { displayMedia } from '../initial_state';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';


M app/javascript/mastodon/components/status_action_bar.jsx => app/javascript/mastodon/components/status_action_bar.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { IconButton } from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';

M app/javascript/mastodon/components/status_content.jsx => app/javascript/mastodon/components/status_content.jsx +1 -1
@@ 6,7 6,7 @@ import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';

const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)

D app/javascript/mastodon/components/verified_badge.jsx => app/javascript/mastodon/components/verified_badge.jsx +0 -25
@@ 1,25 0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';

class VerifiedBadge extends React.PureComponent {

  static propTypes = {
    link: PropTypes.string.isRequired,
    verifiedAt: PropTypes.string.isRequired,
  };

  render () {
    const { link } = this.props;

    return (
      <span className='verified-badge'>
        <Icon id='check' className='verified-badge__mark' />
        <span dangerouslySetInnerHTML={{ __html: link }} />
      </span>
    );
  }

}

export default VerifiedBadge;
\ No newline at end of file

A app/javascript/mastodon/components/verified_badge.tsx => app/javascript/mastodon/components/verified_badge.tsx +12 -0
@@ 0,0 1,12 @@
import React from 'react';
import { Icon } from './icon';

type Props = {
  link: string;
};
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
  <span className='verified-badge'>
    <Icon id='check' className='verified-badge__mark' />
    <span dangerouslySetInnerHTML={{ __html: link }} />
  </span>
);

M app/javascript/mastodon/containers/compose_container.jsx => app/javascript/mastodon/containers/compose_container.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import { store } from '../store/configureStore';
import { store } from '../store';
import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';

M app/javascript/mastodon/containers/domain_container.jsx => app/javascript/mastodon/containers/domain_container.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { blockDomain, unblockDomain } from '../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Domain from '../components/domain';
import { Domain } from '../components/domain';
import { openModal } from '../actions/modal';

const messages = defineMessages({

M app/javascript/mastodon/containers/mastodon.jsx => app/javascript/mastodon/containers/mastodon.jsx +1 -1
@@ 5,7 5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { Provider as ReduxProvider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import { store } from 'mastodon/store/configureStore';
import { store } from 'mastodon/store';
import UI from 'mastodon/features/ui';
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
import { hydrateStore } from 'mastodon/actions/store';

M app/javascript/mastodon/features/about/index.jsx => app/javascript/mastodon/features/about/index.jsx +2 -2
@@ 9,9 9,9 @@ import { Helmet } from 'react-helmet';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
import Account from 'mastodon/containers/account_container';
import Skeleton from 'mastodon/components/skeleton';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import classNames from 'classnames';
import Image from 'mastodon/components/image';
import { Image } from 'mastodon/components/image';

const messages = defineMessages({
  title: { id: 'column.about', defaultMessage: 'About' },

M app/javascript/mastodon/features/account/components/follow_request_note.jsx => app/javascript/mastodon/features/account/components/follow_request_note.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

export default class FollowRequestNote extends ImmutablePureComponent {


M app/javascript/mastodon/features/account/components/header.jsx => app/javascript/mastodon/features/account/components/header.jsx +3 -3
@@ 6,9 6,9 @@ import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, domain } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
import Avatar from 'mastodon/components/avatar';
import { Icon }  from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { Avatar } from 'mastodon/components/avatar';
import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
import { NavLink } from 'react-router-dom';

M app/javascript/mastodon/features/account_gallery/components/media_item.jsx => app/javascript/mastodon/features/account_gallery/components/media_item.jsx +2 -2
@@ 1,6 1,6 @@
import Blurhash from 'mastodon/components/blurhash';
import { Blurhash } from 'mastodon/components/blurhash';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
import PropTypes from 'prop-types';
import React from 'react';

M app/javascript/mastodon/features/account_timeline/components/moved_note.jsx => app/javascript/mastodon/features/account_timeline/components/moved_note.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from '../../../components/avatar_overlay';
import { AvatarOverlay } from '../../../components/avatar_overlay';
import DisplayName from '../../../components/display_name';
import { Link } from 'react-router-dom';


M app/javascript/mastodon/features/audio/index.jsx => app/javascript/mastodon/features/audio/index.jsx +2 -2
@@ 2,12 2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/features/video';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import classNames from 'classnames';
import { throttle, debounce } from 'lodash';
import Visualizer from './visualizer';
import { displayMedia, useBlurhash } from '../../initial_state';
import Blurhash from '../../components/blurhash';
import { Blurhash } from '../../components/blurhash';
import { is } from 'immutable';

const messages = defineMessages({

M app/javascript/mastodon/features/compose/components/autosuggest_account.jsx => app/javascript/mastodon/features/compose/components/autosuggest_account.jsx +1 -1
@@ 1,5 1,5 @@
import React from 'react';
import Avatar from '../../../components/avatar';
import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';

M app/javascript/mastodon/features/compose/components/compose_form.jsx => app/javascript/mastodon/features/compose/components/compose_form.jsx +1 -1
@@ 19,7 19,7 @@ import LanguageDropdown from '../containers/language_dropdown_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import classNames from 'classnames';
import { maxChars } from '../../../initial_state';


M app/javascript/mastodon/features/compose/components/navigation_bar.jsx => app/javascript/mastodon/features/compose/components/navigation_bar.jsx +2 -2
@@ 2,9 2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ActionBar from './action_bar';
import Avatar from '../../../components/avatar';
import { Avatar } from '../../../components/avatar';
import { Link } from 'react-router-dom';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';


M app/javascript/mastodon/features/compose/components/poll_button.jsx => app/javascript/mastodon/features/compose/components/poll_button.jsx +1 -1
@@ 1,5 1,5 @@
import React from 'react';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';


M app/javascript/mastodon/features/compose/components/poll_form.jsx => app/javascript/mastodon/features/compose/components/poll_form.jsx +2 -2
@@ 3,8 3,8 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { Icon }  from 'mastodon/components/icon';
import AutosuggestInput from 'mastodon/components/autosuggest_input';
import classNames from 'classnames';


M app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx => app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx +2 -2
@@ 1,11 1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import Overlay from 'react-overlays/Overlay';
import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const messages = defineMessages({
  public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },

M app/javascript/mastodon/features/compose/components/reply_indicator.jsx => app/javascript/mastodon/features/compose/components/reply_indicator.jsx +2 -2
@@ 1,8 1,8 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from '../../../components/avatar';
import IconButton from '../../../components/icon_button';
import { Avatar } from '../../../components/avatar';
import { IconButton } from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';

M app/javascript/mastodon/features/compose/components/search.jsx => app/javascript/mastodon/features/compose/components/search.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { searchEnabled } from 'mastodon/initial_state';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import classNames from 'classnames';
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';


M app/javascript/mastodon/features/compose/components/search_results.jsx => app/javascript/mastodon/features/compose/components/search_results.jsx +1 -1
@@ 6,7 6,7 @@ import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { searchEnabled } from '../../../initial_state';
import LoadMore from 'mastodon/components/load_more';


M app/javascript/mastodon/features/compose/components/upload.jsx => app/javascript/mastodon/features/compose/components/upload.jsx +1 -1
@@ 5,7 5,7 @@ import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

export default class Upload extends ImmutablePureComponent {


M app/javascript/mastodon/features/compose/components/upload_button.jsx => app/javascript/mastodon/features/compose/components/upload_button.jsx +1 -1
@@ 1,5 1,5 @@
import React from 'react';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';

M app/javascript/mastodon/features/compose/components/upload_progress.jsx => app/javascript/mastodon/features/compose/components/upload_progress.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { FormattedMessage } from 'react-intl';

export default class UploadProgress extends React.PureComponent {

M app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js => app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js +1 -0
@@ 72,6 72,7 @@ const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
  },

  onPickEmoji: emoji => {
    // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
    dispatch(useEmoji(emoji));

    if (onPickEmoji) {

M app/javascript/mastodon/features/compose/containers/language_dropdown_container.js => app/javascript/mastodon/features/compose/containers/language_dropdown_container.js +1 -0
@@ 26,6 26,7 @@ const mapDispatchToProps = dispatch => ({
  },

  onClose (value) {
    // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
    dispatch(useLanguage(value));
  },


M app/javascript/mastodon/features/compose/index.jsx => app/javascript/mastodon/features/compose/index.jsx +1 -1
@@ 14,7 14,7 @@ import SearchResultsContainer from './containers/search_results_container';
import { openModal } from 'mastodon/actions/modal';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { mascot } from '../../initial_state';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out';
import Column from 'mastodon/components/column';
import { Helmet } from 'react-helmet';

M app/javascript/mastodon/features/direct_timeline/components/conversation.jsx => app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +2 -2
@@ 8,8 8,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AvatarComposite from 'mastodon/components/avatar_composite';
import { Link } from 'react-router-dom';
import IconButton from 'mastodon/components/icon_button';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { IconButton } from 'mastodon/components/icon_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { HotKeys } from 'react-hotkeys';
import { autoPlayGif } from 'mastodon/initial_state';
import classNames from 'classnames';

M app/javascript/mastodon/features/directory/components/account_card.jsx => app/javascript/mastodon/features/directory/components/account_card.jsx +1 -1
@@ 4,7 4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import Avatar from 'mastodon/components/avatar';
import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import { Link } from 'react-router-dom';
import Button from 'mastodon/components/button';

M app/javascript/mastodon/features/directory/index.jsx => app/javascript/mastodon/features/directory/index.jsx +1 -1
@@ 9,7 9,7 @@ import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodo
import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
import { List as ImmutableList } from 'immutable';
import AccountCard from './components/account_card';
import RadioButton from 'mastodon/components/radio_button';
import { RadioButton } from 'mastodon/components/radio_button';
import LoadMore from 'mastodon/components/load_more';
import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadingIndicator from 'mastodon/components/loading_indicator';

M app/javascript/mastodon/features/emoji/emoji_compressed.js => app/javascript/mastodon/features/emoji/emoji_compressed.js +2 -0
@@ 1,3 1,5 @@
/* eslint-disable import/no-commonjs --
   We need to use CommonJS here due to preval */
// @preval
// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
// This file contains the compressed version of the emoji data from

M app/javascript/mastodon/features/emoji/emoji_mart_data_light.js => app/javascript/mastodon/features/emoji/emoji_mart_data_light.js +5 -3
@@ 1,8 1,10 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed');
import { unicodeToUnifiedName } from './unicode_to_unified_name';
import emojiCompressed from './emoji_compressed';

const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;

const emojis = {};



@@ 33,7 35,7 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
  };
});

module.exports = {
export {
  emojis,
  skins,
  categories,

M app/javascript/mastodon/features/emoji/emoji_mart_search_light.js => app/javascript/mastodon/features/emoji/emoji_mart_search_light.js +1 -1
@@ 1,7 1,7 @@
// This code is largely borrowed from:
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js

import data from './emoji_mart_data_light';
import * as data from './emoji_mart_data_light';
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';

let originalPool = {};

M app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.js => app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.js +9 -6
@@ 2,14 2,17 @@
// (i.e. the svg filename) and a shortCode intended to be shown
// as a "title" attribute in an HTML element (aka tooltip).

import emojiCompressed from './emoji_compressed';

import { unicodeToFilename } from './unicode_to_filename';

const [
  shortCodesToEmojiData,
  skins, // eslint-disable-line @typescript-eslint/no-unused-vars
  categories, // eslint-disable-line @typescript-eslint/no-unused-vars
  short_names, // eslint-disable-line @typescript-eslint/no-unused-vars
  _skins,
  _categories,
  _short_names,
  emojisWithoutShortCodes,
] = require('./emoji_compressed');
const { unicodeToFilename } = require('./unicode_to_filename');
] = emojiCompressed;

// decompress
const unicodeMapping = {};


@@ 32,4 35,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
});
emojisWithoutShortCodes.forEach(emojiMapData => processEmojiMapData(emojiMapData));

module.exports = unicodeMapping;
export default unicodeMapping;

M app/javascript/mastodon/features/emoji/emoji_utils.js => app/javascript/mastodon/features/emoji/emoji_utils.js +1 -1
@@ 1,7 1,7 @@
// This code is largely borrowed from:
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js

import data from './emoji_mart_data_light';
import * as data from './emoji_mart_data_light';

const buildSearch = (data) => {
  const search = [];

M app/javascript/mastodon/features/emoji/unicode_to_filename.js => app/javascript/mastodon/features/emoji/unicode_to_filename.js +3 -0
@@ 1,3 1,6 @@
/* eslint-disable import/no-commonjs --
   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */

// taken from:
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
exports.unicodeToFilename = (str) => {

M app/javascript/mastodon/features/emoji/unicode_to_unified_name.js => app/javascript/mastodon/features/emoji/unicode_to_unified_name.js +3 -0
@@ 1,3 1,6 @@
/* eslint-disable import/no-commonjs --
   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */

function padLeft(str, num) {
  while (str.length < num) {
    str = '0' + str;

M app/javascript/mastodon/features/explore/components/story.jsx => app/javascript/mastodon/features/explore/components/story.jsx +1 -1
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Blurhash from 'mastodon/components/blurhash';
import { Blurhash } from 'mastodon/components/blurhash';
import { accountsCountRenderer } from 'mastodon/components/hashtag';
import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton';

M app/javascript/mastodon/features/favourites/index.jsx => app/javascript/mastodon/features/favourites/index.jsx +1 -1
@@ 5,7 5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import ColumnHeader from 'mastodon/components/column_header';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { fetchFavourites } from 'mastodon/actions/interactions';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';

M app/javascript/mastodon/features/filters/select_filter.jsx => app/javascript/mastodon/features/filters/select_filter.jsx +1 -1
@@ 4,7 4,7 @@ import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { toServerSideType } from 'mastodon/utils/filters';
import { loupeIcon, deleteIcon } from 'mastodon/utils/icons';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import fuzzysort from 'fuzzysort';

const messages = defineMessages({

M app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx => app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx +2 -2
@@ 2,9 2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
import Avatar from '../../../components/avatar';
import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';


M app/javascript/mastodon/features/getting_started/components/announcements.jsx => app/javascript/mastodon/features/getting_started/components/announcements.jsx +3 -3
@@ 3,15 3,15 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { Icon }  from 'mastodon/components/icon';
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'mastodon/initial_state';
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
import classNames from 'classnames';
import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
import AnimatedNumber from 'mastodon/components/animated_number';
import { AnimatedNumber } from 'mastodon/components/animated_number';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { assetHost } from 'mastodon/utils/config';

M app/javascript/mastodon/features/hashtag_timeline/index.jsx => app/javascript/mastodon/features/hashtag_timeline/index.jsx +1 -1
@@ 12,7 12,7 @@ import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connectHashtagStream } from 'mastodon/actions/streaming';
import { isEqual } from 'lodash';
import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';


M app/javascript/mastodon/features/home_timeline/index.jsx => app/javascript/mastodon/features/home_timeline/index.jsx +2 -2
@@ 12,8 12,8 @@ import { Link } from 'react-router-dom';
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
import classNames from 'classnames';
import IconWithBadge from 'mastodon/components/icon_with_badge';
import NotSignedInIndicator from 'mastodon/components/not_signed_in_indicator';
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { Helmet } from 'react-helmet';

const messages = defineMessages({

M app/javascript/mastodon/features/interaction_modal/index.jsx => app/javascript/mastodon/features/interaction_modal/index.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { registrationsOpen } from 'mastodon/initial_state';
import { connect } from 'react-redux';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import classNames from 'classnames';
import { openModal, closeModal } from 'mastodon/actions/modal';


M app/javascript/mastodon/features/list_adder/components/account.jsx => app/javascript/mastodon/features/list_adder/components/account.jsx +1 -1
@@ 3,7 3,7 @@ import { connect } from 'react-redux';
import { makeGetAccount } from '../../../selectors';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import { injectIntl } from 'react-intl';


M app/javascript/mastodon/features/list_adder/components/list.jsx => app/javascript/mastodon/features/list_adder/components/list.jsx +2 -2
@@ 3,10 3,10 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from '../../../components/icon_button';
import { IconButton }  from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const messages = defineMessages({
  remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },

M app/javascript/mastodon/features/list_editor/components/account.jsx => app/javascript/mastodon/features/list_editor/components/account.jsx +2 -2
@@ 4,9 4,9 @@ import { connect } from 'react-redux';
import { makeGetAccount } from '../../../selectors';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';


M app/javascript/mastodon/features/list_editor/components/edit_list_form.jsx => app/javascript/mastodon/features/list_editor/components/edit_list_form.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';

const messages = defineMessages({

M app/javascript/mastodon/features/list_editor/components/search.jsx => app/javascript/mastodon/features/list_editor/components/search.jsx +1 -1
@@ 4,7 4,7 @@ import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const messages = defineMessages({
  search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },

M app/javascript/mastodon/features/list_timeline/index.jsx => app/javascript/mastodon/features/list_timeline/index.jsx +2 -2
@@ 11,9 11,9 @@ import { connectListStream } from 'mastodon/actions/streaming';
import { expandListTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import RadioButton from 'mastodon/components/radio_button';
import { RadioButton } from 'mastodon/components/radio_button';
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';


M app/javascript/mastodon/features/notifications/components/clear_column_button.jsx => app/javascript/mastodon/features/notifications/components/clear_column_button.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

export default class ClearColumnButton extends React.PureComponent {


M app/javascript/mastodon/features/notifications/components/filter_bar.jsx => app/javascript/mastodon/features/notifications/components/filter_bar.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const tooltips = defineMessages({
  mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },

M app/javascript/mastodon/features/notifications/components/follow_request.jsx => app/javascript/mastodon/features/notifications/components/follow_request.jsx +2 -2
@@ 1,10 1,10 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from 'mastodon/components/avatar';
import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import { Link } from 'react-router-dom';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';


M app/javascript/mastodon/features/notifications/components/notification.jsx => app/javascript/mastodon/features/notifications/components/notification.jsx +1 -1
@@ 9,7 9,7 @@ import StatusContainer from 'mastodon/containers/status_container';
import AccountContainer from 'mastodon/containers/account_container';
import Report from './report';
import FollowRequestContainer from '../containers/follow_request_container';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { Link } from 'react-router-dom';
import classNames from 'classnames';


M app/javascript/mastodon/features/notifications/components/notifications_permission_banner.jsx => app/javascript/mastodon/features/notifications/components/notifications_permission_banner.jsx +2 -2
@@ 1,7 1,7 @@
import React from 'react';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import Button from 'mastodon/components/button';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import { requestBrowserPermission } from 'mastodon/actions/notifications';
import { changeSetting } from 'mastodon/actions/settings';
import { connect } from 'react-redux';

M app/javascript/mastodon/features/notifications/components/report.jsx => app/javascript/mastodon/features/notifications/components/report.jsx +2 -2
@@ 3,8 3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from 'mastodon/components/avatar_overlay';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { AvatarOverlay } from 'mastodon/components/avatar_overlay';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';

const messages = defineMessages({
  openReport: { id: 'report_notification.open', defaultMessage: 'Open report' },

M app/javascript/mastodon/features/notifications/index.jsx => app/javascript/mastodon/features/notifications/index.jsx +3 -3
@@ 23,10 23,10 @@ import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list';
import LoadGap from '../../components/load_gap';
import Icon from 'mastodon/components/icon';
import compareId from 'mastodon/compare_id';
import { Icon }  from 'mastodon/components/icon';
import { compareId } from 'mastodon/compare_id';
import NotificationsPermissionBanner from './components/notifications_permission_banner';
import NotSignedInIndicator from 'mastodon/components/not_signed_in_indicator';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { Helmet } from 'react-helmet';

const messages = defineMessages({

M app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx => app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx +2 -2
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Check from 'mastodon/components/check';
import { Check } from 'mastodon/components/check';
import classNames from 'classnames';

const ProgressIndicator = ({ steps, completed }) => (


@@ 22,4 22,4 @@ ProgressIndicator.propTypes = {
  completed: PropTypes.number,
};

export default ProgressIndicator;
\ No newline at end of file
export default ProgressIndicator;

M app/javascript/mastodon/features/onboarding/components/step.jsx => app/javascript/mastodon/features/onboarding/components/step.jsx +3 -3
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
import Check from 'mastodon/components/check';
import { Icon }  from 'mastodon/components/icon';
import { Check } from 'mastodon/components/check';

const Step = ({ label, description, icon, completed, onClick, href }) => {
  const content = (


@@ 47,4 47,4 @@ Step.propTypes = {
  onClick: PropTypes.func,
};

export default Step;
\ No newline at end of file
export default Step;

M app/javascript/mastodon/features/onboarding/share.jsx => app/javascript/mastodon/features/onboarding/share.jsx +2 -2
@@ 7,7 7,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage, FormattedHTMLMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import ArrowSmallRight from './components/arrow_small_right';
import { Link } from 'react-router-dom';
import SwipeableViews from 'react-swipeable-views';


@@ 190,4 190,4 @@ class Share extends React.PureComponent {

}

export default connect(mapStateToProps)(injectIntl(Share));
\ No newline at end of file
export default connect(mapStateToProps)(injectIntl(Share));

M app/javascript/mastodon/features/picture_in_picture/components/footer.jsx => app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +1 -1
@@ 3,7 3,7 @@ import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import classNames from 'classnames';
import { me, boostModal } from 'mastodon/initial_state';
import { defineMessages, injectIntl } from 'react-intl';

M app/javascript/mastodon/features/picture_in_picture/components/header.jsx => app/javascript/mastodon/features/picture_in_picture/components/header.jsx +2 -2
@@ 3,9 3,9 @@ import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import { Link } from 'react-router-dom';
import Avatar from 'mastodon/components/avatar';
import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import { defineMessages, injectIntl } from 'react-intl';


M app/javascript/mastodon/features/reblogs/index.jsx => app/javascript/mastodon/features/reblogs/index.jsx +1 -1
@@ 9,7 9,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import ColumnHeader from '../../components/column_header';
import { Helmet } from 'react-helmet';


M app/javascript/mastodon/features/report/components/option.jsx => app/javascript/mastodon/features/report/components/option.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Check from 'mastodon/components/check';
import { Check } from 'mastodon/components/check';

export default class Option extends React.PureComponent {


M app/javascript/mastodon/features/report/components/status_check_box.jsx => app/javascript/mastodon/features/report/components/status_check_box.jsx +3 -3
@@ 2,13 2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContent from 'mastodon/components/status_content';
import Avatar from 'mastodon/components/avatar';
import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import Option from './option';
import MediaAttachments from 'mastodon/components/media_attachments';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

const messages = defineMessages({
  public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },

M app/javascript/mastodon/features/status/components/action_bar.jsx => app/javascript/mastodon/features/status/components/action_bar.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';

M app/javascript/mastodon/features/status/components/card.jsx => app/javascript/mastodon/features/status/components/card.jsx +2 -2
@@ 5,9 5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import punycode from 'punycode';
import classnames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { useBlurhash } from 'mastodon/initial_state';
import Blurhash from 'mastodon/components/blurhash';
import { Blurhash } from 'mastodon/components/blurhash';

const IDNA_PREFIX = 'xn--';


M app/javascript/mastodon/features/status/components/detailed_status.jsx => app/javascript/mastodon/features/status/components/detailed_status.jsx +3 -3
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery';


@@ 13,8 13,8 @@ import Video from '../../video';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number';
import { Icon }  from 'mastodon/components/icon';
import { AnimatedNumber } from 'mastodon/components/animated_number';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import EditedTimestamp from 'mastodon/components/edited_timestamp';


M app/javascript/mastodon/features/status/index.jsx => app/javascript/mastodon/features/status/index.jsx +1 -1
@@ 58,7 58,7 @@ import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import { Helmet } from 'react-helmet';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';


M app/javascript/mastodon/features/subscribed_languages_modal/index.jsx => app/javascript/mastodon/features/subscribed_languages_modal/index.jsx +1 -1
@@ 8,7 8,7 @@ import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable';
import { languages as preloadedLanguages } from 'mastodon/initial_state';
import Option from 'mastodon/features/report/components/option';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import Button from 'mastodon/components/button';
import { followAccount } from 'mastodon/actions/accounts';


M app/javascript/mastodon/features/ui/components/actions_modal.jsx => app/javascript/mastodon/features/ui/components/actions_modal.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';
import classNames from 'classnames';

export default class ActionsModal extends ImmutablePureComponent {

M app/javascript/mastodon/features/ui/components/boost_modal.jsx => app/javascript/mastodon/features/ui/components/boost_modal.jsx +3 -3
@@ 5,11 5,11 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from '../../../components/button';
import StatusContent from '../../../components/status_content';
import Avatar from '../../../components/avatar';
import RelativeTimestamp from '../../../components/relative_timestamp';
import { Avatar } from '../../../components/avatar';
import { RelativeTimestamp } from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import AttachmentList from 'mastodon/components/attachment_list';
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
import classNames from 'classnames';

M app/javascript/mastodon/features/ui/components/bundle_modal_error.jsx => app/javascript/mastodon/features/ui/components/bundle_modal_error.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';

import IconButton from '../../../components/icon_button';
import { IconButton } from '../../../components/icon_button';

const messages = defineMessages({
  error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },

M app/javascript/mastodon/features/ui/components/column_header.jsx => app/javascript/mastodon/features/ui/components/column_header.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';

export default class ColumnHeader extends React.PureComponent {


M app/javascript/mastodon/features/ui/components/column_link.jsx => app/javascript/mastodon/features/ui/components/column_link.jsx +1 -1
@@ 1,7 1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import Icon from 'mastodon/components/icon';
import { Icon }  from 'mastodon/components/icon';
import classNames from 'classnames';

const ColumnLink = ({ icon, text, to, href, method, badge, transparent, ...other }) => {

M app/javascript/mastodon/features/ui/components/compare_history_modal.jsx => app/javascript/mastodon/features/ui/components/compare_history_modal.jsx +2 -2
@@ 7,8 7,8 @@ import { closeModal } from 'mastodon/actions/modal';
import emojify from 'mastodon/features/emoji/emoji';
import escapeTextContentForBrowser from 'escape-html';
import InlineAccount from 'mastodon/components/inline_account';
import IconButton from 'mastodon/components/icon_button';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { IconButton } from 'mastodon/components/icon_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import MediaAttachments from 'mastodon/components/media_attachments';

const mapStateToProps = (state, { statusId }) => ({

M app/javascript/mastodon/features/ui/components/embed_modal.jsx => app/javascript/mastodon/features/ui/components/embed_modal.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import api from 'mastodon/api';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';

const messages = defineMessages({
  close: { id: 'lightbox.close', defaultMessage: 'Close' },

M app/javascript/mastodon/features/ui/components/filter_modal.jsx => app/javascript/mastodon/features/ui/components/filter_modal.jsx +1 -1
@@ 5,7 5,7 @@ import { fetchFilters, createFilter, createFilterStatus } from 'mastodon/actions
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import SelectFilter from 'mastodon/features/filters/select_filter';
import AddedToFilter from 'mastodon/features/filters/added_to_filter';


M app/javascript/mastodon/features/ui/components/focal_point_modal.jsx => app/javascript/mastodon/features/ui/components/focal_point_modal.jsx +2 -2
@@ 7,7 7,7 @@ import classNames from 'classnames';
import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose';
import { getPointerPosition } from '../../video';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import Button from 'mastodon/components/button';
import Video from 'mastodon/features/video';
import Audio from 'mastodon/features/audio';


@@ 16,7 16,7 @@ import UploadProgress from 'mastodon/features/compose/components/upload_progress
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
import { length } from 'stringz';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import GIFV from 'mastodon/components/gifv';
import { GIFV } from 'mastodon/components/gifv';
import { me } from 'mastodon/initial_state';
// eslint-disable-next-line import/no-extraneous-dependencies
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';

M app/javascript/mastodon/features/ui/components/follow_requests_column_link.jsx => app/javascript/mastodon/features/ui/components/follow_requests_column_link.jsx +1 -1
@@ 3,7 3,7 @@ import PropTypes from 'prop-types';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { connect } from 'react-redux';
import ColumnLink from 'mastodon/features/ui/components/column_link';
import IconWithBadge from 'mastodon/components/icon_with_badge';
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
import { List as ImmutableList } from 'immutable';
import { injectIntl, defineMessages } from 'react-intl';


M app/javascript/mastodon/features/ui/components/header.jsx => app/javascript/mastodon/features/ui/components/header.jsx +1 -1
@@ 3,7 3,7 @@ import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
import { Link, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { registrationsOpen, me } from 'mastodon/initial_state';
import Avatar from 'mastodon/components/avatar';
import { Avatar } from 'mastodon/components/avatar';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';

M app/javascript/mastodon/features/ui/components/image_modal.jsx => app/javascript/mastodon/features/ui/components/image_modal.jsx +1 -1
@@ 2,7 2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import ImageLoader from './image_loader';

const messages = defineMessages({

M app/javascript/mastodon/features/ui/components/media_modal.jsx => app/javascript/mastodon/features/ui/components/media_modal.jsx +3 -3
@@ 6,11 6,11 @@ import Video from 'mastodon/features/video';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImageLoader from './image_loader';
import Icon from 'mastodon/components/icon';
import GIFV from 'mastodon/components/gifv';
import { Icon }  from 'mastodon/components/icon';
import { GIFV } from 'mastodon/components/gifv';
import { disableSwiping } from 'mastodon/initial_state';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';

M app/javascript/mastodon/features/ui/components/notifications_counter_icon.js => app/javascript/mastodon/features/ui/components/notifications_counter_icon.js +1 -1
@@ 1,5 1,5 @@
import { connect } from 'react-redux';
import IconWithBadge from 'mastodon/components/icon_with_badge';
import { IconWithBadge } from 'mastodon/components/icon_with_badge';

const mapStateToProps = state => ({
  count: state.getIn(['notifications', 'unread']),

M app/javascript/mastodon/features/ui/components/report_modal.jsx => app/javascript/mastodon/features/ui/components/report_modal.jsx +1 -1
@@ 9,7 9,7 @@ import { makeGetAccount } from 'mastodon/selectors';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { OrderedSet } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import Category from 'mastodon/features/report/category';
import Statuses from 'mastodon/features/report/statuses';
import Rules from 'mastodon/features/report/rules';

M app/javascript/mastodon/features/ui/components/zoomable_image.jsx => app/javascript/mastodon/features/ui/components/zoomable_image.jsx +1 -1
@@ 1,6 1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import { IconButton } from 'mastodon/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';

const messages = defineMessages({

M app/javascript/mastodon/features/video/index.jsx => app/javascript/mastodon/features/video/index.jsx +2 -2
@@ 6,8 6,8 @@ import { throttle } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from '../../initial_state';
import Icon from 'mastodon/components/icon';
import Blurhash from 'mastodon/components/blurhash';
import { Icon }  from 'mastodon/components/icon';
import { Blurhash } from 'mastodon/components/blurhash';

const messages = defineMessages({
  play: { id: 'video.play', defaultMessage: 'Play' },

M app/javascript/mastodon/is_mobile.ts => app/javascript/mastodon/is_mobile.ts +0 -6
@@ 16,10 16,6 @@ export const layoutFromWindow = (): LayoutType => {
  }
};

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null;

const listenerOptions = supportsPassiveEvents ? { passive: true } : false;

let userTouching = false;


@@ 33,5 29,3 @@ const touchListener = () => {
window.addEventListener('touchstart', touchListener, listenerOptions);

export const isUserTouching = () => userTouching;

export const isIOS = () => iOS;

M app/javascript/mastodon/main.jsx => app/javascript/mastodon/main.jsx +2 -3
@@ 2,11 2,10 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { setupBrowserNotifications } from 'mastodon/actions/notifications';
import Mastodon from 'mastodon/containers/mastodon';
import { store } from 'mastodon/store/configureStore';
import { store } from 'mastodon/store';
import { me } from 'mastodon/initial_state';
import ready from 'mastodon/ready';

const perf = require('mastodon/performance');
import * as perf from 'mastodon/performance';

/**
 * @returns {Promise<void>}

M app/javascript/mastodon/performance.js => app/javascript/mastodon/performance.js +1 -3
@@ 2,9 2,8 @@
// Tools for performance debugging, only enabled in development mode.
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
// Also see config/webpack/loaders/mark.js for the webpack loader marks.
//

let marky;
import * as marky from 'marky';

if (process.env.NODE_ENV === 'development') {
  if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {


@@ 13,7 12,6 @@ if (process.env.NODE_ENV === 'development') {
    performance.setResourceTimingBufferSize(Infinity);
  }

  marky = require('marky');
  // allows us to easily do e.g. ReactPerf.printWasted() while debugging
  //window.ReactPerf = require('react-addons-perf');
  //window.ReactPerf.start();

M app/javascript/mastodon/permissions.ts => app/javascript/mastodon/permissions.ts +3 -3
@@ 1,4 1,4 @@
export const PERMISSION_INVITE_USERS      = 0x0000000000010000;
export const PERMISSION_MANAGE_USERS      = 0x0000000000000400;
export const PERMISSION_INVITE_USERS = 0x0000000000010000;
export const PERMISSION_MANAGE_USERS = 0x0000000000000400;
export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020;
export const PERMISSION_MANAGE_REPORTS    = 0x0000000000000010;
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010;

R app/javascript/mastodon/base_polyfills.js => app/javascript/mastodon/polyfills/base_polyfills.ts +6 -16
@@ 1,26 1,16 @@
import 'intl';
import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
import assign from 'object-assign';
import values from 'object.values';
import { decode as decodeBase64 } from './utils/base64';
import promiseFinally from 'promise.prototype.finally';

if (!Object.assign) {
  Object.assign = assign;
}

if (!Object.values) {
  values.shim();
}

promiseFinally.shim();
import 'core-js/features/object/assign';
import 'core-js/features/object/values';
import 'core-js/features/symbol';
import 'core-js/features/promise/finally';
import { decode as decodeBase64 } from '../utils/base64';

if (!HTMLCanvasElement.prototype.toBlob) {
  const BASE64_MARKER = ';base64,';

  Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
    value(callback, type = 'image/png', quality) {
    value(callback: BlobCallback, type = 'image/png', quality: any) {
      const dataURL = this.toDataURL(type, quality);
      let data;


R app/javascript/mastodon/extra_polyfills.js => app/javascript/mastodon/polyfills/extra_polyfills.ts +0 -0
R app/javascript/mastodon/load_polyfills.js => app/javascript/mastodon/polyfills/index.ts +7 -9
@@ 10,14 10,14 @@ function importExtraPolyfills() {
  return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
}

function loadPolyfills() {
export function loadPolyfills() {
  const needsBasePolyfills = !(
    HTMLCanvasElement.prototype.toBlob &&
    window.Intl &&
    Object.assign &&
    Object.values &&
    window.Symbol &&
    Promise.prototype.finally
    'toBlob' in HTMLCanvasElement.prototype &&
    'Intl' in window &&
    'assign' in Object &&
    'values' in Object &&
    'Symbol' in window &&
    'finally' in Promise.prototype
  );

  // Latest version of Firefox and Safari do not have IntersectionObserver.


@@ 36,5 36,3 @@ function loadPolyfills() {
    needsExtraPolyfills && importExtraPolyfills(),
  ]);
}

export default loadPolyfills;

M app/javascript/mastodon/reducers/compose.js => app/javascript/mastodon/reducers/compose.js +1 -1
@@ 52,7 52,7 @@ import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
import { REDRAFT } from '../actions/statuses';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from '../uuid';
import { uuid } from '../uuid';
import { me } from '../initial_state';
import { unescapeHTML } from '../utils/html';


M app/javascript/mastodon/reducers/contexts.js => app/javascript/mastodon/reducers/contexts.js +1 -1
@@ 5,7 5,7 @@ import {
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap({
  inReplyTos: ImmutableMap(),

M app/javascript/mastodon/reducers/conversations.js => app/javascript/mastodon/reducers/conversations.js +1 -1
@@ 11,7 11,7 @@ import {
} from '../actions/conversations';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap({
  items: ImmutableList(),

R app/javascript/mastodon/reducers/index.js => app/javascript/mastodon/reducers/index.ts +5 -3
@@ 33,7 33,7 @@ import conversations from './conversations';
import suggestions from './suggestions';
import polls from './polls';
import trends from './trends';
import missed_updates from './missed_updates';
import { missedUpdatesReducer } from './missed_updates';
import announcements from './announcements';
import markers from './markers';
import picture_in_picture from './picture_in_picture';


@@ 79,7 79,7 @@ const reducers = {
  suggestions,
  polls,
  trends,
  missed_updates,
  missed_updates: missedUpdatesReducer,
  markers,
  picture_in_picture,
  history,


@@ 87,4 87,6 @@ const reducers = {
  followed_tags,
};

export default combineReducers(reducers);
const rootReducer = combineReducers(reducers);

export { rootReducer };

M app/javascript/mastodon/reducers/missed_updates.ts => app/javascript/mastodon/reducers/missed_updates.ts +12 -12
@@ 12,20 12,20 @@ const initialState = Record<MissedUpdatesState>({
  unread: 0,
})();

export default function missed_updates(
export function missedUpdatesReducer(
  state = initialState,
  action: Action<string>,
  action: Action<string>
) {
  switch (action.type) {
  case focusApp.type:
    return state.set('focused', true).set('unread', 0);
  case unfocusApp.type:
    return state.set('focused', false);
  case NOTIFICATIONS_UPDATE:
    return state.get('focused')
      ? state
      : state.update('unread', (x) => x + 1);
  default:
    return state;
    case focusApp.type:
      return state.set('focused', true).set('unread', 0);
    case unfocusApp.type:
      return state.set('focused', false);
    case NOTIFICATIONS_UPDATE:
      return state.get('focused')
        ? state
        : state.update('unread', (x) => x + 1);
    default:
      return state;
  }
}

M app/javascript/mastodon/reducers/notifications.js => app/javascript/mastodon/reducers/notifications.js +1 -1
@@ 29,7 29,7 @@ import {
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap({
  pendingItems: ImmutableList(),

M app/javascript/mastodon/reducers/settings.js => app/javascript/mastodon/reducers/settings.js +1 -1
@@ 6,7 6,7 @@ import { EMOJI_USE } from '../actions/emojis';
import { LANGUAGE_USE } from '../actions/languages';
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
import { Map as ImmutableMap, fromJS } from 'immutable';
import uuid from '../uuid';
import { uuid } from '../uuid';

const initialState = ImmutableMap({
  saved: true,

M app/javascript/mastodon/reducers/timelines.js => app/javascript/mastodon/reducers/timelines.js +1 -1
@@ 17,7 17,7 @@ import {
  ACCOUNT_UNFOLLOW_SUCCESS,
} from '../actions/accounts';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import compareId from '../compare_id';
import { compareId } from '../compare_id';

const initialState = ImmutableMap();


M app/javascript/mastodon/scroll.ts => app/javascript/mastodon/scroll.ts +27 -10
@@ 1,13 1,23 @@
const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
const easingOutQuint = (
  x: number,
  t: number,
  b: number,
  c: number,
  d: number
) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
const scroll = (
  node: Element,
  key: 'scrollTop' | 'scrollLeft',
  target: number
) => {
  const startTime = Date.now();
  const offset    = node[key];
  const gap       = target - offset;
  const duration  = 1000;
  let interrupt   = false;
  const offset = node[key];
  const gap = target - offset;
  const duration = 1000;
  let interrupt = false;

  const step = () => {
    const elapsed    = Date.now() - startTime;
    const elapsed = Date.now() - startTime;
    const percentage = elapsed / duration;

    if (percentage > 1 || interrupt) {


@@ 25,7 35,14 @@ const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) 
  };
};

const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
const isScrollBehaviorSupported =
  'scrollBehavior' in document.documentElement.style;

export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
export const scrollRight = (node: Element, position: number) =>
  isScrollBehaviorSupported
    ? node.scrollTo({ left: position, behavior: 'smooth' })
    : scroll(node, 'scrollLeft', position);
export const scrollTop = (node: Element) =>
  isScrollBehaviorSupported
    ? node.scrollTo({ top: 0, behavior: 'smooth' })
    : scroll(node, 'scrollTop', 0);

M app/javascript/mastodon/service_worker/web_push_locales.js => app/javascript/mastodon/service_worker/web_push_locales.js +3 -0
@@ 1,3 1,6 @@
/* eslint-disable import/no-commonjs --
   We need to use CommonJS here as its imported into a preval file (`emoji_compressed.js`) */

/* @preval */

const fs   = require('fs');

D app/javascript/mastodon/store/configureStore.js => app/javascript/mastodon/store/configureStore.js +0 -16
@@ 1,16 0,0 @@
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import appReducer from '../reducers';
import loadingBarMiddleware from '../middleware/loading_bar';
import errorsMiddleware from '../middleware/errors';
import soundsMiddleware from '../middleware/sounds';

export const store = configureStore({
  reducer: appReducer,
  middleware: [
    thunk,
    loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
    errorsMiddleware(),
    soundsMiddleware(),
  ],
});

A app/javascript/mastodon/store/index.ts => app/javascript/mastodon/store/index.ts +27 -0
@@ 0,0 1,27 @@
import { configureStore } from '@reduxjs/toolkit';
import { rootReducer } from '../reducers';
import { loadingBarMiddleware } from './middlewares/loading_bar';
import { errorsMiddleware } from './middlewares/errors';
import { soundsMiddleware } from './middlewares/sounds';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware()
      .concat(
        loadingBarMiddleware({
          promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
        })
      )
      .concat(errorsMiddleware)
      .concat(soundsMiddleware()),
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

R app/javascript/mastodon/middleware/errors.js => app/javascript/mastodon/store/middlewares/errors.ts +7 -4
@@ 1,9 1,13 @@
import { showAlertForError } from '../actions/alerts';
import { Middleware } from 'redux';
import { showAlertForError } from '../../actions/alerts';
import { RootState } from '..';

const defaultFailSuffix = 'FAIL';

export default function errorsMiddleware() {
  return ({ dispatch }) => next => action => {
export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
  ({ dispatch }) =>
  (next) =>
  (action) => {
    if (action.type && !action.skipAlert) {
      const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');



@@ 14,4 18,3 @@ export default function errorsMiddleware() {

    return next(action);
  };
}

R app/javascript/mastodon/middleware/loading_bar.js => app/javascript/mastodon/store/middlewares/loading_bar.ts +33 -16
@@ 1,25 1,42 @@
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { Middleware } from 'redux';
import { RootState } from '..';

const defaultTypeSuffixes = ['PENDING', 'FULFILLED', 'REJECTED'];
interface Config {
  promiseTypeSuffixes?: string[];
}

const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
  'PENDING',
  'FULFILLED',
  'REJECTED',
];

export default function loadingBarMiddleware(config = {}) {
export const loadingBarMiddleware = (
  config: Config = {}
): Middleware<Record<string, never>, RootState> => {
  const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;

  return ({ dispatch }) => next => (action) => {
    if (action.type && !action.skipLoading) {
      const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
  return ({ dispatch }) =>
    (next) =>
    (action) => {
      if (action.type && !action.skipLoading) {
        const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;

      const isPending = new RegExp(`${PENDING}$`, 'g');
      const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
      const isRejected = new RegExp(`${REJECTED}$`, 'g');
        const isPending = new RegExp(`${PENDING}$`, 'g');
        const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
        const isRejected = new RegExp(`${REJECTED}$`, 'g');

      if (action.type.match(isPending)) {
        dispatch(showLoading());
      } else if (action.type.match(isFulfilled) || action.type.match(isRejected)) {
        dispatch(hideLoading());
        if (action.type.match(isPending)) {
          dispatch(showLoading());
        } else if (
          action.type.match(isFulfilled) ||
          action.type.match(isRejected)
        ) {
          dispatch(hideLoading());
        }
      }
    }

    return next(action);
  };
}
      return next(action);
    };
};

R app/javascript/mastodon/middleware/sounds.js => app/javascript/mastodon/store/middlewares/sounds.ts +21 -8
@@ 1,4 1,12 @@
const createAudio = sources => {
import { Middleware, AnyAction } from 'redux';
import { RootState } from '..';

interface AudioSource {
  src: string;
  type: string;
}

const createAudio = (sources: AudioSource[]) => {
  const audio = new Audio();
  sources.forEach(({ type, src }) => {
    const source = document.createElement('source');


@@ 9,7 17,7 @@ const createAudio = sources => {
  return audio;
};

const play = audio => {
const play = (audio: HTMLAudioElement) => {
  if (!audio.paused) {
    audio.pause();
    if (typeof audio.fastSeek === 'function') {


@@ 22,8 30,11 @@ const play = audio => {
  audio.play();
};

export default function soundsMiddleware() {
  const soundCache = {
export const soundsMiddleware = (): Middleware<
  Record<string, never>,
  RootState
> => {
  const soundCache: { [key: string]: HTMLAudioElement } = {
    boop: createAudio([
      {
        src: '/sounds/boop.ogg',


@@ 36,11 47,13 @@ export default function soundsMiddleware() {
    ]),
  };

  return () => next => action => {
    if (action.meta && action.meta.sound && soundCache[action.meta.sound]) {
      play(soundCache[action.meta.sound]);
  return () => (next) => (action: AnyAction) => {
    const sound = action?.meta?.sound;

    if (sound && soundCache[sound]) {
      play(soundCache[sound]);
    }

    return next(action);
  };
}
};

M app/javascript/mastodon/utils/filters.ts => app/javascript/mastodon/utils/filters.ts +12 -12
@@ 1,16 1,16 @@
export const toServerSideType = (columnType: string) => {
  switch (columnType) {
  case 'home':
  case 'notifications':
  case 'public':
  case 'thread':
  case 'account':
    return columnType;
  default:
    if (columnType.indexOf('list:') > -1) {
      return 'home';
    } else {
      return 'public'; // community, account, hashtag
    }
    case 'home':
    case 'notifications':
    case 'public':
    case 'thread':
    case 'account':
      return columnType;
    default:
      if (columnType.indexOf('list:') > -1) {
        return 'home';
      } else {
        return 'public'; // community, account, hashtag
      }
  }
};

M app/javascript/mastodon/utils/hashtags.ts => app/javascript/mastodon/utils/hashtags.ts +4 -22
@@ 5,17 5,8 @@ const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
const buildHashtagPatternRegex = () => {
  try {
    return new RegExp(
      '(?:^|[^\\/\\)\\w])#((' +
      '[' + WORD + '_]' +
      '[' + WORD + HASHTAG_SEPARATORS + ']*' +
      '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
      '[' + WORD + HASHTAG_SEPARATORS +']*' +
      '[' + WORD + '_]' +
      ')|(' +
      '[' + WORD + '_]*' +
      '[' + ALPHA + ']' +
      '[' + WORD + '_]*' +
      '))', 'iu',
      `(?:^|[^\\/\\)\\w])#(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))`,
      'iu'
    );
  } catch {
    return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;


@@ 25,17 16,8 @@ const buildHashtagPatternRegex = () => {
const buildHashtagRegex = () => {
  try {
    return new RegExp(
      '^((' +
      '[' + WORD + '_]' +
      '[' + WORD + HASHTAG_SEPARATORS + ']*' +
      '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
      '[' + WORD + HASHTAG_SEPARATORS +']*' +
      '[' + WORD + '_]' +
      ')|(' +
      '[' + WORD + '_]*' +
      '[' + ALPHA + ']' +
      '[' + WORD + '_]*' +
      '))$', 'iu',
      `^(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))$`,
      'iu'
    );
  } catch {
    return /^(\w*[a-zA-Z·]\w*)$/i;

M app/javascript/mastodon/utils/numbers.ts => app/javascript/mastodon/utils/numbers.ts +6 -7
@@ 21,7 21,7 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 * shortNumber(5936);
 * // => [5.936, 1000, 1]
 */
export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits
export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
export function toShortNumber(sourceNumber: number): ShortNumber {
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];


@@ 38,11 38,7 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
      sourceNumber < TEN_MILLIONS ? 1 : 0,
    ];
  } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
    return [
      sourceNumber / DECIMAL_UNITS.BILLION,
      DECIMAL_UNITS.BILLION,
      0,
    ];
    return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
  }

  return [sourceNumber, DECIMAL_UNITS.ONE, 0];


@@ 56,7 52,10 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 * // => 1790
 */
export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
export function pluralReady(
  sourceNumber: number,
  division: DecimalUnits
): number {
  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
    return sourceNumber;
  }

M app/javascript/mastodon/uuid.ts => app/javascript/mastodon/uuid.ts +3 -3
@@ 1,8 1,8 @@
export default function uuid(a?: string): string {
export function uuid(a?: string): string {
  return a
    ? (
      (a as any as number) ^
        (a as any as number) ^
        ((Math.random() * 16) >> ((a as any as number) / 4))
    ).toString(16)
      ).toString(16)
    : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
}

M app/javascript/packs/admin.jsx => app/javascript/packs/admin.jsx +2 -3
@@ 1,10 1,9 @@
import './public-path';
import ready from '../mastodon/ready';
import React from 'react';
import ReactDOM from 'react-dom';

ready(() => {
  const React    = require('react');
  const ReactDOM = require('react-dom');

  [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
    const componentName  = element.getAttribute('data-admin-component');
    const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props'));

M app/javascript/packs/application.js => app/javascript/packs/application.js +1 -1
@@ 1,5 1,5 @@
import './public-path';
import loadPolyfills from '../mastodon/load_polyfills';
import { loadPolyfills } from '../mastodon/polyfills';
import { start } from '../mastodon/common';

start();

M app/javascript/packs/public.jsx => app/javascript/packs/public.jsx +19 -12
@@ 1,12 1,24 @@
import './public-path';
import loadPolyfills from '../mastodon/load_polyfills';

import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
import { start } from '../mastodon/common';

import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
import 'cocoon-js-vanilla';
import axios from 'axios';
import { throttle } from 'lodash';
import { defineMessages } from 'react-intl';
import * as IntlMessageFormat  from 'intl-messageformat';
import { timeAgoString }  from '../mastodon/components/relative_timestamp';
import { delegate }  from '@rails/ujs';
import emojify  from '../mastodon/features/emoji/emoji';
import { getLocale }  from '../mastodon/locales';
import React  from 'react';
import ReactDOM  from 'react-dom';
import { createBrowserHistory }  from 'history';

start();

const messages = defineMessages({
  usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },


@@ 14,18 26,8 @@ const messages = defineMessages({
  passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' },
});

start();

function main() {
  const IntlMessageFormat = require('intl-messageformat').default;
  const { timeAgoString } = require('../mastodon/components/relative_timestamp');
  const { delegate } = require('@rails/ujs');
  const emojify = require('../mastodon/features/emoji/emoji').default;
  const { getLocale } = require('../mastodon/locales');
function loaded() {
  const { localeData } = getLocale();
  const React = require('react');
  const ReactDOM = require('react-dom');
  const { createBrowserHistory } = require('history');

  const scrollToDetailedStatus = () => {
    const history = createBrowserHistory();


@@ 238,6 240,11 @@ function main() {
  });
}


function main() {
  ready(loaded);
}

loadPolyfills()
  .then(main)
  .then(loadKeyboardExtensions)

M app/javascript/packs/share.jsx => app/javascript/packs/share.jsx +10 -7
@@ 1,23 1,26 @@
import './public-path';
import loadPolyfills from '../mastodon/load_polyfills';
import { loadPolyfills } from '../mastodon/polyfills';
import { start } from '../mastodon/common';
import ready from '../mastodon/ready';
import ComposeContainer  from '../mastodon/containers/compose_container';
import React from 'react';
import ReactDOM from 'react-dom';

start();

function loaded() {
  const ComposeContainer = require('../mastodon/containers/compose_container').default;
  const React = require('react');
  const ReactDOM = require('react-dom');
  const mountNode = document.getElementById('mastodon-compose');

  if (mountNode !== null) {
    const props = JSON.parse(mountNode.getAttribute('data-props'));
  if (mountNode) {
    const attr = mountNode.getAttribute('data-props');
    if(!attr) return;

    const props = JSON.parse(attr);
    ReactDOM.render(<ComposeContainer {...props} />, mountNode);
  }
}

function main() {
  const ready = require('../mastodon/ready').default;
  ready(loaded);
}


A app/javascript/types/image.d.ts => app/javascript/types/image.d.ts +35 -0
@@ 0,0 1,35 @@
/* eslint-disable import/no-default-export */
declare module '*.avif' {
  const path: string;
  export default path;
}

declare module '*.gif' {
  const path: string;
  export default path;
}

declare module '*.jpg' {
  const path: string;
  export default path;
}

declare module '*.jpg' {
  const path: string;
  export default path;
}

declare module '*.png' {
  const path: string;
  export default path;
}

declare module '*.svg' {
  const path: string;
  export default path;
}

declare module '*.webp' {
  const path: string;
  export default path;
}

M app/javascript/types/resources.ts => app/javascript/types/resources.ts +48 -4
@@ 1,10 1,54 @@
import type { Record } from 'immutable';

type AccountValues = {
  id: number;
type CustomEmoji = Record<{
  shortcode: string;
  static_url: string;
  url: string;
}>;

type AccountField = Record<{
  name: string;
  value: string;
  verified_at: string | null;
}>;

type AccountApiResponseValues = {
  acct: string;
  avatar: string;
  avatar_static: string;
  [key: string]: any;
  bot: boolean;
  created_at: string;
  discoverable: boolean;
  display_name: string;
  emojis: CustomEmoji[];
  fields: AccountField[];
  followers_count: number;
  following_count: number;
  group: boolean;
  header: string;
  header_static: string;
  id: string;
  last_status_at: string;
  locked: boolean;
  note: string;
  statuses_count: number;
  url: string;
  username: string;
};

type NormalizedAccountField = Record<{
  name_emojified: string;
  value_emojified: string;
  value_plain: string;
}>;

type NormalizedAccountValues = {
  display_name_html: string;
  fields: NormalizedAccountField[];
  note_emojified: string;
  note_plain: string;
};

export type Account = Record<AccountValues>;
export type Account = Record<
  AccountApiResponseValues & NormalizedAccountValues
>;

M config/webpack/shared.js => config/webpack/shared.js +1 -0
@@ 89,6 89,7 @@ module.exports = {

  module: {
    rules: Object.keys(rules).map(key => rules[key]),
    strictExportPresence: true,
  },

  plugins: [

M lib/mastodon/ip_blocks_cli.rb => lib/mastodon/ip_blocks_cli.rb +20 -1
@@ 11,7 11,7 @@ module Mastodon
      true
    end

    option :severity, required: true, enum: %w(no_access sign_up_requires_approval), desc: 'Severity of the block'
    option :severity, required: true, enum: %w(no_access sign_up_requires_approval sign_up_block), desc: 'Severity of the block'
    option :comment, aliases: [:c], desc: 'Optional comment'
    option :duration, aliases: [:d], type: :numeric, desc: 'Duration of the block in seconds'
    option :force, type: :boolean, aliases: [:f], desc: 'Overwrite existing blocks'


@@ 36,6 36,12 @@ module Mastodon
      failed    = 0

      addresses.each do |address|
        unless valid_ip_address?(address)
          say("#{address} is invalid", :red)
          failed += 1
          next
        end

        ip_block = IpBlock.find_by(ip: address)

        if ip_block.present? && !options[:force]


@@ 79,6 85,12 @@ module Mastodon
      skipped   = 0

      addresses.each do |address|
        unless valid_ip_address?(address)
          say("#{address} is invalid", :yellow)
          skipped += 1
          next
        end

        ip_blocks = if options[:force]
                      IpBlock.where('ip >>= ?', address)
                    else


@@ 126,5 138,12 @@ module Mastodon
        :red
      end
    end

    def valid_ip_address?(ip_address)
      IPAddr.new(ip_address)
      true
    rescue IPAddr::InvalidAddressError
      false
    end
  end
end

M package.json => package.json +9 -9
@@ 53,13 53,13 @@
    "cocoon-js-vanilla": "^1.3.0",
    "color-blend": "^4.0.0",
    "compression-webpack-plugin": "^6.1.1",
    "core-js": "^3.30.2",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.7",
    "cssnano": "^6.0.1",
    "detect-passive-events": "^2.0.3",
    "dotenv": "^16.0.3",
    "emoji-mart": "npm:emoji-mart-lazyload@latest",
    "es6-symbol": "^3.1.3",
    "escape-html": "^1.0.3",
    "exif-js": "^2.3.0",
    "express": "^4.18.2",


@@ 79,17 79,15 @@
    "jsdom": "^21.1.2",
    "lodash": "^4.17.21",
    "mark-loader": "^0.1.6",
    "marky": "^1.2.5",
    "mini-css-extract-plugin": "^1.6.2",
    "mkdirp": "^2.1.6",
    "npmlog": "^7.0.1",
    "object-assign": "^4.1.1",
    "object.values": "^1.1.6",
    "path-complete-extname": "^1.0.0",
    "pg": "^8.5.0",
    "pg-connection-string": "^2.5.0",
    "postcss": "^8.4.23",
    "postcss-loader": "^4.3.0",
    "promise.prototype.finally": "^3.1.4",
    "prop-types": "^15.8.1",
    "punycode": "^2.3.0",
    "react": "^16.14.0",


@@ 128,7 126,7 @@
    "tesseract.js": "^2.1.5",
    "tiny-queue": "^0.2.1",
    "twitter-text": "3.1.0",
    "uuid": "^8.3.1",
    "uuid": "^9.0.0",
    "webpack": "^4.46.0",
    "webpack-assets-manifest": "^4.0.6",
    "webpack-bundle-analyzer": "^4.8.0",


@@ 178,24 176,26 @@
    "@types/react-toggle": "^4.0.3",
    "@types/redux-immutable": "^4.0.3",
    "@types/requestidlecallback": "^0.3.5",
    "@types/uuid": "^8.3.4",
    "@types/uuid": "^9.0.0",
    "@types/webpack": "^4.41.33",
    "@types/yargs": "^17.0.24",
    "@typescript-eslint/eslint-plugin": "^5.59.2",
    "@typescript-eslint/parser": "^5.59.2",
    "@typescript-eslint/eslint-plugin": "^5.59.5",
    "@typescript-eslint/parser": "^5.59.5",
    "babel-jest": "^29.5.0",
    "eslint": "^8.39.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-plugin-formatjs": "^4.10.1",
    "eslint-plugin-import": "~2.27.5",
    "eslint-plugin-jsdoc": "^43.1.1",
    "eslint-plugin-jsx-a11y": "~6.7.1",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-promise": "~6.1.1",
    "eslint-plugin-react": "~7.32.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "husky": "^8.0.3",
    "jest": "^29.5.0",
    "jest-environment-jsdom": "^29.5.0",
    "lint-staged": "^13.2.2",
    "marky": "^1.2.5",
    "prettier": "^2.8.8",
    "raf": "^3.4.1",
    "react-intl-translations-manager": "^5.0.3",

M tsconfig.json => tsconfig.json +10 -4
@@ 2,20 2,26 @@
  "compilerOptions": {
    "jsx": "react",
    "target": "esnext",
    "module": "CommonJS",
    "moduleResolution": "node",
    "allowJs": true,
    "noEmit": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "baseUrl": "./",
    "paths": {
      "*": ["app/javascript/*"]
      "locales": ["app/javascript/locales"],
      "flavours/glitch": ["app/javascript/flavours/glitch"],
      "flavours/glitch/*": ["app/javascript/flavours/glitch/*"],
      "mastodon": ["app/javascript/mastodon"],
      "mastodon/*": ["app/javascript/mastodon/*"]
    }
  },
  "include": [
    "app/javascript/mastodon",
    "app/javascript/flavours/glitch",
    "app/javascript/packs"
    "app/javascript/packs",
    "app/javascript/types",
    "app/javascript/flavours/glitch"
  ]
}

M yarn.lock => yarn.lock +90 -121
@@ 2425,10 2425,10 @@
  dependencies:
    source-map "^0.6.1"

"@types/uuid@^8.3.4":
  version "8.3.4"
  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
  integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/uuid@^9.0.0":
  version "9.0.1"
  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6"
  integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==

"@types/warning@^3.0.0":
  version "3.0.0"


@@ 2475,15 2475,15 @@
  dependencies:
    "@types/yargs-parser" "*"

"@typescript-eslint/eslint-plugin@^5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd"
  integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==
"@typescript-eslint/eslint-plugin@^5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz#f156827610a3f8cefc56baeaa93cd4a5f32966b4"
  integrity sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==
  dependencies:
    "@eslint-community/regexpp" "^4.4.0"
    "@typescript-eslint/scope-manager" "5.59.2"
    "@typescript-eslint/type-utils" "5.59.2"
    "@typescript-eslint/utils" "5.59.2"
    "@typescript-eslint/scope-manager" "5.59.5"
    "@typescript-eslint/type-utils" "5.59.5"
    "@typescript-eslint/utils" "5.59.5"
    debug "^4.3.4"
    grapheme-splitter "^1.0.4"
    ignore "^5.2.0"


@@ 2491,31 2491,31 @@
    semver "^7.3.7"
    tsutils "^3.21.0"

"@typescript-eslint/parser@^5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8"
  integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==
"@typescript-eslint/parser@^5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.5.tgz#63064f5eafbdbfb5f9dfbf5c4503cdf949852981"
  integrity sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==
  dependencies:
    "@typescript-eslint/scope-manager" "5.59.2"
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/typescript-estree" "5.59.2"
    "@typescript-eslint/scope-manager" "5.59.5"
    "@typescript-eslint/types" "5.59.5"
    "@typescript-eslint/typescript-estree" "5.59.5"
    debug "^4.3.4"

"@typescript-eslint/scope-manager@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c"
  integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==
"@typescript-eslint/scope-manager@5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz#33ffc7e8663f42cfaac873de65ebf65d2bce674d"
  integrity sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==
  dependencies:
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/visitor-keys" "5.59.2"
    "@typescript-eslint/types" "5.59.5"
    "@typescript-eslint/visitor-keys" "5.59.5"

"@typescript-eslint/type-utils@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf"
  integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==
"@typescript-eslint/type-utils@5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz#485b0e2c5b923460bc2ea6b338c595343f06fc9b"
  integrity sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==
  dependencies:
    "@typescript-eslint/typescript-estree" "5.59.2"
    "@typescript-eslint/utils" "5.59.2"
    "@typescript-eslint/typescript-estree" "5.59.5"
    "@typescript-eslint/utils" "5.59.5"
    debug "^4.3.4"
    tsutils "^3.21.0"



@@ 2524,10 2524,10 @@
  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32"
  integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==

"@typescript-eslint/types@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655"
  integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==
"@typescript-eslint/types@5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.5.tgz#e63c5952532306d97c6ea432cee0981f6d2258c7"
  integrity sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==

"@typescript-eslint/typescript-estree@5.59.0":
  version "5.59.0"


@@ 2542,30 2542,30 @@
    semver "^7.3.7"
    tsutils "^3.21.0"

"@typescript-eslint/typescript-estree@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936"
  integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==
"@typescript-eslint/typescript-estree@5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz#9b252ce55dd765e972a7a2f99233c439c5101e42"
  integrity sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==
  dependencies:
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/visitor-keys" "5.59.2"
    "@typescript-eslint/types" "5.59.5"
    "@typescript-eslint/visitor-keys" "5.59.5"
    debug "^4.3.4"
    globby "^11.1.0"
    is-glob "^4.0.3"
    semver "^7.3.7"
    tsutils "^3.21.0"

"@typescript-eslint/utils@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4"
  integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==
"@typescript-eslint/utils@5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.5.tgz#15b3eb619bb223302e60413adb0accd29c32bcae"
  integrity sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==
  dependencies:
    "@eslint-community/eslint-utils" "^4.2.0"
    "@types/json-schema" "^7.0.9"
    "@types/semver" "^7.3.12"
    "@typescript-eslint/scope-manager" "5.59.2"
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/typescript-estree" "5.59.2"
    "@typescript-eslint/scope-manager" "5.59.5"
    "@typescript-eslint/types" "5.59.5"
    "@typescript-eslint/typescript-estree" "5.59.5"
    eslint-scope "^5.1.1"
    semver "^7.3.7"



@@ 2577,12 2577,12 @@
    "@typescript-eslint/types" "5.59.0"
    eslint-visitor-keys "^3.3.0"

"@typescript-eslint/visitor-keys@5.59.2":
  version "5.59.2"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750"
  integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==
"@typescript-eslint/visitor-keys@5.59.5":
  version "5.59.5"
  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz#ba5b8d6791a13cf9fea6716af1e7626434b29b9b"
  integrity sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==
  dependencies:
    "@typescript-eslint/types" "5.59.2"
    "@typescript-eslint/types" "5.59.5"
    eslint-visitor-keys "^3.3.0"

"@webassemblyjs/ast@1.9.0":


@@ 4140,6 4140,11 @@ core-js@^2.5.0:
  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==

core-js@^3.30.2:
  version "3.30.2"
  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.2.tgz#6528abfda65e5ad728143ea23f7a14f0dcf503fc"
  integrity sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==

core-util-is@~1.0.0:
  version "1.0.2"
  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"


@@ 4414,14 4419,6 @@ csstype@^3.0.2:
  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef"
  integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==

d@1, d@^1.0.1:
  version "1.0.1"
  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
  dependencies:
    es5-ext "^0.10.50"
    type "^1.0.1"

damerau-levenshtein@^1.0.8:
  version "1.0.8"
  resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"


@@ 4972,32 4969,6 @@ es-to-primitive@^1.2.1:
    is-date-object "^1.0.1"
    is-symbol "^1.0.2"

es5-ext@^0.10.35, es5-ext@^0.10.50:
  version "0.10.53"
  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
  dependencies:
    es6-iterator "~2.0.3"
    es6-symbol "~3.1.3"
    next-tick "~1.0.0"

es6-iterator@~2.0.3:
  version "2.0.3"
  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
  integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
  dependencies:
    d "1"
    es5-ext "^0.10.35"
    es6-symbol "^3.1.1"

es6-symbol@^3.1.1, es6-symbol@^3.1.3, es6-symbol@~3.1.3:
  version "3.1.3"
  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
  dependencies:
    d "^1.0.1"
    ext "^1.1.2"

escalade@^3.1.1:
  version "3.1.1"
  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"


@@ 5035,6 5006,11 @@ escodegen@^2.0.0:
  optionalDependencies:
    source-map "~0.6.1"

eslint-config-prettier@^8.8.0:
  version "8.8.0"
  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
  integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==

eslint-import-resolver-node@^0.3.7:
  version "0.3.7"
  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7"


@@ 5125,11 5101,23 @@ eslint-plugin-jsx-a11y@~6.7.1:
    object.fromentries "^2.0.6"
    semver "^6.3.0"

eslint-plugin-prettier@^4.2.1:
  version "4.2.1"
  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
  integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
  dependencies:
    prettier-linter-helpers "^1.0.0"

eslint-plugin-promise@~6.1.1:
  version "6.1.1"
  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816"
  integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==

eslint-plugin-react-hooks@^4.6.0:
  version "4.6.0"
  resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
  integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==

eslint-plugin-react@~7.32.2:
  version "7.32.2"
  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10"


@@ 5430,13 5418,6 @@ express@^4.17.1, express@^4.18.2:
    utils-merge "1.0.1"
    vary "~1.1.2"

ext@^1.1.2:
  version "1.4.0"
  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
  dependencies:
    type "^2.0.0"

extend-shallow@^2.0.1:
  version "2.0.1"
  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"


@@ 5471,6 5452,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==

fast-diff@^1.1.2:
  version "1.2.0"
  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
  integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==

fast-glob@^3.2.12, fast-glob@^3.2.9:
  version "3.2.12"
  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"


@@ 8244,11 8230,6 @@ neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2:
  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==

next-tick@~1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=

nice-try@^1.0.4:
  version "1.0.5"
  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"


@@ 9250,6 9231,13 @@ prelude-ls@~1.1.2:
  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=

prettier-linter-helpers@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
  integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
  dependencies:
    fast-diff "^1.1.2"

prettier@^2.8.8:
  version "2.8.8"
  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"


@@ 9294,15 9282,6 @@ promise-inflight@^1.0.1:
  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
  integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=

promise.prototype.finally@^3.1.4:
  version "3.1.4"
  resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.4.tgz#4e756a154e4db27fae24c6b18703495c31da3927"
  integrity sha512-nNc3YbgMfLzqtqvO/q5DP6RR0SiHI9pUPGzyDf1q+usTwCN2kjvAnJkBb7bHe3o+fFSBPpsGMoYtaSi+LTNqng==
  dependencies:
    call-bind "^1.0.2"
    define-properties "^1.1.4"
    es-abstract "^1.20.4"

prompts@^2.0.1:
  version "2.3.2"
  resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068"


@@ 11436,16 11415,6 @@ type-is@~1.6.18:
    media-typer "0.3.0"
    mime-types "~2.1.24"

type@^1.0.1:
  version "1.2.0"
  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==

type@^2.0.0:
  version "2.0.0"
  resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
  integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==

"typescript@^4.7 || 5", typescript@^5.0.4:
  version "5.0.4"
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"


@@ 11664,10 11633,10 @@ uuid@^3.3.2, uuid@^3.4.0:
  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==

uuid@^8.3.1:
  version "8.3.2"
  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
  integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
  version "9.0.0"
  resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
  integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==

v8-compile-cache@^2.1.1, v8-compile-cache@^2.3.0:
  version "2.3.0"