Browse Source

Merge remote-tracking branch 'upstream/master'

master
Zac 2 months ago
parent
commit
63d705f060
100 changed files with 1035 additions and 281 deletions
  1. 10
    10
      .circleci/config.yml
  2. 1
    0
      .env.production.sample
  3. 3
    3
      Dockerfile
  4. 5
    5
      Gemfile
  5. 24
    24
      Gemfile.lock
  6. 3
    2
      app/controllers/admin/tags_controller.rb
  7. 8
    0
      app/controllers/api/base_controller.rb
  8. 1
    2
      app/controllers/api/v1/accounts/statuses_controller.rb
  9. 30
    0
      app/controllers/api/v1/directories_controller.rb
  10. 11
    1
      app/controllers/application_controller.rb
  11. 23
    0
      app/controllers/auth/confirmations_controller.rb
  12. 2
    6
      app/controllers/directories_controller.rb
  13. 1
    1
      app/controllers/remote_follow_controller.rb
  14. 1
    1
      app/controllers/remote_interaction_controller.rb
  15. 1
    1
      app/controllers/well_known/webfinger_controller.rb
  16. 12
    0
      app/helpers/instance_helper.rb
  17. 20
    0
      app/helpers/statuses_helper.rb
  18. 9
    1
      app/javascript/core/admin.js
  19. 39
    8
      app/javascript/flavours/glitch/actions/compose.js
  20. 61
    0
      app/javascript/flavours/glitch/actions/directory.js
  21. 1
    1
      app/javascript/flavours/glitch/actions/polls.js
  22. 32
    0
      app/javascript/flavours/glitch/actions/trends.js
  23. 2
    2
      app/javascript/flavours/glitch/components/account.js
  24. 3
    2
      app/javascript/flavours/glitch/components/attachment_list.js
  25. 28
    0
      app/javascript/flavours/glitch/components/autosuggest_hashtag.js
  26. 8
    7
      app/javascript/flavours/glitch/components/autosuggest_input.js
  27. 8
    7
      app/javascript/flavours/glitch/components/autosuggest_textarea.js
  28. 2
    1
      app/javascript/flavours/glitch/components/column_back_button.js
  29. 2
    1
      app/javascript/flavours/glitch/components/column_back_button_slim.js
  30. 11
    10
      app/javascript/flavours/glitch/components/column_header.js
  31. 2
    2
      app/javascript/flavours/glitch/components/domain.js
  32. 18
    23
      app/javascript/flavours/glitch/components/icon.js
  33. 3
    2
      app/javascript/flavours/glitch/components/icon_button.js
  34. 1
    1
      app/javascript/flavours/glitch/components/icon_with_badge.js
  35. 4
    3
      app/javascript/flavours/glitch/components/load_gap.js
  36. 14
    7
      app/javascript/flavours/glitch/components/media_gallery.js
  37. 4
    3
      app/javascript/flavours/glitch/components/notification_purge_buttons.js
  38. 3
    3
      app/javascript/flavours/glitch/components/poll.js
  39. 35
    0
      app/javascript/flavours/glitch/components/radio_button.js
  40. 2
    2
      app/javascript/flavours/glitch/components/status_action_bar.js
  41. 5
    4
      app/javascript/flavours/glitch/components/status_content.js
  42. 14
    8
      app/javascript/flavours/glitch/components/status_icons.js
  43. 4
    4
      app/javascript/flavours/glitch/components/status_prepend.js
  44. 8
    5
      app/javascript/flavours/glitch/components/status_visibility_icon.js
  45. 8
    5
      app/javascript/flavours/glitch/containers/media_container.js
  46. 3
    3
      app/javascript/flavours/glitch/features/account/components/action_bar.js
  47. 8
    7
      app/javascript/flavours/glitch/features/account/components/header.js
  48. 3
    2
      app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
  49. 2
    2
      app/javascript/flavours/glitch/features/account_gallery/index.js
  50. 2
    1
      app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js
  51. 2
    2
      app/javascript/flavours/glitch/features/account_timeline/index.js
  52. 2
    2
      app/javascript/flavours/glitch/features/audio/index.js
  53. 2
    2
      app/javascript/flavours/glitch/features/blocks/index.js
  54. 2
    2
      app/javascript/flavours/glitch/features/bookmarked_statuses/index.js
  55. 2
    2
      app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js
  56. 2
    2
      app/javascript/flavours/glitch/features/community_timeline/index.js
  57. 1
    1
      app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js
  58. 7
    7
      app/javascript/flavours/glitch/features/compose/components/header.js
  59. 1
    1
      app/javascript/flavours/glitch/features/compose/components/poll_form.js
  60. 2
    2
      app/javascript/flavours/glitch/features/compose/components/publisher.js
  61. 2
    2
      app/javascript/flavours/glitch/features/compose/components/search.js
  62. 5
    5
      app/javascript/flavours/glitch/features/compose/components/search_results.js
  63. 2
    2
      app/javascript/flavours/glitch/features/compose/components/textarea_icons.js
  64. 1
    1
      app/javascript/flavours/glitch/features/compose/components/upload.js
  65. 1
    1
      app/javascript/flavours/glitch/features/compose/components/upload_progress.js
  66. 2
    2
      app/javascript/flavours/glitch/features/compose/containers/search_results_container.js
  67. 3
    2
      app/javascript/flavours/glitch/features/compose/containers/warning_container.js
  68. 2
    2
      app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.js
  69. 2
    2
      app/javascript/flavours/glitch/features/direct_timeline/index.js
  70. 190
    0
      app/javascript/flavours/glitch/features/directory/components/account_card.js
  71. 171
    0
      app/javascript/flavours/glitch/features/directory/index.js
  72. 2
    2
      app/javascript/flavours/glitch/features/domain_blocks/index.js
  73. 2
    2
      app/javascript/flavours/glitch/features/emoji_picker/index.js
  74. 2
    2
      app/javascript/flavours/glitch/features/favourited_statuses/index.js
  75. 2
    2
      app/javascript/flavours/glitch/features/favourites/index.js
  76. 2
    2
      app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js
  77. 2
    2
      app/javascript/flavours/glitch/features/follow_requests/index.js
  78. 2
    3
      app/javascript/flavours/glitch/features/followers/index.js
  79. 2
    3
      app/javascript/flavours/glitch/features/following/index.js
  80. 46
    0
      app/javascript/flavours/glitch/features/getting_started/components/trends.js
  81. 13
    0
      app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js
  82. 13
    5
      app/javascript/flavours/glitch/features/getting_started/index.js
  83. 2
    2
      app/javascript/flavours/glitch/features/getting_started_misc/index.js
  84. 2
    2
      app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js
  85. 1
    1
      app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js
  86. 2
    2
      app/javascript/flavours/glitch/features/hashtag_timeline/index.js
  87. 2
    2
      app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js
  88. 2
    2
      app/javascript/flavours/glitch/features/home_timeline/index.js
  89. 2
    2
      app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
  90. 2
    1
      app/javascript/flavours/glitch/features/list_adder/components/list.js
  91. 2
    2
      app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js
  92. 3
    2
      app/javascript/flavours/glitch/features/list_editor/components/search.js
  93. 2
    2
      app/javascript/flavours/glitch/features/list_editor/index.js
  94. 5
    4
      app/javascript/flavours/glitch/features/list_timeline/index.js
  95. 2
    2
      app/javascript/flavours/glitch/features/lists/components/new_list_form.js
  96. 2
    2
      app/javascript/flavours/glitch/features/lists/index.js
  97. 2
    2
      app/javascript/flavours/glitch/features/local_settings/navigation/index.js
  98. 3
    1
      app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js
  99. 2
    2
      app/javascript/flavours/glitch/features/local_settings/page/index.js
  100. 0
    0
      app/javascript/flavours/glitch/features/mutes/index.js

+ 10
- 10
.circleci/config.yml View File

@@ -3,7 +3,7 @@ version: 2
aliases:
- &defaults
docker:
- image: circleci/ruby:2.6.0-stretch-node
- image: circleci/ruby:2.6-stretch-node
environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/
DB_HOST: localhost
@@ -105,14 +105,14 @@ jobs:
install-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5.3-stretch-node
- image: circleci/ruby:2.5-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies

install-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4.5-stretch-node
- image: circleci/ruby:2.4-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies

@@ -134,40 +134,40 @@ jobs:
test-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6.0-stretch-node
- image: circleci/ruby:2.6-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:5.0.3-alpine3.8
- image: circleci/redis:5-alpine
<<: *test_steps

test-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5.3-stretch-node
- image: circleci/ruby:2.5-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.12-alpine
- image: circleci/redis:5-alpine
<<: *test_steps

test-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4.5-stretch-node
- image: circleci/ruby:2.4-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.12-alpine
- image: circleci/redis:5-alpine
<<: *test_steps

test-webui:
<<: *defaults
docker:
- image: circleci/node:8.15.0-stretch
- image: circleci/node:12.9-stretch
steps:
- *attach_workspace
- run: ./bin/retry yarn test:jest

+ 1
- 0
.env.production.sample View File

@@ -69,6 +69,7 @@ SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain

+ 3
- 3
Dockerfile View File

@@ -4,7 +4,7 @@ FROM ubuntu:18.04 as build-dep
SHELL ["bash", "-c"]

# Install Node
ENV NODE_VER="8.15.0"
ENV NODE_VER="12.9.1"
RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y install wget make gcc g++ python && \
@@ -17,7 +17,7 @@ RUN echo "Etc/UTC" > /etc/localtime && \
make install

# Install jemalloc
ENV JE_VER="5.1.0"
ENV JE_VER="5.2.1"
RUN apt update && \
apt -y install autoconf && \
cd ~ && \
@@ -30,7 +30,7 @@ RUN apt update && \
make install_bin install_include install_lib

# Install ruby
ENV RUBY_VER="2.6.1"
ENV RUBY_VER="2.6.4"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt update && \

+ 5
- 5
Gemfile View File

@@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.3'
gem 'dotenv-rails', '~> 2.7'

gem 'aws-sdk-s3', '~> 1.46', require: false
gem 'aws-sdk-s3', '~> 1.48', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@@ -24,7 +24,7 @@ gem 'streamio-ffmpeg', '~> 3.0'
gem 'blurhash', '~> 0.1'

gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.6'
gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.4', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.6'
@@ -94,7 +94,7 @@ gem 'tzinfo-data', '~> 1.2019'
gem 'webpacker', '~> 4.0'
gem 'webpush'

gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: '345b7a5733308af827e8491d284dbafa9128d7a2'
gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d'
gem 'json-ld-preloaded', '~> 3.0'
gem 'rdf-normalize', '~> 0.3'

@@ -116,12 +116,12 @@ end
group :test do
gem 'capybara', '~> 3.28'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.1'
gem 'faker', '~> 2.2'
gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.17', require: false
gem 'webmock', '~> 3.6'
gem 'webmock', '~> 3.7'
gem 'parallel_tests', '~> 2.29'
end


+ 24
- 24
Gemfile.lock View File

@@ -7,8 +7,8 @@ GIT

GIT
remote: https://github.com/ruby-rdf/json-ld.git
revision: 345b7a5733308af827e8491d284dbafa9128d7a2
ref: 345b7a5733308af827e8491d284dbafa9128d7a2
revision: e742697a0906e74e8bb777ef98137bc3955d981d
ref: e742697a0906e74e8bb777ef98137bc3955d981d
specs:
json-ld (3.0.2)
htmlentities (~> 4.3)
@@ -83,9 +83,9 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.3.3)
sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.5)
activerecord (>= 3.2, < 7.0)
@@ -97,8 +97,8 @@ GEM
av (0.9.0)
cocaine (~> 0.5.3)
aws-eventstream (1.0.3)
aws-partitions (1.193.0)
aws-sdk-core (3.61.1)
aws-partitions (1.207.0)
aws-sdk-core (3.65.1)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.1)
@@ -106,7 +106,7 @@ GEM
aws-sdk-kms (1.24.0)
aws-sdk-core (~> 3, >= 3.61.1)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.46.0)
aws-sdk-s3 (1.48.0)
aws-sdk-core (~> 3, >= 3.61.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
@@ -122,7 +122,7 @@ GEM
debug_inspector (>= 0.0.1)
blurhash (0.1.3)
ffi (~> 1.10.0)
bootsnap (1.4.4)
bootsnap (1.4.5)
msgpack (~> 1.0)
brakeman (4.6.1)
browser (2.6.1)
@@ -134,7 +134,7 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.0.0)
capistrano (3.11.0)
capistrano (3.11.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@@ -231,7 +231,7 @@ GEM
tzinfo
excon (0.62.0)
fabrication (2.20.2)
faker (2.1.2)
faker (2.2.1)
i18n (>= 0.8)
faraday (0.15.0)
multipart-post (>= 1.2, < 3)
@@ -371,14 +371,14 @@ GEM
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
msgpack (1.2.10)
msgpack (1.3.1)
multi_json (1.13.1)
multipart-post (2.0.0)
necromancer (0.5.0)
net-ldap (0.16.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (5.0.2)
net-scp (2.0.0)
net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0)
nio4r (2.4.0)
nokogiri (1.10.4)
mini_portile2 (~> 2.4.0)
@@ -418,7 +418,7 @@ GEM
parallel (1.17.0)
parallel_tests (2.29.2)
parallel
parser (2.6.3.0)
parser (2.6.4.0)
ast (~> 2.4.0)
parslet (1.8.2)
pastel (0.7.2)
@@ -444,7 +444,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (3.1.1)
public_suffix (4.0.1)
puma (4.1.0)
nio4r (~> 2.0)
pundit (2.1.0)
@@ -557,7 +557,7 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.3.1)
rubocop-rails (2.3.2)
rack (>= 1.1)
rubocop (>= 0.72.0)
ruby-progressbar (1.10.1)
@@ -603,7 +603,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.17.0)
sshkit (1.20.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.12)
@@ -647,7 +647,7 @@ GEM
uniform_notifier (1.12.1)
warden (1.2.8)
rack (>= 2.0.6)
webmock (3.6.2)
webmock (3.7.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -671,9 +671,9 @@ PLATFORMS
DEPENDENCIES
active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.6)
addressable (~> 2.6)
addressable (~> 2.7)
annotate (~> 2.7)
aws-sdk-s3 (~> 1.46)
aws-sdk-s3 (~> 1.48)
better_errors (~> 2.5)
binding_of_caller (~> 0.7)
blurhash (~> 0.1)
@@ -701,7 +701,7 @@ DEPENDENCIES
doorkeeper (~> 5.1)
dotenv-rails (~> 2.7)
fabrication (~> 2.20)
faker (~> 2.1)
faker (~> 2.2)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
@@ -789,7 +789,7 @@ DEPENDENCIES
tty-prompt (~> 0.19)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2019)
webmock (~> 3.6)
webmock (~> 3.7)
webpacker (~> 4.0)
webpush


+ 3
- 2
app/controllers/admin/tags_controller.rb View File

@@ -37,7 +37,8 @@ module Admin

def set_usage_by_domain
@usage_by_domain = @tag.statuses
.where(visibility: :public)
.with_public_visibility
.excluding_silenced_accounts
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
.joins(:account)
.group('accounts.domain')
@@ -56,7 +57,7 @@ module Admin
scope = scope.unreviewed if filter_params[:review] == 'unreviewed'
scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed'
scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review'
scope.order(score: :desc)
scope.order(max_score: :desc)
end

def filter_params

+ 8
- 0
app/controllers/api/base_controller.rb View File

@@ -36,6 +36,14 @@ class Api::BaseController < ApplicationController
render json: { error: 'This action is not allowed' }, status: 403
end

rescue_from Mastodon::RaceConditionError do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end

rescue_from ActionController::ParameterMissing do |e|
render json: { error: e.to_s }, status: 400
end

def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: (error.try(:description) || 'Not authorized') } }
end

+ 1
- 2
app/controllers/api/v1/accounts/statuses_controller.rb View File

@@ -29,14 +29,13 @@ class Api::V1::Accounts::StatusesController < Api::BaseController

def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))

statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?

statuses
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end

def permitted_account_statuses

+ 30
- 0
app/controllers/api/v1/directories_controller.rb View File

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

class Api::V1::DirectoriesController < Api::BaseController
before_action :require_enabled!
before_action :set_accounts

def show
render json: @accounts, each_serializer: REST::AccountSerializer
end

private

def require_enabled!
return not_found unless Setting.profile_directory
end

def set_accounts
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
end

def accounts_scope
Account.discoverable.tap do |scope|
scope.merge!(Account.local) if truthy_param?(:local)
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
end
end
end

+ 11
- 1
app/controllers/application_controller.rb View File

@@ -22,11 +22,13 @@ class ApplicationController < ActionController::Base
helper_method :whitelist_mode?

rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from ActionController::UnknownFormat, with: :not_acceptable
rescue_from ActionController::ParameterMissing, with: :bad_request
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
rescue_from Mastodon::RaceConditionError, with: :service_unavailable

before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :require_functional!, if: :user_signed_in?
@@ -166,10 +168,18 @@ class ApplicationController < ActionController::Base
respond_with_error(406)
end

def bad_request
respond_with_error(400)
end

def internal_server_error
respond_with_error(500)
end

def service_unavailable
respond_with_error(503)
end

def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
end

+ 23
- 0
app/controllers/auth/confirmations_controller.rb View File

@@ -5,19 +5,42 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController

before_action :set_body_classes
before_action :set_pack
before_action :require_unconfirmed!

skip_before_action :require_functional!

def new
super

resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
end

private

def set_pack
use_pack 'auth'
end

def require_unconfirmed!
redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
end

def set_body_classes
@body_classes = 'lighter'
end

def after_resending_confirmation_instructions_path_for(_resource_name)
if user_signed_in?
if current_user.confirmed? && current_user.approved?
edit_user_registration_path
else
auth_setup_path
end
else
new_user_session_path
end
end

def after_confirmation_path_for(_resource_name, user)
if user.created_by_application && truthy_param?(:redirect_to_app)
user.created_by_application.redirect_uri

+ 2
- 6
app/controllers/directories_controller.rb View File

@@ -7,7 +7,6 @@ class DirectoriesController < ApplicationController
before_action :require_enabled!
before_action :set_instance_presenter
before_action :set_tag, only: :show
before_action :set_tags
before_action :set_accounts
before_action :set_pack

@@ -33,13 +32,10 @@ class DirectoriesController < ApplicationController
@tag = Tag.discoverable.find_normalized!(params[:id])
end

def set_tags
@tags = Tag.discoverable.limit(30).reject { |tag| tag.cached_sample_accounts.empty? }
end

def set_accounts
@accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query|
@accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
query.merge!(Account.tagged_with(@tag.id)) if @tag
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
end
end


+ 1
- 1
app/controllers/remote_follow_controller.rb View File

@@ -30,7 +30,7 @@ class RemoteFollowController < ApplicationController
end

def session_params
{ acct: session[:remote_follow] }
{ acct: session[:remote_follow] || current_account&.username }
end

def set_pack

+ 1
- 1
app/controllers/remote_interaction_controller.rb View File

@@ -33,7 +33,7 @@ class RemoteInteractionController < ApplicationController
end

def session_params
{ acct: session[:remote_follow] }
{ acct: session[:remote_follow] || current_account&.username }
end

def set_status

+ 1
- 1
app/controllers/well_known/webfinger_controller.rb View File

@@ -11,7 +11,7 @@ module WellKnown

expires_in 3.days, public: true
render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
rescue ActiveRecord::RecordNotFound
rescue ActiveRecord::RecordNotFound, ActionController::ParameterMissing
head 404
end


+ 12
- 0
app/helpers/instance_helper.rb View File

@@ -8,4 +8,16 @@ module InstanceHelper
def site_hostname
@site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host
end

def description_for_sign_up
prefix = begin
if @invite.present?
I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username)
else
I18n.t('auth.description.prefix_sign_up')
end
end

safe_join([prefix, I18n.t('auth.description.suffix')], ' ')
end
end

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

@@ -34,6 +34,26 @@ module StatusesHelper
end
end

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

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

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

+ 9
- 1
app/javascript/core/admin.js View File

@@ -1,6 +1,7 @@
// This file will be loaded on admin pages, regardless of theme.

import { delegate } from 'rails-ujs';
import ready from '../mastodon/ready';

const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';

@@ -31,7 +32,7 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => {
});
});

delegate(document, '#domain_block_severity', 'change', ({ target }) => {
const onDomainBlockSeverityChange = (target) => {
const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');

@@ -42,4 +43,11 @@ delegate(document, '#domain_block_severity', 'change', ({ target }) => {
if (rejectReportsDiv) {
rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
}
};

delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));

ready(() => {
const input = document.getElementById('domain_block_severity');
if (input) onDomainBlockSeverityChange(input);
});

+ 39
- 8
app/javascript/flavours/glitch/actions/compose.js View File

@@ -12,7 +12,7 @@ import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';

let cancelFetchComposeSuggestionsAccounts;
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;

export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND';
@@ -352,10 +352,12 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
if (cancelFetchComposeSuggestionsAccounts) {
cancelFetchComposeSuggestionsAccounts();
}

api(getState).get('/api/v1/accounts/search', {
cancelToken: new CancelToken(cancel => {
cancelFetchComposeSuggestionsAccounts = cancel;
}),

params: {
q: token.slice(1),
resolve: false,
@@ -376,9 +378,32 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
dispatch(readyComposeSuggestionsEmojis(token, results));
};

const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
if (cancelFetchComposeSuggestionsTags) {
cancelFetchComposeSuggestionsTags();
}

dispatch(updateSuggestionTags(token));
};

api(getState).get('/api/v2/search', {
cancelToken: new CancelToken(cancel => {
cancelFetchComposeSuggestionsTags = cancel;
}),

params: {
type: 'hashtags',
q: token.slice(1),
resolve: false,
limit: 4,
},
}).then(({ data }) => {
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
}).catch(error => {
if (!isCancel(error)) {
dispatch(showAlertForError(error));
}
});
}, 200, { leading: true, trailing: true });

export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
@@ -412,16 +437,22 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
};
};

export const readyComposeSuggestionsTags = (token, tags) => ({
type: COMPOSE_SUGGESTIONS_READY,
token,
tags,
});

export function selectComposeSuggestion(position, token, suggestion, path) {
return (dispatch, getState) => {
let completion;
if (typeof suggestion === 'object' && suggestion.id) {
if (suggestion.type === 'emoji') {
dispatch(useEmoji(suggestion));
completion = suggestion.native || suggestion.colons;
} else if (suggestion[0] === '#') {
completion = suggestion;
} else {
completion = '@' + getState().getIn(['accounts', suggestion, 'acct']);
} else if (suggestion.type === 'hashtag') {
completion = `#${suggestion.name}`;
} else if (suggestion.type === 'account') {
completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']);
}

dispatch({

+ 61
- 0
app/javascript/flavours/glitch/actions/directory.js View File

@@ -0,0 +1,61 @@
import api from 'flavours/glitch/util/api';
import { importFetchedAccounts } from './importer';
import { fetchRelationships } from './accounts';

export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';

export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';

export const fetchDirectory = params => (dispatch, getState) => {
dispatch(fetchDirectoryRequest());

api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(fetchDirectoryFail(error)));
};

export const fetchDirectoryRequest = () => ({
type: DIRECTORY_FETCH_REQUEST,
});

export const fetchDirectorySuccess = accounts => ({
type: DIRECTORY_FETCH_SUCCESS,
accounts,
});

export const fetchDirectoryFail = error => ({
type: DIRECTORY_FETCH_FAIL,
error,
});

export const expandDirectory = params => (dispatch, getState) => {
dispatch(expandDirectoryRequest());

const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;

api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(expandDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(expandDirectoryFail(error)));
};

export const expandDirectoryRequest = () => ({
type: DIRECTORY_EXPAND_REQUEST,
});

export const expandDirectorySuccess = accounts => ({
type: DIRECTORY_EXPAND_SUCCESS,
accounts,
});

export const expandDirectoryFail = error => ({
type: DIRECTORY_EXPAND_FAIL,
error,
});

+ 1
- 1
app/javascript/flavours/glitch/actions/polls.js View File

@@ -1,4 +1,4 @@
import api from '../api';
import api from 'flavours/glitch/util/api';
import { importFetchedPoll } from './importer';

export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';

+ 32
- 0
app/javascript/flavours/glitch/actions/trends.js View File

@@ -0,0 +1,32 @@
import api from 'flavours/glitch/util/api';

export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';

export const fetchTrends = () => (dispatch, getState) => {
dispatch(fetchTrendsRequest());

api(getState)
.get('/api/v1/trends')
.then(({ data }) => dispatch(fetchTrendsSuccess(data)))
.catch(err => dispatch(fetchTrendsFail(err)));
};

export const fetchTrendsRequest = () => ({
type: TRENDS_FETCH_REQUEST,
skipLoading: true,
});

export const fetchTrendsSuccess = trends => ({
type: TRENDS_FETCH_SUCCESS,
trends,
skipLoading: true,
});

export const fetchTrendsFail = error => ({
type: TRENDS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});

+ 2
- 2
app/javascript/flavours/glitch/components/account.js View File

@@ -19,8 +19,8 @@ const messages = defineMessages({
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' },
});

@injectIntl
export default class Account extends ImmutablePureComponent {
export default @injectIntl
class Account extends ImmutablePureComponent {

static propTypes = {
account: ImmutablePropTypes.map.isRequired,

+ 3
- 2
app/javascript/flavours/glitch/components/attachment_list.js View File

@@ -2,6 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'flavours/glitch/components/icon';

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

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

return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a>
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
</li>
);
})}
@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<div className='attachment-list'>
<div className='attachment-list__icon'>
<i className='fa fa-link' />
<Icon id='link' />
</div>

<ul className='attachment-list__list'>

+ 28
- 0
app/javascript/flavours/glitch/components/autosuggest_hashtag.js View File

@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
import { FormattedMessage } from 'react-intl';

export default class AutosuggestHashtag extends React.PureComponent {

static propTypes = {
tag: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string,
history: PropTypes.array,
}).isRequired,
};

render () {
const { tag } = this.props;
const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));

return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
{tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>}
</div>
);
}

}

+ 8
- 7
app/javascript/flavours/glitch/components/autosuggest_input.js View File

@@ -1,6 +1,7 @@
import React from 'react';
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from 'flavours/glitch/util/rtl';
@@ -167,15 +168,15 @@ export default class AutosuggestInput extends ImmutablePureComponent {
const { selectedSuggestion } = this.state;
let inner, key;

if (typeof suggestion === 'object') {
if (suggestion.type === 'emoji') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
} else if (suggestion.type ==='hashtag') {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}

return (

+ 8
- 7
app/javascript/flavours/glitch/components/autosuggest_textarea.js View File

@@ -1,6 +1,7 @@
import React from 'react';
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from 'flavours/glitch/util/rtl';
@@ -173,15 +174,15 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
const { selectedSuggestion } = this.state;
let inner, key;

if (typeof suggestion === 'object') {
if (suggestion.type === 'emoji') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
} else if (suggestion.type === 'hashtag') {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}

return (

+ 2
- 1
app/javascript/flavours/glitch/components/column_back_button.js View File

@@ -1,6 +1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'flavours/glitch/components/icon';

export default class ColumnBackButton extends React.PureComponent {

@@ -25,7 +26,7 @@ export default class ColumnBackButton extends React.PureComponent {
render () {
return (
<button onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);

+ 2
- 1
app/javascript/flavours/glitch/components/column_back_button_slim.js View File

@@ -1,6 +1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';

export default class ColumnBackButtonSlim extends React.PureComponent {

@@ -26,7 +27,7 @@ export default class ColumnBackButtonSlim extends React.PureComponent {
return (
<div className='column-back-button--slim'>
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</div>

+ 11
- 10
app/javascript/flavours/glitch/components/column_header.js View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Icon from 'flavours/glitch/components/icon';

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

@@ -14,8 +15,8 @@ const messages = defineMessages({
enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
});

@injectIntl
export default class ColumnHeader extends React.PureComponent {
export default @injectIntl
class ColumnHeader extends React.PureComponent {

static contextTypes = {
router: PropTypes.object,
@@ -148,22 +149,22 @@ export default class ColumnHeader extends React.PureComponent {
}

if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;

moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
</div>
);
} else if (multiColumn) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
}

if (!pinned && (multiColumn || showBackButton)) {
backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
@@ -179,7 +180,7 @@ export default class ColumnHeader extends React.PureComponent {
}

if (children || multiColumn) {
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
}

const hasTitle = icon && title;
@@ -189,7 +190,7 @@ export default class ColumnHeader extends React.PureComponent {
<h1 className={buttonClassName}>
{hasTitle && (
<button onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
<Icon id={icon} fixedWidth className='column-header__icon' />
{title}
</button>
)}
@@ -206,7 +207,7 @@ export default class ColumnHeader extends React.PureComponent {
onClick={this.onEnterCleaningMode}
className={notifCleaningButtonClassName}
>
<i className='fa fa-eraser' />
<Icon id='eraser' />
</button>
) : null}
{collapseButton}

+ 2
- 2
app/javascript/flavours/glitch/components/domain.js View File

@@ -8,8 +8,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
});

@injectIntl
export default class Account extends ImmutablePureComponent {
export default @injectIntl
class Account extends ImmutablePureComponent {

static propTypes = {
domain: PropTypes.string,

+ 18
- 23
app/javascript/flavours/glitch/components/icon.js View File

@@ -1,26 +1,21 @@
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

// This just renders a FontAwesome icon.
export default function Icon ({
className,
fullwidth,
icon,
}) {
const computedClass = classNames('icon', 'fa', { 'fa-fw': fullwidth }, `fa-${icon}`, className);
return icon ? (
<span
aria-hidden='true'
className={computedClass}
/>
) : null;
}
export default class Icon extends React.PureComponent {

// Props.
Icon.propTypes = {
className: PropTypes.string,
fullwidth: PropTypes.bool,
icon: PropTypes.string,
};
static propTypes = {
id: PropTypes.string.isRequired,
className: PropTypes.string,
fixedWidth: PropTypes.bool,
};

render () {
const { id, className, fixedWidth, ...other } = this.props;

return (
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
);
}

}

+ 3
- 2
app/javascript/flavours/glitch/components/icon_button.js View File

@@ -3,6 +3,7 @@ import Motion from 'flavours/glitch/util/optional_motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';

export default class IconButton extends React.PureComponent {

@@ -133,7 +134,7 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
disabled={disabled}
>
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}
@@ -155,7 +156,7 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
disabled={disabled}
>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
{this.props.label}
</button>)
}

+ 1
- 1
app/javascript/flavours/glitch/components/icon_with_badge.js View File

@@ -6,7 +6,7 @@ const formatNumber = num => num > 40 ? '40+' : num;

const IconWithBadge = ({ id, count, className }) => (
<i className='icon-with-badge'>
<Icon icon={id} fixedWidth className={className} />
<Icon id={id} fixedWidth className={className} />
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
</i>
);

+ 4
- 3
app/javascript/flavours/glitch/components/load_gap.js View File

@@ -1,13 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';

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

@injectIntl
export default class LoadGap extends React.PureComponent {
export default @injectIntl
class LoadGap extends React.PureComponent {

static propTypes = {
disabled: PropTypes.bool,
@@ -25,7 +26,7 @@ export default class LoadGap extends React.PureComponent {

return (
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
<i className='fa fa-ellipsis-h' />
<Icon id='ellipsis-h' />
</button>
);
}

+ 14
- 7
app/javascript/flavours/glitch/components/media_gallery.js View File

@@ -179,7 +179,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') {
return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a>
</div>
@@ -254,8 +254,8 @@ class Item extends React.PureComponent {

}

@injectIntl
export default class MediaGallery extends React.PureComponent {
export default @injectIntl
class MediaGallery extends React.PureComponent {

static propTypes = {
sensitive: PropTypes.bool,
@@ -329,7 +329,8 @@ export default class MediaGallery extends React.PureComponent {
render () {
const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props;
const { visible } = this.state;
const size = media.take(4).size;
const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown');

const width = this.state.width || defaultWidth;

@@ -350,10 +351,16 @@ export default class MediaGallery extends React.PureComponent {
if (this.isStandaloneEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible} />);
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />);
}

if (visible) {
if (uncached) {
spoilerButton = (
<button type='button' disabled className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span>
</button>
);
} else if (visible) {
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
} else {
spoilerButton = (
@@ -365,7 +372,7 @@ export default class MediaGallery extends React.PureComponent {

return (
<div className={computedClass} style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
{spoilerButton}
{visible && sensitive && (
<span className='sensitive-marker'>

+ 4
- 3
app/javascript/flavours/glitch/components/notification_purge_buttons.js View File

@@ -10,6 +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';

const messages = defineMessages({
btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
@@ -18,8 +19,8 @@ const messages = defineMessages({
btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
});

@injectIntl
export default class NotificationPurgeButtons extends ImmutablePureComponent {
export default @injectIntl
class NotificationPurgeButtons extends ImmutablePureComponent {

static propTypes = {
onDeleteMarked : PropTypes.func.isRequired,
@@ -49,7 +50,7 @@ export default class NotificationPurgeButtons extends ImmutablePureComponent {
</button>

<button onClick={this.props.onDeleteMarked}>
<i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)}
<Icon id='trash' /><br />{intl.formatMessage(messages.btnApply)}
</button>
</div>
);

+ 3
- 3
app/javascript/flavours/glitch/components/poll.js View File

@@ -4,11 +4,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { vote, fetchPoll } from 'mastodon/actions/polls';
import Motion from 'mastodon/features/ui/util/optional_motion';
import { vote, fetchPoll } from 'flavours/glitch/actions/polls';
import Motion from 'flavours/glitch/util/optional_motion';
import spring from 'react-motion/lib/spring';
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'mastodon/features/emoji/emoji';
import emojify from 'flavours/glitch/util/emoji';
import RelativeTimestamp from './relative_timestamp';

const messages = defineMessages({

+ 35
- 0
app/javascript/flavours/glitch/components/radio_button.js View File

@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

export default class RadioButton extends React.PureComponent {

static propTypes = {
value: PropTypes.string.isRequired,
checked: PropTypes.bool,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
label: PropTypes.node.isRequired,
};

render () {
const { name, value, checked, onChange, label } = this.props;

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>
);
}

}

+ 2
- 2
app/javascript/flavours/glitch/components/status_action_bar.js View File

@@ -48,8 +48,8 @@ const obfuscatedCount = count => {
}
};

@injectIntl
export default class StatusActionBar extends ImmutablePureComponent {
export default @injectIntl
class StatusActionBar extends ImmutablePureComponent {

static contextTypes = {
router: PropTypes.object,

+ 5
- 4
app/javascript/flavours/glitch/components/status_content.js View File

@@ -5,6 +5,7 @@ import { isRtl } from 'flavours/glitch/util/rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/util/idna';

@@ -289,10 +290,10 @@ export default class StatusContent extends React.PureComponent {
key='0'
/>,
mediaIcon ? (
<i
className={
`fa fa-fw fa-${mediaIcon} status__content__spoiler-icon`
}
<Icon
fixedWidth
className='status__content__spoiler-icon'
id={mediaIcon}
aria-hidden='true'
key='1'
/>

+ 14
- 8
app/javascript/flavours/glitch/components/status_icons.js View File

@@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
// Mastodon imports.
import IconButton from './icon_button';
import VisibilityIcon from './status_visibility_icon';
import Icon from 'flavours/glitch/components/icon';

// Messages for use with internationalization stuff.
const messages = defineMessages({
@@ -21,8 +22,8 @@ const messages = defineMessages({
localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' },
});

@injectIntl
export default class StatusIcons extends React.PureComponent {
export default @injectIntl
class StatusIcons extends React.PureComponent {

static propTypes = {
status: ImmutablePropTypes.map.isRequired,
@@ -74,21 +75,26 @@ export default class StatusIcons extends React.PureComponent {
return (
<div className='status__info__icons'>
{status.get('in_reply_to_id', null) !== null ? (
<i
className={`fa fa-fw fa-comment status__reply-icon`}
<Icon
className='status__reply-icon'
fixedWidth
id='comment'
aria-hidden='true'
title={intl.formatMessage(messages.inReplyTo)}
/>
) : null}
{status.get('local_only') &&
<i
className={`fa fa-fw fa-home`}
<Icon
fixedWidth
id='home'
aria-hidden='true'
title={intl.formatMessage(messages.localOnly)}
/>}
{mediaIcon ? (
<i
className={`fa fa-fw fa-${mediaIcon} status__media-icon`}
<Icon
fixedWidth
className='status__media-icon'
id={mediaIcon}
aria-hidden='true'
title={this.mediaIconTitleText()}
/>

+ 4
- 4
app/javascript/flavours/glitch/components/status_prepend.js View File

@@ -3,6 +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';

export default class StatusPrepend extends React.PureComponent {

@@ -80,10 +81,9 @@ export default class StatusPrepend extends React.PureComponent {
return !type ? null : (
<aside className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend' : 'notification__message'}>
<div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
<i
className={`fa fa-fw fa-${
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'retweet'))
} status__prepend-icon`}
<Icon
className={`status__prepend-icon ${type === 'favourite' ? 'star-icon' : ''}`}
id={type === 'favourite' ? 'star' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'retweet'))}
/>
</div>
<Message />

+ 8
- 5
app/javascript/flavours/glitch/components/status_visibility_icon.js View File

@@ -3,6 +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';

const messages = defineMessages({
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
@@ -11,8 +12,8 @@ const messages = defineMessages({
direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});

@injectIntl
export default class VisibilityIcon extends ImmutablePureComponent {
export default @injectIntl
class VisibilityIcon extends ImmutablePureComponent {

static propTypes = {
visibility: PropTypes.string,
@@ -23,7 +24,7 @@ export default class VisibilityIcon extends ImmutablePureComponent {
render() {
const { withLabel, visibility, intl } = this.props;

const visibilityClass = {
const visibilityIcon = {
public: 'globe',
unlisted: 'unlock',
private: 'lock',
@@ -32,8 +33,10 @@ export default class VisibilityIcon extends ImmutablePureComponent {

const label = intl.formatMessage(messages[visibility]);

const icon = (<i
className={`status__visibility-icon fa fa-fw fa-${visibilityClass}`}
const icon = (<Icon
className='status__visibility-icon'
fixedWidth
id={visibilityIcon}
title={label}
aria-hidden='true'
/>);

+ 8
- 5
app/javascript/flavours/glitch/containers/media_container.js View File

@@ -7,6 +7,7 @@ import MediaGallery from 'flavours/glitch/components/media_gallery';
import Video from 'flavours/glitch/features/video';
import Card from 'flavours/glitch/features/status/components/card';
import Poll from 'flavours/glitch/components/poll';
import Hashtag from 'flavours/glitch/components/hashtag';
import Audio from 'flavours/glitch/features/audio';
import ModalRoot from 'flavours/glitch/components/modal_root';
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
@@ -15,7 +16,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
const { localeData, messages } = getLocale();
addLocaleData(localeData);

const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Audio };
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };

export default class MediaContainer extends PureComponent {

@@ -56,12 +57,13 @@ export default class MediaContainer extends PureComponent {
{[].map.call(components, (component, i) => {
const componentName = component.getAttribute('data-component');
const Component = MEDIA_COMPONENTS[componentName];
const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props'));

Object.assign(props, {
...(media ? { media: fromJS(media) } : {}),
...(card ? { card: fromJS(card) } : {}),
...(poll ? { poll: fromJS(poll) } : {}),
...(media ? { media: fromJS(media) } : {}),
...(card ? { card: fromJS(card) } : {}),
...(poll ? { poll: fromJS(poll) } : {}),
...(hashtag ? { hashtag: fromJS(hashtag) } : {}),

...(componentName === 'Video' ? {
onOpenVideo: this.handleOpenVideo,
@@ -75,6 +77,7 @@ export default class MediaContainer extends PureComponent {
component,
);
})}

<ModalRoot onClose={this.handleCloseMedia}>
{this.state.media && (
<MediaModal

+ 3
- 3
app/javascript/flavours/glitch/features/account/components/action_bar.js View File

@@ -8,8 +8,8 @@ import { me, isStaff } from 'flavours/glitch/util/initial_state';
import { profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links';
import Icon from 'flavours/glitch/components/icon';

@injectIntl
export default class ActionBar extends React.PureComponent {
export default @injectIntl
class ActionBar extends React.PureComponent {

static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@@ -31,7 +31,7 @@ export default class ActionBar extends React.PureComponent {
if (account.get('acct') !== account.get('username')) {
extraInfo = (
<div className='account__disclaimer'>
<Icon icon='info-circle' fixedWidth /> <FormattedMessage
<Icon id='info-circle' fixedWidth /> <FormattedMessage
id='account.disclaimer_full'
defaultMessage="Information below may reflect the user's profile incompletely."
/>

+ 8
- 7
app/javascript/flavours/glitch/features/account/components/header.js View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff } from 'flavours/glitch/util/initial_state';
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import Avatar from 'flavours/glitch/components/avatar';
@@ -69,7 +70,7 @@ class Header extends ImmutablePureComponent {
};

openEditProfile = () => {
window.open('/settings/profile', '_blank');
window.open(profileLink, '_blank');
}

_updateEmojis () {
@@ -148,7 +149,7 @@ class Header extends ImmutablePureComponent {
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
} else {
} else if (profileLink) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
}

@@ -157,7 +158,7 @@ class Header extends ImmutablePureComponent {
}

if (account.get('locked')) {
lockedIcon = <Icon icon='lock' title={intl.formatMessage(messages.account_locked)} />;
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
}

if (account.get('id') !== me) {
@@ -172,8 +173,8 @@ class Header extends ImmutablePureComponent {
}

if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
@@ -223,9 +224,9 @@ class Header extends ImmutablePureComponent {
}
}

if (account.get('id') !== me && isStaff) {
if (account.get('id') !== me && isStaff && accountAdminLink) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) });
}

const content = { __html: account.get('note_emojified') };

+ 3
- 2
app/javascript/flavours/glitch/features/account_gallery/components/media_item.js View File

@@ -2,6 +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 Icon from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state';
import classNames from 'classnames';
import { decode } from 'blurhash';
@@ -97,7 +98,7 @@ export default class MediaItem extends ImmutablePureComponent {
} else if (attachment.get('type') === 'audio') {
thumbnail = (
<span className='account-gallery__item__icons'>
<i className='fa fa-music' />
<Icon id='music' />
</span>
);
} else if (attachment.get('type') === 'image') {
@@ -139,7 +140,7 @@ export default class MediaItem extends ImmutablePureComponent {

const icon = (
<span className='account-gallery__item__icons'>
<i className='fa fa-eye-slash' />
<Icon id='eye-slash' />
</span>
);


+ 2
- 2
app/javascript/flavours/glitch/features/account_gallery/index.js View File

@@ -45,8 +45,8 @@ class LoadMoreMedia extends ImmutablePureComponent {

}

@connect(mapStateToProps)
export default class AccountGallery extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class AccountGallery extends ImmutablePureComponent {

static propTypes = {
params: PropTypes.object.isRequired,

+ 2
- 1
app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js View File

@@ -5,6 +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';

export default class MovedNote extends ImmutablePureComponent {

@@ -35,7 +36,7 @@ export default class MovedNote extends ImmutablePureComponent {
return (
<div className='account__moved-note'>
<div className='account__moved-note__message'>
<div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
<div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
</div>


+ 2
- 2
app/javascript/flavours/glitch/features/account_timeline/index.js View File

@@ -27,8 +27,8 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
};
};

@connect(mapStateToProps)
export default class AccountTimeline extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class AccountTimeline extends ImmutablePureComponent {

static propTypes = {
params: PropTypes.object.isRequired,

+ 2
- 2
app/javascript/flavours/glitch/features/audio/index.js View File

@@ -198,8 +198,8 @@ class Audio extends React.PureComponent {
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon icon={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon icon={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>

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

+ 2
- 2
app/javascript/flavours/glitch/features/blocks/index.js View File

@@ -21,9 +21,9 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
});

@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Blocks extends ImmutablePureComponent {
class Blocks extends ImmutablePureComponent {

static propTypes = {
params: PropTypes.object.isRequired,

+ 2
- 2
app/javascript/flavours/glitch/features/bookmarked_statuses/index.js View File

@@ -21,9 +21,9 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
});

@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Bookmarks extends ImmutablePureComponent {
class Bookmarks extends ImmutablePureComponent {

static propTypes = {
dispatch: PropTypes.func.isRequired,

+ 2
- 2
app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js View File

@@ -10,8 +10,8 @@ const messages = defineMessages({
settings: { id: 'home.settings', defaultMessage: 'Column settings' },
});

@injectIntl
export default class ColumnSettings extends React.PureComponent {
export default @injectIntl
class ColumnSettings extends React.PureComponent {

static propTypes = {
settings: ImmutablePropTypes.map.isRequired,

+ 2
- 2
app/javascript/flavours/glitch/features/community_timeline/index.js View File

@@ -25,9 +25,9 @@ const mapStateToProps = (state, { onlyMedia, columnId }) => {
};
};

@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class CommunityTimeline extends React.PureComponent {
class CommunityTimeline extends React.PureComponent {

static defaultProps = {
onlyMedia: false,

+ 1
- 1
app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js View File

@@ -185,7 +185,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
if (on !== null && typeof on !== 'undefined') {
prefix = <Toggle checked={on} onChange={this.handleClick} />;
} else if (icon) {
prefix = <Icon className='icon' fullwidth icon={icon} />
prefix = <Icon className='icon' fixedWidth id={icon} />
}

return (

+ 7
- 7
app/javascript/flavours/glitch/features/compose/components/header.js View File

@@ -82,13 +82,13 @@ class Header extends ImmutablePureComponent {
aria-label={intl.formatMessage(messages.start)}
title={intl.formatMessage(messages.start)}
to='/getting-started'
><Icon icon='asterisk' /></Link>
><Icon id='asterisk' /></Link>
{renderForColumn('HOME', (
<Link
aria-label={intl.formatMessage(messages.home_timeline)}
title={intl.formatMessage(messages.home_timeline)}
to='/timelines/home'
><Icon icon='home' /></Link>
><Icon id='home' /></Link>
))}
{renderForColumn('NOTIFICATIONS', (
<Link
@@ -97,7 +97,7 @@ class Header extends ImmutablePureComponent {
to='/notifications'
>
<span className='icon-badge-wrapper'>
<Icon icon='bell' />
<Icon id='bell' />
{ showNotificationsBadge && unreadNotifications > 0 && <div className='icon-badge' />}
</span>
</Link>
@@ -107,27 +107,27 @@ class Header extends ImmutablePureComponent {
aria-label={intl.formatMessage(messages.community)}
title={intl.formatMessage(messages.community)}
to='/timelines/public/local'
><Icon icon='users' /></Link>
><Icon id='users' /></Link>
))}
{renderForColumn('PUBLIC', (
<Link
aria-label={intl.formatMessage(messages.public)}
title={intl.formatMessage(messages.public)}
to='/timelines/public'
><Icon icon='globe' /></Link>
><Icon id='globe' /></Link>
))}
<a
aria-label={intl.formatMessage(messages.settings)}
onClick={onSettingsClick}
href='#'
title={intl.formatMessage(messages.settings)}
><Icon icon='cogs' /></a>
><Icon id='cogs' /></a>
<a
aria-label={intl.formatMessage(messages.logout)}
onClick={this.handleLogoutClick}
href={ signOutLink }
title={intl.formatMessage(messages.logout)}
><Icon icon='sign-out' /></a>
><Icon id='sign-out' /></a>
</nav>
);
};

+ 1
- 1
app/javascript/flavours/glitch/features/compose/components/poll_form.js View File

@@ -132,7 +132,7 @@ class PollForm extends ImmutablePureComponent {
{options.size < pollLimits.max_options && (
<label className='poll__text editable'>
<span className={classNames('poll__input')} style={{ opacity: 0 }} />
<button className='button button-secondary' onClick={this.handleAddOption}><Icon icon='plus' /> <FormattedMessage {...messages.add_option} /></button>
<button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
</label>
)}
</ul>

+ 2
- 2
app/javascript/flavours/glitch/features/compose/components/publisher.js View File

@@ -58,7 +58,7 @@ class Publisher extends ImmutablePureComponent {
text={
<span>
<Icon
icon={{
id={{
public: 'globe',
unlisted: 'unlock',
private: 'lock',
@@ -80,7 +80,7 @@ class Publisher extends ImmutablePureComponent {
return (
<span>
<Icon
icon={{
id={{
direct: 'envelope',
private: 'lock',
public: 'globe',

+ 2
- 2
app/javascript/flavours/glitch/features/compose/components/search.js View File

@@ -153,8 +153,8 @@ class Search extends React.PureComponent {
role='button'
tabIndex='0'
>
<Icon icon='search' className={hasValue ? '' : 'active'} />
<Icon icon='times-circle' className={hasValue ? 'active' : ''} />
<Icon id='search' className={hasValue ? '' : 'active'} />
<Icon id='times-circle' className={hasValue ? 'active' : ''} />
</div>

<Overlay show={expanded && !hasValue} placement='bottom' target={this}>

+ 5
- 5
app/javascript/flavours/glitch/features/compose/components/search_results.js View File

@@ -47,7 +47,7 @@ class SearchResults extends ImmutablePureComponent {
<div className='drawer--results'>
<div className='trends'>
<div className='trends__header'>
<i className='fa fa-user-plus fa-fw' />
<Icon fixedWidth id='user-plus' />
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
</div>

@@ -82,7 +82,7 @@ class SearchResults extends ImmutablePureComponent {
count += results.get('accounts').size;
accounts = (
<section>
<h5><Icon icon='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>

{results.get('accounts').map(accountId => <AccountContainer id={accountId} key={accountId} />)}

@@ -95,7 +95,7 @@ class SearchResults extends ImmutablePureComponent {
count += results.get('statuses').size;
statuses = (
<section>
<h5><Icon icon='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>

{results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId}/>)}

@@ -108,7 +108,7 @@ class SearchResults extends ImmutablePureComponent {
count += results.get('hashtags').size;
hashtags = (
<section>
<h5><Icon icon='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>

{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}

@@ -121,7 +121,7 @@ class SearchResults extends ImmutablePureComponent {
return (
<div className='drawer--results'>
<header className='search-results__header'>
<Icon icon='search' fixedWidth />
<Icon id='search' fixedWidth />
<FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
</header>


+ 2
- 2
app/javascript/flavours/glitch/features/compose/components/textarea_icons.js View File

@@ -47,8 +47,8 @@ class TextareaIcons extends ImmutablePureComponent {
title={intl.formatMessage(message)}
>
<Icon
fullwidth
icon={icon}
fixedWidth
id={icon}
/>
</span>
) : null

+ 1
- 1
app/javascript/flavours/glitch/features/compose/components/upload.js View File

@@ -44,7 +44,7 @@ export default class Upload extends ImmutablePureComponent {
{({ scale }) => (
<div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className={classNames('composer--upload_form--actions', { active: true })}>
<button className='icon-button' onClick={this.handleUndoClick}><Icon icon='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div>
</div>