mirror of
https://git.bsd.gay/fef/nyastodon.git
synced 2024-12-24 18:13:42 +01:00
Merge upstream!! #64 <3 <3
This commit is contained in:
commit
79d898ae0a
340 changed files with 4980 additions and 2321 deletions
1
.babelrc
1
.babelrc
|
@ -44,6 +44,7 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"transform-react-inline-elements",
|
||||||
[
|
[
|
||||||
"transform-runtime",
|
"transform-runtime",
|
||||||
{
|
{
|
||||||
|
|
|
@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
|
||||||
# PAPERCLIP_ROOT_URL=/system
|
# PAPERCLIP_ROOT_URL=/system
|
||||||
|
|
||||||
# Optional asset host for multi-server setups
|
# Optional asset host for multi-server setups
|
||||||
# CDN_HOST=assets.example.com
|
# CDN_HOST=https://assets.example.com
|
||||||
|
|
||||||
# S3 (optional)
|
# S3 (optional)
|
||||||
# S3_ENABLED=true
|
# S3_ENABLED=true
|
||||||
|
|
|
@ -32,6 +32,7 @@ addons:
|
||||||
- g++-6
|
- g++-6
|
||||||
- libprotobuf-dev
|
- libprotobuf-dev
|
||||||
- protobuf-compiler
|
- protobuf-compiler
|
||||||
|
- libicu-dev
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.3.4
|
- 2.3.4
|
||||||
|
|
1
Aptfile
1
Aptfile
|
@ -3,3 +3,4 @@ libprotobuf-dev
|
||||||
ffmpeg
|
ffmpeg
|
||||||
libxdamage1
|
libxdamage1
|
||||||
libxfixes3
|
libxfixes3
|
||||||
|
libicu-dev
|
||||||
|
|
|
@ -25,6 +25,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
file \
|
file \
|
||||||
git \
|
git \
|
||||||
|
icu-dev \
|
||||||
imagemagick@edge \
|
imagemagick@edge \
|
||||||
libpq \
|
libpq \
|
||||||
libxml2 \
|
libxml2 \
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -18,9 +18,11 @@ gem 'aws-sdk', '~> 2.9'
|
||||||
gem 'paperclip', '~> 5.1'
|
gem 'paperclip', '~> 5.1'
|
||||||
gem 'paperclip-av-transcoder', '~> 0.6'
|
gem 'paperclip-av-transcoder', '~> 0.6'
|
||||||
|
|
||||||
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.5'
|
gem 'addressable', '~> 2.5'
|
||||||
gem 'bootsnap'
|
gem 'bootsnap'
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
|
gem 'charlock_holmes', '~> 0.7.3'
|
||||||
gem 'cld3', '~> 3.1'
|
gem 'cld3', '~> 3.1'
|
||||||
gem 'devise', '~> 4.2'
|
gem 'devise', '~> 4.2'
|
||||||
gem 'devise-two-factor', '~> 3.0'
|
gem 'devise-two-factor', '~> 3.0'
|
||||||
|
@ -35,6 +37,7 @@ gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'httplog', '~> 0.99'
|
gem 'httplog', '~> 0.99'
|
||||||
gem 'kaminari', '~> 1.0'
|
gem 'kaminari', '~> 1.0'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
|
gem 'mime-types', '~> 3.1'
|
||||||
gem 'nokogiri', '~> 1.7'
|
gem 'nokogiri', '~> 1.7'
|
||||||
gem 'oj', '~> 3.0'
|
gem 'oj', '~> 3.0'
|
||||||
gem 'ostatus2', '~> 2.0'
|
gem 'ostatus2', '~> 2.0'
|
||||||
|
|
60
Gemfile.lock
60
Gemfile.lock
|
@ -24,6 +24,11 @@ GEM
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||||
|
active_model_serializers (0.10.6)
|
||||||
|
actionpack (>= 4.1, < 6)
|
||||||
|
activemodel (>= 4.1, < 6)
|
||||||
|
case_transform (>= 0.2)
|
||||||
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
|
||||||
active_record_query_trace (1.5.4)
|
active_record_query_trace (1.5.4)
|
||||||
activejob (5.1.2)
|
activejob (5.1.2)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.2)
|
||||||
|
@ -41,7 +46,7 @@ GEM
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.5.1)
|
addressable (2.5.1)
|
||||||
public_suffix (~> 2.0, >= 2.0.2)
|
public_suffix (~> 2.0, >= 2.0.2)
|
||||||
airbrussh (1.2.0)
|
airbrussh (1.3.0)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
annotate (2.7.2)
|
annotate (2.7.2)
|
||||||
activerecord (>= 3.2, < 6.0)
|
activerecord (>= 3.2, < 6.0)
|
||||||
|
@ -52,13 +57,13 @@ GEM
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-sdk (2.9.37)
|
aws-sdk (2.10.6)
|
||||||
aws-sdk-resources (= 2.9.37)
|
aws-sdk-resources (= 2.10.6)
|
||||||
aws-sdk-core (2.9.37)
|
aws-sdk-core (2.10.6)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-resources (2.9.37)
|
aws-sdk-resources (2.10.6)
|
||||||
aws-sdk-core (= 2.9.37)
|
aws-sdk-core (= 2.10.6)
|
||||||
aws-sigv4 (1.0.0)
|
aws-sigv4 (1.0.0)
|
||||||
bcrypt (3.1.11)
|
bcrypt (3.1.11)
|
||||||
better_errors (2.1.1)
|
better_errors (2.1.1)
|
||||||
|
@ -67,7 +72,7 @@ GEM
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
bootsnap (1.0.0)
|
bootsnap (1.1.1)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (3.6.2)
|
brakeman (3.6.2)
|
||||||
browser (2.4.0)
|
browser (2.4.0)
|
||||||
|
@ -78,7 +83,7 @@ GEM
|
||||||
bundler-audit (0.5.0)
|
bundler-audit (0.5.0)
|
||||||
bundler (~> 1.2)
|
bundler (~> 1.2)
|
||||||
thor (~> 0.18)
|
thor (~> 0.18)
|
||||||
capistrano (3.8.1)
|
capistrano (3.8.2)
|
||||||
airbrussh (>= 1.0.0)
|
airbrussh (>= 1.0.0)
|
||||||
i18n
|
i18n
|
||||||
rake (>= 10.0.0)
|
rake (>= 10.0.0)
|
||||||
|
@ -94,15 +99,18 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (2.14.2)
|
capybara (2.14.4)
|
||||||
addressable
|
addressable
|
||||||
mime-types (>= 1.16)
|
mime-types (>= 1.16)
|
||||||
nokogiri (>= 1.3.3)
|
nokogiri (>= 1.3.3)
|
||||||
rack (>= 1.0.0)
|
rack (>= 1.0.0)
|
||||||
rack-test (>= 0.5.4)
|
rack-test (>= 0.5.4)
|
||||||
xpath (~> 2.0)
|
xpath (~> 2.0)
|
||||||
|
case_transform (0.2)
|
||||||
|
activesupport
|
||||||
|
charlock_holmes (0.7.3)
|
||||||
chunky_png (1.3.8)
|
chunky_png (1.3.8)
|
||||||
cld3 (3.1.2)
|
cld3 (3.1.3)
|
||||||
ffi (>= 1.1.0, < 1.10.0)
|
ffi (>= 1.1.0, < 1.10.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
|
@ -142,9 +150,9 @@ GEM
|
||||||
thread
|
thread
|
||||||
thread_safe
|
thread_safe
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
erubi (1.6.0)
|
erubi (1.6.1)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
et-orbi (1.0.4)
|
et-orbi (1.0.5)
|
||||||
tzinfo
|
tzinfo
|
||||||
execjs (2.7.0)
|
execjs (2.7.0)
|
||||||
fabrication (2.16.1)
|
fabrication (2.16.1)
|
||||||
|
@ -161,7 +169,7 @@ GEM
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
http (~> 2.0)
|
http (~> 2.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
hamlit (2.8.1)
|
hamlit (2.8.4)
|
||||||
temple (>= 0.8.0)
|
temple (>= 0.8.0)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
|
@ -182,9 +190,9 @@ GEM
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (1.0.3)
|
http-form_data (1.0.3)
|
||||||
http_accept_language (2.1.0)
|
http_accept_language (2.1.1)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
httplog (0.99.3)
|
httplog (0.99.4)
|
||||||
colorize
|
colorize
|
||||||
rack
|
rack
|
||||||
i18n (0.8.4)
|
i18n (0.8.4)
|
||||||
|
@ -200,6 +208,7 @@ GEM
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
jmespath (1.3.1)
|
jmespath (1.3.1)
|
||||||
json (2.1.0)
|
json (2.1.0)
|
||||||
|
jsonapi-renderer (0.1.2)
|
||||||
kaminari (1.0.1)
|
kaminari (1.0.1)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.0.1)
|
kaminari-actionview (= 1.0.1)
|
||||||
|
@ -249,8 +258,8 @@ GEM
|
||||||
mini_portile2 (~> 2.2.0)
|
mini_portile2 (~> 2.2.0)
|
||||||
nokogumbo (1.4.13)
|
nokogumbo (1.4.13)
|
||||||
nokogiri
|
nokogiri
|
||||||
oj (3.1.0)
|
oj (3.2.0)
|
||||||
openssl (2.0.3)
|
openssl (2.0.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostatus2 (2.0.1)
|
ostatus2 (2.0.1)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
|
@ -272,7 +281,7 @@ GEM
|
||||||
parallel
|
parallel
|
||||||
parser (2.4.0.0)
|
parser (2.4.0.0)
|
||||||
ast (~> 2.2)
|
ast (~> 2.2)
|
||||||
pg (0.20.0)
|
pg (0.21.0)
|
||||||
pghero (1.7.0)
|
pghero (1.7.0)
|
||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.2.3)
|
pkg-config (1.2.3)
|
||||||
|
@ -374,7 +383,7 @@ GEM
|
||||||
rspec-expectations (~> 3.6.0)
|
rspec-expectations (~> 3.6.0)
|
||||||
rspec-mocks (~> 3.6.0)
|
rspec-mocks (~> 3.6.0)
|
||||||
rspec-support (~> 3.6.0)
|
rspec-support (~> 3.6.0)
|
||||||
rspec-sidekiq (3.0.1)
|
rspec-sidekiq (3.0.3)
|
||||||
rspec-core (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.6.0)
|
rspec-support (3.6.0)
|
||||||
|
@ -395,10 +404,10 @@ GEM
|
||||||
nokogiri (>= 1.4.4)
|
nokogiri (>= 1.4.4)
|
||||||
nokogumbo (~> 1.4.1)
|
nokogumbo (~> 1.4.1)
|
||||||
sass (3.4.24)
|
sass (3.4.24)
|
||||||
scss_lint (0.53.0)
|
scss_lint (0.54.0)
|
||||||
rake (>= 0.9, < 13)
|
rake (>= 0.9, < 13)
|
||||||
sass (~> 3.4.20)
|
sass (~> 3.4.20)
|
||||||
sidekiq (5.0.2)
|
sidekiq (5.0.3)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
connection_pool (~> 2.2, >= 2.2.0)
|
connection_pool (~> 2.2, >= 2.2.0)
|
||||||
rack-protection (>= 1.5.0)
|
rack-protection (>= 1.5.0)
|
||||||
|
@ -406,7 +415,7 @@ GEM
|
||||||
sidekiq-bulk (0.1.1)
|
sidekiq-bulk (0.1.1)
|
||||||
activesupport
|
activesupport
|
||||||
sidekiq
|
sidekiq
|
||||||
sidekiq-scheduler (2.1.5)
|
sidekiq-scheduler (2.1.7)
|
||||||
redis (~> 3)
|
redis (~> 3)
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
|
@ -443,7 +452,7 @@ GEM
|
||||||
thread (0.2.2)
|
thread (0.2.2)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.7)
|
tilt (2.0.7)
|
||||||
twitter-text (1.14.5)
|
twitter-text (1.14.6)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.3)
|
tzinfo (1.2.3)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
|
@ -454,7 +463,7 @@ GEM
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.4)
|
unf_ext (0.0.7.4)
|
||||||
unicode-display_width (1.2.1)
|
unicode-display_width (1.3.0)
|
||||||
uniform_notifier (1.10.0)
|
uniform_notifier (1.10.0)
|
||||||
warden (1.2.7)
|
warden (1.2.7)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
|
@ -476,6 +485,7 @@ PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
active_model_serializers (~> 0.10)
|
||||||
active_record_query_trace (~> 1.5)
|
active_record_query_trace (~> 1.5)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
annotate (~> 2.7)
|
annotate (~> 2.7)
|
||||||
|
@ -492,6 +502,7 @@ DEPENDENCIES
|
||||||
capistrano-rbenv (~> 2.1)
|
capistrano-rbenv (~> 2.1)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 2.14)
|
capybara (~> 2.14)
|
||||||
|
charlock_holmes (~> 0.7.3)
|
||||||
cld3 (~> 3.1)
|
cld3 (~> 3.1)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
devise (~> 4.2)
|
devise (~> 4.2)
|
||||||
|
@ -516,6 +527,7 @@ DEPENDENCIES
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.5)
|
lograge (~> 0.5)
|
||||||
microformats2 (~> 3.0)
|
microformats2 (~> 3.0)
|
||||||
|
mime-types (~> 3.1)
|
||||||
nokogiri (~> 1.7)
|
nokogiri (~> 1.7)
|
||||||
oj (~> 3.0)
|
oj (~> 3.0)
|
||||||
ostatus2 (~> 2.0)
|
ostatus2 (~> 2.0)
|
||||||
|
|
1
Vagrantfile
vendored
1
Vagrantfile
vendored
|
@ -37,6 +37,7 @@ sudo apt-get install \
|
||||||
yarn \
|
yarn \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libreadline-dev \
|
libreadline-dev \
|
||||||
|
libicu-dev \
|
||||||
-y
|
-y
|
||||||
|
|
||||||
# Install rvm
|
# Install rvm
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
class AboutController < ApplicationController
|
class AboutController < ApplicationController
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_instance_presenter, only: [:show, :more]
|
before_action :set_instance_presenter, only: [:show, :more, :terms]
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
||||||
|
@initial_state_json = serializable_resource.to_json
|
||||||
|
end
|
||||||
|
|
||||||
def more; end
|
def more; end
|
||||||
|
|
||||||
|
@ -15,6 +18,7 @@ class AboutController < ApplicationController
|
||||||
def new_user
|
def new_user
|
||||||
User.new.tap(&:build_account)
|
User.new.tap(&:build_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
helper_method :new_user
|
helper_method :new_user
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
|
@ -24,4 +28,11 @@ class AboutController < ApplicationController
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'about-body'
|
@body_classes = 'about-body'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def initial_state_params
|
||||||
|
{
|
||||||
|
settings: {},
|
||||||
|
token: current_session&.token,
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,8 +22,8 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def redownload
|
def redownload
|
||||||
@account.avatar = @account.avatar_remote_url
|
@account.reset_avatar!
|
||||||
@account.header = @account.header_remote_url
|
@account.reset_header!
|
||||||
@account.save!
|
@account.save!
|
||||||
|
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
|
|
|
@ -8,13 +8,21 @@ module Admin
|
||||||
site_title
|
site_title
|
||||||
site_description
|
site_description
|
||||||
site_extended_description
|
site_extended_description
|
||||||
|
site_terms
|
||||||
open_registrations
|
open_registrations
|
||||||
closed_registrations_message
|
closed_registrations_message
|
||||||
|
open_deletion
|
||||||
|
timeline_preview
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
BOOLEAN_SETTINGS = %w(
|
||||||
|
open_registrations
|
||||||
|
open_deletion
|
||||||
|
timeline_preview
|
||||||
).freeze
|
).freeze
|
||||||
BOOLEAN_SETTINGS = %w(open_registrations).freeze
|
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
@settings = Setting.all_as_records
|
@admin_settings = Form::AdminSettings.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -23,19 +31,19 @@ module Admin
|
||||||
setting.update(value: value_for_update(key, value))
|
setting.update(value: value_for_update(key, value))
|
||||||
end
|
end
|
||||||
|
|
||||||
flash[:notice] = 'Success!'
|
flash[:notice] = I18n.t('generic.changes_saved_msg')
|
||||||
redirect_to edit_admin_settings_path
|
redirect_to edit_admin_settings_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def settings_params
|
def settings_params
|
||||||
params.permit(ADMIN_SETTINGS)
|
params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def value_for_update(key, value)
|
def value_for_update(key, value)
|
||||||
if BOOLEAN_SETTINGS.include?(key)
|
if BOOLEAN_SETTINGS.include?(key)
|
||||||
value == 'true'
|
value == '1'
|
||||||
else
|
else
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,8 +5,7 @@ class Api::OEmbedController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@stream_entry = find_stream_entry.stream_entry
|
@stream_entry = find_stream_entry.stream_entry
|
||||||
@width = maxwidth_or_default
|
render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
|
||||||
@height = maxheight_or_default
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -6,13 +6,13 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@account = current_account
|
@account = current_account
|
||||||
render 'api/v1/accounts/show'
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
current_account.update!(account_params)
|
current_account.update!(account_params)
|
||||||
@account = current_account
|
@account = current_account
|
||||||
render 'api/v1/accounts/show'
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
render 'api/v1/accounts/index'
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
render 'api/v1/accounts/index'
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -8,16 +8,15 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = Account.where(id: account_ids).select('id')
|
@accounts = Account.where(id: account_ids).select('id')
|
||||||
@following = Account.following_map(account_ids, current_user.account_id)
|
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
@followed_by = Account.followed_by_map(account_ids, current_user.account_id)
|
|
||||||
@blocking = Account.blocking_map(account_ids, current_user.account_id)
|
|
||||||
@muting = Account.muting_map(account_ids, current_user.account_id)
|
|
||||||
@requested = Account.requested_map(account_ids, current_user.account_id)
|
|
||||||
@domain_blocking = Account.domain_blocking_map(account_ids, current_user.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def relationships
|
||||||
|
AccountRelationshipsPresenter.new(@accounts, current_user.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
def account_ids
|
def account_ids
|
||||||
@_account_ids ||= Array(params[:id]).map(&:to_i)
|
@_account_ids ||= Array(params[:id]).map(&:to_i)
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,8 +8,7 @@ class Api::V1::Accounts::SearchController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@accounts = account_search
|
@accounts = account_search
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
render 'api/v1/accounts/index'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -18,9 +19,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_account_statuses.tap do |statuses|
|
cached_account_statuses
|
||||||
set_maps(statuses)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_account_statuses
|
def cached_account_statuses
|
||||||
|
|
|
@ -8,49 +8,38 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
render json: @account, serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account.acct)
|
FollowService.new.call(current_user.account, @account.acct)
|
||||||
set_relationship
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
render :relationship
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def block
|
def block
|
||||||
BlockService.new.call(current_user.account, @account)
|
BlockService.new.call(current_user.account, @account)
|
||||||
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
@following = { @account.id => false }
|
|
||||||
@followed_by = { @account.id => false }
|
|
||||||
@blocking = { @account.id => true }
|
|
||||||
@requested = { @account.id => false }
|
|
||||||
@muting = { @account.id => current_account.muting?(@account.id) }
|
|
||||||
@domain_blocking = { @account.id => current_account.domain_blocking?(@account.domain) }
|
|
||||||
|
|
||||||
render :relationship
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def mute
|
def mute
|
||||||
MuteService.new.call(current_user.account, @account)
|
MuteService.new.call(current_user.account, @account)
|
||||||
set_relationship
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
render :relationship
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def unfollow
|
def unfollow
|
||||||
UnfollowService.new.call(current_user.account, @account)
|
UnfollowService.new.call(current_user.account, @account)
|
||||||
set_relationship
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
render :relationship
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def unblock
|
def unblock
|
||||||
UnblockService.new.call(current_user.account, @account)
|
UnblockService.new.call(current_user.account, @account)
|
||||||
set_relationship
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
render :relationship
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def unmute
|
def unmute
|
||||||
UnmuteService.new.call(current_user.account, @account)
|
UnmuteService.new.call(current_user.account, @account)
|
||||||
set_relationship
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
render :relationship
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -59,12 +48,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
@account = Account.find(params[:id])
|
@account = Account.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_relationship
|
def relationships
|
||||||
@following = Account.following_map([@account.id], current_user.account_id)
|
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||||
@followed_by = Account.followed_by_map([@account.id], current_user.account_id)
|
|
||||||
@blocking = Account.blocking_map([@account.id], current_user.account_id)
|
|
||||||
@muting = Account.muting_map([@account.id], current_user.account_id)
|
|
||||||
@requested = Account.requested_map([@account.id], current_user.account_id)
|
|
||||||
@domain_blocking = Account.domain_blocking_map([@account.id], current_user.account_id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::AppsController < Api::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@app = Doorkeeper::Application.create!(application_options)
|
@app = Doorkeeper::Application.create!(application_options)
|
||||||
|
render json: @app, serializer: REST::ApplicationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Api::V1::BlocksController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,14 +9,13 @@ class Api::V1::FavouritesController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_favourites.tap do |statuses|
|
cached_favourites
|
||||||
set_maps(statuses)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_favourites
|
def cached_favourites
|
||||||
|
|
|
@ -7,6 +7,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorize
|
def authorize
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::FollowsController < Api::BaseController
|
||||||
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
|
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
|
||||||
|
|
||||||
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
|
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
|
||||||
render :show
|
render json: @account, serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -3,5 +3,7 @@
|
||||||
class Api::V1::InstancesController < Api::BaseController
|
class Api::V1::InstancesController < Api::BaseController
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
render json: {}, serializer: REST::InstanceSerializer
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,7 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@media = current_account.media_attachments.create!(file: media_params[:file])
|
@media = current_account.media_attachments.create!(file: media_params[:file])
|
||||||
|
render json: @media, serializer: REST::MediaAttachmentSerializer
|
||||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
||||||
render json: file_type_error, status: 422
|
render json: file_type_error, status: 422
|
||||||
rescue Paperclip::Error
|
rescue Paperclip::Error
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Api::V1::MutesController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -11,11 +11,12 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@notifications = load_notifications
|
@notifications = load_notifications
|
||||||
set_maps_for_notification_target_statuses
|
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_account.notifications.find(params[:id])
|
@notification = current_account.notifications.find(params[:id])
|
||||||
|
render json: @notification, serializer: REST::NotificationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear
|
def clear
|
||||||
|
@ -46,10 +47,6 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
current_account.notifications.browserable(exclude_types)
|
current_account.notifications.browserable(exclude_types)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_maps_for_notification_target_statuses
|
|
||||||
set_maps target_statuses_from_notifications
|
|
||||||
end
|
|
||||||
|
|
||||||
def target_statuses_from_notifications
|
def target_statuses_from_notifications
|
||||||
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
|
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Api::V1::ReportsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@reports = current_account.reports
|
@reports = current_account.reports
|
||||||
|
render json: @reports, each_serializer: REST::ReportSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -20,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController
|
||||||
|
|
||||||
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
|
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
|
||||||
|
|
||||||
render :show
|
render json: @report, serializer: REST::ReportSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -3,10 +3,14 @@
|
||||||
class Api::V1::SearchController < Api::BaseController
|
class Api::V1::SearchController < Api::BaseController
|
||||||
RESULTS_LIMIT = 5
|
RESULTS_LIMIT = 5
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :read }
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@search = OpenStruct.new(search_results)
|
@search = Search.new(search_results)
|
||||||
|
render json: @search, serializer: REST::SearchSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
render 'api/v1/statuses/accounts'
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@status = favourited_status
|
@status = favourited_status
|
||||||
render 'api/v1/statuses/show'
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
|
||||||
|
|
||||||
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
|
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
|
||||||
|
|
||||||
render 'api/v1/statuses/show'
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -14,14 +14,14 @@ class Api::V1::Statuses::MutesController < Api::BaseController
|
||||||
current_account.mute_conversation!(@conversation)
|
current_account.mute_conversation!(@conversation)
|
||||||
@mutes_map = { @conversation.id => true }
|
@mutes_map = { @conversation.id => true }
|
||||||
|
|
||||||
render 'api/v1/statuses/show'
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
current_account.unmute_conversation!(@conversation)
|
current_account.unmute_conversation!(@conversation)
|
||||||
@mutes_map = { @conversation.id => false }
|
@mutes_map = { @conversation.id => false }
|
||||||
|
|
||||||
render 'api/v1/statuses/show'
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@accounts = load_accounts
|
||||||
render 'api/v1/statuses/accounts'
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@status = ReblogService.new.call(current_user.account, status_for_reblog)
|
@status = ReblogService.new.call(current_user.account, status_for_reblog)
|
||||||
render 'api/v1/statuses/show'
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
||||||
authorize status_for_destroy, :unreblog?
|
authorize status_for_destroy, :unreblog?
|
||||||
RemovalWorker.perform_async(status_for_destroy.id)
|
RemovalWorker.perform_async(status_for_destroy.id)
|
||||||
|
|
||||||
render 'api/v1/statuses/show'
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -13,6 +13,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
def show
|
def show
|
||||||
cached = Rails.cache.read(@status.cache_key)
|
cached = Rails.cache.read(@status.cache_key)
|
||||||
@status = cached unless cached.nil?
|
@status = cached unless cached.nil?
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def context
|
def context
|
||||||
|
@ -21,15 +22,20 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
loaded_ancestors = cache_collection(ancestors_results, Status)
|
loaded_ancestors = cache_collection(ancestors_results, Status)
|
||||||
loaded_descendants = cache_collection(descendants_results, Status)
|
loaded_descendants = cache_collection(descendants_results, Status)
|
||||||
|
|
||||||
@context = OpenStruct.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||||
statuses = [@status] + @context[:ancestors] + @context[:descendants]
|
statuses = [@status] + @context.ancestors + @context.descendants
|
||||||
|
|
||||||
set_maps(statuses)
|
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def card
|
def card
|
||||||
@card = PreviewCard.find_by(status: @status)
|
@card = PreviewCard.find_by(status: @status)
|
||||||
render_empty if @card.nil?
|
|
||||||
|
if @card.nil?
|
||||||
|
render_empty
|
||||||
|
else
|
||||||
|
render json: @card, serializer: REST::PreviewCardSerializer
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -43,7 +49,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
application: doorkeeper_token.application,
|
application: doorkeeper_token.application,
|
||||||
idempotency: request.headers['Idempotency-Key'])
|
idempotency: request.headers['Idempotency-Key'])
|
||||||
|
|
||||||
render :show
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|
|
@ -9,15 +9,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
render 'api/v1/timelines/show'
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_home_statuses.tap do |statuses|
|
cached_home_statuses
|
||||||
set_maps(statuses)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_home_statuses
|
def cached_home_statuses
|
||||||
|
|
|
@ -7,15 +7,13 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
render 'api/v1/timelines/show'
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_public_statuses.tap do |statuses|
|
cached_public_statuses
|
||||||
set_maps(statuses)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_public_statuses
|
def cached_public_statuses
|
||||||
|
|
|
@ -8,7 +8,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
render 'api/v1/timelines/show'
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -18,9 +18,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_tagged_statuses.tap do |statuses|
|
cached_tagged_statuses
|
||||||
set_maps(statuses)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_tagged_statuses
|
def cached_tagged_statuses
|
||||||
|
|
|
@ -70,7 +70,7 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_session
|
def current_session
|
||||||
@current_session ||= SessionActivation.find_by(session_id: session['auth_id'])
|
@current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_collection(raw, klass)
|
def cache_collection(raw, klass)
|
||||||
|
|
|
@ -15,7 +15,7 @@ class AuthorizeFollowsController < ApplicationController
|
||||||
if @account.nil?
|
if @account.nil?
|
||||||
render :error
|
render :error
|
||||||
else
|
else
|
||||||
redirect_to web_url("accounts/#{@account.id}")
|
render :success
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
render :error
|
render :error
|
||||||
|
|
|
@ -2,13 +2,10 @@
|
||||||
|
|
||||||
class HomeController < ApplicationController
|
class HomeController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :set_initial_state_json
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
@token = current_session.token
|
|
||||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
|
||||||
@admin = Account.find_local(Setting.site_contact_username)
|
|
||||||
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -16,4 +13,18 @@ class HomeController < ApplicationController
|
||||||
def authenticate_user!
|
def authenticate_user!
|
||||||
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_initial_state_json
|
||||||
|
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
||||||
|
@initial_state_json = serializable_resource.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def initial_state_params
|
||||||
|
{
|
||||||
|
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
||||||
|
current_account: current_account,
|
||||||
|
token: current_session.token,
|
||||||
|
admin: Account.find_local(Setting.site_contact_username),
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,9 +34,11 @@ class Settings::PreferencesController < ApplicationController
|
||||||
def user_settings_params
|
def user_settings_params
|
||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
:setting_default_privacy,
|
:setting_default_privacy,
|
||||||
|
:setting_default_sensitive,
|
||||||
:setting_boost_modal,
|
:setting_boost_modal,
|
||||||
:setting_delete_modal,
|
:setting_delete_modal,
|
||||||
:setting_auto_play_gif,
|
:setting_auto_play_gif,
|
||||||
|
:setting_system_font_ui,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest),
|
notification_emails: %i(follow follow_request reblog favourite mention digest),
|
||||||
interactions: %i(must_be_follower must_be_following)
|
interactions: %i(must_be_follower must_be_following)
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,15 +6,21 @@ module Admin::FilterHelper
|
||||||
|
|
||||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
|
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
|
||||||
|
|
||||||
def filter_link_to(text, more_params)
|
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||||
new_url = filtered_url_for(more_params)
|
new_url = filtered_url_for(link_to_params)
|
||||||
link_to text, new_url, class: filter_link_class(new_url)
|
new_class = filtered_url_for(link_class_params)
|
||||||
|
link_to text, new_url, class: filter_link_class(new_class)
|
||||||
end
|
end
|
||||||
|
|
||||||
def table_link_to(icon, text, path, options = {})
|
def table_link_to(icon, text, path, options = {})
|
||||||
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
|
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def selected?(more_params)
|
||||||
|
new_url = filtered_url_for(more_params)
|
||||||
|
filter_link_class(new_url) == 'selected' ? true : false
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_params(more_params)
|
def filter_params(more_params)
|
||||||
|
|
|
@ -31,7 +31,11 @@ module ApplicationHelper
|
||||||
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
||||||
end
|
end
|
||||||
|
|
||||||
def fa_icon(icon)
|
def fa_icon(icon, attributes = {})
|
||||||
content_tag(:i, nil, class: 'fa ' + icon.split(' ').map { |cl| "fa-#{cl}" }.join(' '))
|
class_names = attributes[:class]&.split(' ') || []
|
||||||
|
class_names << 'fa'
|
||||||
|
class_names += icon.split(' ').map { |cl| "fa-#{cl}" }
|
||||||
|
|
||||||
|
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,6 +19,7 @@ module SettingsHelper
|
||||||
io: 'Ido',
|
io: 'Ido',
|
||||||
it: 'Italiano',
|
it: 'Italiano',
|
||||||
ja: '日本語',
|
ja: '日本語',
|
||||||
|
ko: '한국어',
|
||||||
nl: 'Nederlands',
|
nl: 'Nederlands',
|
||||||
no: 'Norsk',
|
no: 'Norsk',
|
||||||
oc: 'Occitan',
|
oc: 'Occitan',
|
||||||
|
|
BIN
app/javascript/fonts/montserrat/Montserrat-Medium.ttf
Normal file
BIN
app/javascript/fonts/montserrat/Montserrat-Medium.ttf
Normal file
Binary file not shown.
BIN
app/javascript/images/cloud2.png
Normal file
BIN
app/javascript/images/cloud2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
BIN
app/javascript/images/cloud3.png
Normal file
BIN
app/javascript/images/cloud3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
BIN
app/javascript/images/cloud4.png
Normal file
BIN
app/javascript/images/cloud4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
app/javascript/images/elephant-fren.png
Normal file
BIN
app/javascript/images/elephant-fren.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#fff"/></svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
25
app/javascript/mastodon/actions/bundles.js
Normal file
25
app/javascript/mastodon/actions/bundles.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
|
||||||
|
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
|
||||||
|
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
|
||||||
|
|
||||||
|
export function fetchBundleRequest(skipLoading) {
|
||||||
|
return {
|
||||||
|
type: BUNDLE_FETCH_REQUEST,
|
||||||
|
skipLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBundleSuccess(skipLoading) {
|
||||||
|
return {
|
||||||
|
type: BUNDLE_FETCH_SUCCESS,
|
||||||
|
skipLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBundleFail(error, skipLoading) {
|
||||||
|
return {
|
||||||
|
type: BUNDLE_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import Immutable from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import IntlMessageFormat from 'intl-messageformat';
|
import IntlMessageFormat from 'intl-messageformat';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
@ -124,7 +124,7 @@ export function refreshNotificationsFail(error, skipLoading) {
|
||||||
|
|
||||||
export function expandNotifications() {
|
export function expandNotifications() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const items = getState().getIn(['notifications', 'items'], Immutable.List());
|
const items = getState().getIn(['notifications', 'items'], ImmutableList());
|
||||||
|
|
||||||
if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) {
|
if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import Immutable from 'immutable';
|
import { Iterable, fromJS } from 'immutable';
|
||||||
|
|
||||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||||
|
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||||
|
|
||||||
const convertState = rawState =>
|
const convertState = rawState =>
|
||||||
Immutable.fromJS(rawState, (k, v) =>
|
fromJS(rawState, (k, v) =>
|
||||||
Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
|
Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
|
||||||
Number.isNaN(x * 1) ? x : x * 1));
|
Number.isNaN(x * 1) ? x : x * 1));
|
||||||
|
|
||||||
export function hydrateStore(rawState) {
|
export function hydrateStore(rawState) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import Immutable from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||||
|
@ -66,13 +66,13 @@ export function refreshTimelineRequest(timeline, skipLoading) {
|
||||||
|
|
||||||
export function refreshTimeline(timelineId, path, params = {}) {
|
export function refreshTimeline(timelineId, path, params = {}) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
|
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||||
|
|
||||||
if (timeline.get('isLoading') || timeline.get('online')) {
|
if (timeline.get('isLoading') || timeline.get('online')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = timeline.get('items', Immutable.List());
|
const ids = timeline.get('items', ImmutableList());
|
||||||
const newestId = ids.size > 0 ? ids.first() : null;
|
const newestId = ids.size > 0 ? ids.first() : null;
|
||||||
|
|
||||||
let skipLoading = timeline.get('loaded');
|
let skipLoading = timeline.get('loaded');
|
||||||
|
@ -111,8 +111,8 @@ export function refreshTimelineFail(timeline, error, skipLoading) {
|
||||||
|
|
||||||
export function expandTimeline(timelineId, path, params = {}) {
|
export function expandTimeline(timelineId, path, params = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
|
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||||
const ids = timeline.get('items', Immutable.List());
|
const ids = timeline.get('items', ImmutableList());
|
||||||
|
|
||||||
if (timeline.get('isLoading') || ids.size === 0) {
|
if (timeline.get('isLoading') || ids.size === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.node.isRequired,
|
||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent {
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
direction: PropTypes.string,
|
direction: PropTypes.string,
|
||||||
ariaLabel: PropTypes.string,
|
ariaLabel: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { icon, items, size, direction, ariaLabel } = this.props;
|
const { icon, items, size, direction, ariaLabel, disabled } = this.props;
|
||||||
const { expanded } = this.state;
|
const { expanded } = this.state;
|
||||||
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
|
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
|
||||||
|
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
|
||||||
|
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return (
|
||||||
|
<div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}>
|
||||||
|
<i className={iconClassname} aria-hidden />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const dropdownItems = expanded && (
|
const dropdownItems = expanded && (
|
||||||
<ul className='dropdown__content-list'>
|
<ul className='dropdown__content-list'>
|
||||||
|
@ -80,8 +91,8 @@ export default class DropdownMenu extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
|
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
|
||||||
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
|
<DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}>
|
||||||
<i className={`fa fa-fw fa-${icon} dropdown__icon`} aria-hidden />
|
<i className={iconClassname} aria-hidden />
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
|
|
||||||
<DropdownContent className={directionClass}>
|
<DropdownContent className={directionClass}>
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default class Permalink extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.context.router.history.push(this.props.to);
|
this.context.router.history.push(this.props.to);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
|
||||||
const { href, children, className, ...other } = this.props;
|
const { href, children, className, ...other } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,14 +22,15 @@ import { getLocale } from '../locales';
|
||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
|
||||||
const store = configureStore();
|
export const store = configureStore();
|
||||||
const initialState = JSON.parse(document.getElementById('initial-state').textContent);
|
const initialState = JSON.parse(document.getElementById('initial-state').textContent);
|
||||||
try {
|
try {
|
||||||
initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
|
initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
initialState.local_settings = {};
|
initialState.local_settings = {};
|
||||||
}
|
}
|
||||||
store.dispatch(hydrateStore(initialState));
|
const hydrateAction = hydrateStore(initialState);
|
||||||
|
store.dispatch(hydrateAction);
|
||||||
|
|
||||||
export default class Mastodon extends React.PureComponent {
|
export default class Mastodon extends React.PureComponent {
|
||||||
|
|
||||||
|
|
39
app/javascript/mastodon/containers/timeline_container.js
Normal file
39
app/javascript/mastodon/containers/timeline_container.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import configureStore from '../store/configureStore';
|
||||||
|
import { hydrateStore } from '../actions/store';
|
||||||
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
|
import { getLocale } from '../locales';
|
||||||
|
import PublicTimeline from '../features/standalone/public_timeline';
|
||||||
|
|
||||||
|
const { localeData, messages } = getLocale();
|
||||||
|
addLocaleData(localeData);
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
const initialStateContainer = document.getElementById('initial-state');
|
||||||
|
|
||||||
|
if (initialStateContainer !== null) {
|
||||||
|
const initialState = JSON.parse(initialStateContainer.textContent);
|
||||||
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TimelineContainer extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
locale: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { locale } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<PublicTimeline />
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,35 +1,55 @@
|
||||||
import emojione from 'emojione';
|
import emojione from 'emojione';
|
||||||
|
import Trie from 'substring-trie';
|
||||||
|
|
||||||
const toImage = str => shortnameToImage(unicodeToImage(str));
|
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||||
|
const trie = new Trie(Object.keys(emojione.jsEscapeMap));
|
||||||
|
|
||||||
const unicodeToImage = str => {
|
function emojify(str) {
|
||||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
|
||||||
|
// and replacing valid shortnames like :smile: and :wink: as well as unicode strings
|
||||||
return str.replace(emojione.regUnicode, unicodeChar => {
|
// that _aren't_ within tags with an <img> version.
|
||||||
if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) {
|
// The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster.
|
||||||
return unicodeChar;
|
let i = -1;
|
||||||
|
let insideTag = false;
|
||||||
|
let insideShortname = false;
|
||||||
|
let shortnameStartIndex = -1;
|
||||||
|
let match;
|
||||||
|
while (++i < str.length) {
|
||||||
|
const char = str.charAt(i);
|
||||||
|
if (insideShortname && char === ':') {
|
||||||
|
const shortname = str.substring(shortnameStartIndex, i + 1);
|
||||||
|
if (shortname in emojione.emojioneList) {
|
||||||
|
const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
|
||||||
|
const alt = emojione.convert(unicode.toUpperCase());
|
||||||
|
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
|
||||||
|
str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
|
||||||
|
i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
|
||||||
|
} else {
|
||||||
|
i--; // stray colon, try again
|
||||||
|
}
|
||||||
|
insideShortname = false;
|
||||||
|
} else if (insideTag && char === '>') {
|
||||||
|
insideTag = false;
|
||||||
|
} else if (char === '<') {
|
||||||
|
insideTag = true;
|
||||||
|
insideShortname = false;
|
||||||
|
} else if (!insideTag && char === ':') {
|
||||||
|
insideShortname = true;
|
||||||
|
shortnameStartIndex = i;
|
||||||
|
} else if (!insideTag && (match = trie.search(str.substring(i)))) {
|
||||||
|
const unicodeStr = match;
|
||||||
|
if (unicodeStr in emojione.jsEscapeMap) {
|
||||||
|
const unicode = emojione.jsEscapeMap[unicodeStr];
|
||||||
|
const short = mappedUnicode[unicode];
|
||||||
|
const filename = emojione.emojioneList[short].fname;
|
||||||
|
const alt = emojione.convert(unicode.toUpperCase());
|
||||||
|
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`;
|
||||||
|
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
|
||||||
|
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unicode = emojione.jsEscapeMap[unicodeChar];
|
|
||||||
const short = mappedUnicode[unicode];
|
|
||||||
const filename = emojione.emojioneList[short].fname;
|
|
||||||
const alt = emojione.convert(unicode.toUpperCase());
|
|
||||||
|
|
||||||
return `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => {
|
|
||||||
if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) {
|
|
||||||
return shortname;
|
|
||||||
}
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
|
export default emojify;
|
||||||
const alt = emojione.convert(unicode.toUpperCase());
|
|
||||||
|
|
||||||
return `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function emojify(text) {
|
|
||||||
return toImage(text);
|
|
||||||
};
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import HeaderContainer from './containers/header_container';
|
import HeaderContainer from './containers/header_container';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import ColumnBackButton from '../../components/column_back_button';
|
||||||
import Immutable from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], Immutable.List()),
|
statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], ImmutableList()),
|
||||||
isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']),
|
||||||
hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']),
|
hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']),
|
||||||
me: state.getIn(['meta', 'me']),
|
me: state.getIn(['meta', 'me']),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
@ -50,7 +51,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
|
||||||
this.setState({ active: true });
|
this.setState({ active: true });
|
||||||
if (!EmojiPicker) {
|
if (!EmojiPicker) {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => {
|
EmojiPickerAsync().then(TheEmojiPicker => {
|
||||||
EmojiPicker = TheEmojiPicker.default;
|
EmojiPicker = TheEmojiPicker.default;
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import ComposeFormContainer from './containers/compose_form_container';
|
import ComposeFormContainer from './containers/compose_form_container';
|
||||||
import NavigationContainer from './containers/navigation_container';
|
import NavigationContainer from './containers/navigation_container';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mountCompose, unmountCompose } from '../../actions/compose';
|
import { mountCompose, unmountCompose } from '../../actions/compose';
|
||||||
import { openModal } from '../../actions/modal';
|
import { openModal } from '../../actions/modal';
|
||||||
|
@ -15,6 +16,8 @@ import SearchResultsContainer from './containers/search_results_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
|
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||||
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||||
|
@ -22,6 +25,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
columns: state.getIn(['settings', 'columns']),
|
||||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,6 +35,7 @@ export default class Compose extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
columns: ImmutablePropTypes.list.isRequired,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
@ -60,11 +65,22 @@ export default class Compose extends React.PureComponent {
|
||||||
let header = '';
|
let header = '';
|
||||||
|
|
||||||
if (multiColumn) {
|
if (multiColumn) {
|
||||||
|
const { columns } = this.props;
|
||||||
header = (
|
header = (
|
||||||
<div className='drawer__header'>
|
<div className='drawer__header'>
|
||||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
|
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
|
||||||
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
|
{!columns.some(column => column.get('id') === 'HOME') && (
|
||||||
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
|
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' aria-label={intl.formatMessage(messages.home_timeline)} /></Link>
|
||||||
|
)}
|
||||||
|
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
|
||||||
|
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' aria-label={intl.formatMessage(messages.notifications)} /></Link>
|
||||||
|
)}
|
||||||
|
{!columns.some(column => column.get('id') === 'COMMUNITY') && (
|
||||||
|
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
|
||||||
|
)}
|
||||||
|
{!columns.some(column => column.get('id') === 'PUBLIC') && (
|
||||||
|
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
|
||||||
|
)}
|
||||||
<a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a>
|
<a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a>
|
||||||
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
|
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { ScrollContainer } from 'react-router-scroll';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import Immutable from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import LoadMore from '../../components/load_more';
|
import LoadMore from '../../components/load_more';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const getNotifications = createSelector([
|
const getNotifications = createSelector([
|
||||||
state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
|
state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
|
||||||
state => state.getIn(['notifications', 'items']),
|
state => state.getIn(['notifications', 'items']),
|
||||||
], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
|
], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ export default class Notifications extends React.PureComponent {
|
||||||
let unread = '';
|
let unread = '';
|
||||||
let scrollContainer = '';
|
let scrollContainer = '';
|
||||||
|
|
||||||
if (!isLoading && notifications.size > 0 && hasMore) {
|
if (!isLoading && hasMore) {
|
||||||
loadMore = <LoadMore onClick={this.handleLoadMore} />;
|
loadMore = <LoadMore onClick={this.handleLoadMore} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ export default class Notifications extends React.PureComponent {
|
||||||
|
|
||||||
if (isLoading && this.scrollableArea) {
|
if (isLoading && this.scrollableArea) {
|
||||||
scrollableArea = this.scrollableArea;
|
scrollableArea = this.scrollableArea;
|
||||||
} else if (notifications.size > 0) {
|
} else if (notifications.size > 0 || hasMore) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
|
<div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
|
||||||
{unread}
|
{unread}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import StatusCheckBox from '../components/status_check_box';
|
import StatusCheckBox from '../components/status_check_box';
|
||||||
import { toggleStatusReport } from '../../../actions/reports';
|
import { toggleStatusReport } from '../../../actions/reports';
|
||||||
import Immutable from 'immutable';
|
import { Set as ImmutableSet } from 'immutable';
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
status: state.getIn(['statuses', id]),
|
status: state.getIn(['statuses', id]),
|
||||||
checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id),
|
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||||
|
import {
|
||||||
|
refreshPublicTimeline,
|
||||||
|
expandPublicTimeline,
|
||||||
|
} from '../../../actions/timelines';
|
||||||
|
import Column from '../../../components/column';
|
||||||
|
import ColumnHeader from '../../../components/column_header';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
export default class PublicTimeline extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch(refreshPublicTimeline());
|
||||||
|
|
||||||
|
this.polling = setInterval(() => {
|
||||||
|
dispatch(refreshPublicTimeline());
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (typeof this.polling !== 'undefined') {
|
||||||
|
clearInterval(this.polling);
|
||||||
|
this.polling = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
this.props.dispatch(expandPublicTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column ref={this.setRef}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='globe'
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
timelineId='public'
|
||||||
|
loadMore={this.handleLoadMore}
|
||||||
|
scrollKey='standalone_public_timeline'
|
||||||
|
trackScroll={false}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
101
app/javascript/mastodon/features/ui/components/bundle.js
Normal file
101
app/javascript/mastodon/features/ui/components/bundle.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const emptyComponent = () => null;
|
||||||
|
const noop = () => { };
|
||||||
|
|
||||||
|
class Bundle extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
fetchComponent: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.func,
|
||||||
|
error: PropTypes.func,
|
||||||
|
children: PropTypes.func.isRequired,
|
||||||
|
renderDelay: PropTypes.number,
|
||||||
|
onFetch: PropTypes.func,
|
||||||
|
onFetchSuccess: PropTypes.func,
|
||||||
|
onFetchFail: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
loading: emptyComponent,
|
||||||
|
error: emptyComponent,
|
||||||
|
renderDelay: 0,
|
||||||
|
onFetch: noop,
|
||||||
|
onFetchSuccess: noop,
|
||||||
|
onFetchFail: noop,
|
||||||
|
}
|
||||||
|
|
||||||
|
static cache = {}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
mod: undefined,
|
||||||
|
forceRender: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.load(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.fetchComponent !== this.props.fetchComponent) {
|
||||||
|
this.load(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load = (props) => {
|
||||||
|
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
|
||||||
|
|
||||||
|
this.setState({ mod: undefined });
|
||||||
|
onFetch();
|
||||||
|
|
||||||
|
if (renderDelay !== 0) {
|
||||||
|
this.timestamp = new Date();
|
||||||
|
this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Bundle.cache[fetchComponent.name]) {
|
||||||
|
const mod = Bundle.cache[fetchComponent.name];
|
||||||
|
|
||||||
|
this.setState({ mod: mod.default });
|
||||||
|
onFetchSuccess();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchComponent()
|
||||||
|
.then((mod) => {
|
||||||
|
Bundle.cache[fetchComponent.name] = mod;
|
||||||
|
this.setState({ mod: mod.default });
|
||||||
|
onFetchSuccess();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.setState({ mod: null });
|
||||||
|
onFetchFail(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading: Loading, error: Error, children, renderDelay } = this.props;
|
||||||
|
const { mod, forceRender } = this.state;
|
||||||
|
const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay;
|
||||||
|
|
||||||
|
if (mod === undefined) {
|
||||||
|
return (elapsed >= renderDelay || forceRender) ? <Loading /> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mod === null) {
|
||||||
|
return <Error onRetry={this.load} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children(mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bundle;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import Column from './column';
|
||||||
|
import ColumnHeader from './column_header';
|
||||||
|
import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
|
||||||
|
import IconButton from '../../../components/icon_button';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
|
||||||
|
body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' },
|
||||||
|
retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
|
||||||
|
});
|
||||||
|
|
||||||
|
class BundleColumnError extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onRetry: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry = () => {
|
||||||
|
this.props.onRetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl: { formatMessage } } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} />
|
||||||
|
<ColumnBackButtonSlim />
|
||||||
|
<div className='error-column'>
|
||||||
|
<IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
|
||||||
|
{formatMessage(messages.body)}
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(BundleColumnError);
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import IconButton from '../../../components/icon_button';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
|
||||||
|
retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
|
||||||
|
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
|
||||||
|
});
|
||||||
|
|
||||||
|
class BundleModalError extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onRetry: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry = () => {
|
||||||
|
this.props.onRetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { onClose, intl: { formatMessage } } = this.props;
|
||||||
|
|
||||||
|
// Keep the markup in sync with <ModalLoading />
|
||||||
|
// (make sure they have the same dimensions)
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal error-modal'>
|
||||||
|
<div className='error-modal__body'>
|
||||||
|
<IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
|
||||||
|
{formatMessage(messages.error)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='error-modal__footer'>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className='error-modal__nav onboarding-modal__skip'
|
||||||
|
>
|
||||||
|
{formatMessage(messages.close)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(BundleModalError);
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Column from '../../../components/column';
|
||||||
|
import ColumnHeader from '../../../components/column_header';
|
||||||
|
|
||||||
|
const ColumnLoading = ({ title = '', icon = ' ' }) => (
|
||||||
|
<Column>
|
||||||
|
<ColumnHeader icon={icon} title={title} multiColumn={false} />
|
||||||
|
<div className='scrollable' />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnLoading.propTypes = {
|
||||||
|
title: PropTypes.node,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColumnLoading;
|
|
@ -2,14 +2,14 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import ReactSwipeable from 'react-swipeable';
|
|
||||||
import HomeTimeline from '../../home_timeline';
|
import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
import Notifications from '../../notifications';
|
import { links, getIndex, getLink } from './tabs_bar';
|
||||||
import PublicTimeline from '../../public_timeline';
|
|
||||||
import CommunityTimeline from '../../community_timeline';
|
import BundleContainer from '../containers/bundle_container';
|
||||||
import HashtagTimeline from '../../hashtag_timeline';
|
import ColumnLoading from './column_loading';
|
||||||
import Compose from '../../compose';
|
import BundleColumnError from './bundle_column_error';
|
||||||
import { getPreviousLink, getNextLink } from './tabs_bar';
|
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components';
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
'COMPOSE': Compose,
|
'COMPOSE': Compose,
|
||||||
|
@ -32,39 +32,61 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRightSwipe = () => {
|
handleSwipe = (index) => {
|
||||||
const previousLink = getPreviousLink(this.context.router.history.location.pathname);
|
window.requestAnimationFrame(() => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
if (previousLink) {
|
this.context.router.history.push(getLink(index));
|
||||||
this.context.router.history.push(previousLink);
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLeftSwipe = () => {
|
renderView = (link, index) => {
|
||||||
const previousLink = getNextLink(this.context.router.history.location.pathname);
|
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||||
|
const title = link.props.children[1] && React.cloneElement(link.props.children[1]);
|
||||||
|
const icon = (link.props.children[0] || link.props.children).props.className.split(' ')[2].split('-')[1];
|
||||||
|
|
||||||
if (previousLink) {
|
const view = (index === columnIndex) ?
|
||||||
this.context.router.history.push(previousLink);
|
React.cloneElement(this.props.children) :
|
||||||
}
|
<ColumnLoading title={title} icon={icon} />;
|
||||||
};
|
|
||||||
|
return (
|
||||||
|
<div className='columns-area' key={index}>
|
||||||
|
{view}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoading = () => {
|
||||||
|
return <ColumnLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError = (props) => {
|
||||||
|
return <BundleColumnError {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { columns, children, singleColumn } = this.props;
|
const { columns, children, singleColumn } = this.props;
|
||||||
|
|
||||||
|
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||||
|
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
return (
|
return columnIndex !== -1 ? (
|
||||||
<ReactSwipeable onSwipedLeft={this.handleLeftSwipe} onSwipedRight={this.handleRightSwipe} className='columns-area'>
|
<ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} animateTransitions={false} style={{ height: '100%' }}>
|
||||||
{children}
|
{links.map(this.renderView)}
|
||||||
</ReactSwipeable>
|
</ReactSwipeableViews>
|
||||||
);
|
) : <div className='columns-area'>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='columns-area'>
|
<div className='columns-area'>
|
||||||
{columns.map(column => {
|
{columns.map(column => {
|
||||||
const SpecificComponent = componentMap[column.get('id')];
|
|
||||||
const params = column.get('params', null) === null ? null : column.get('params').toJS();
|
const params = column.get('params', null) === null ? null : column.get('params').toJS();
|
||||||
return <SpecificComponent key={column.get('uuid')} columnId={column.get('uuid')} params={params} multiColumn />;
|
|
||||||
|
return (
|
||||||
|
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading} error={this.renderError}>
|
||||||
|
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />}
|
||||||
|
</BundleContainer>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
|
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
|
||||||
|
|
|
@ -8,12 +8,14 @@ export default class ImageLoader extends React.PureComponent {
|
||||||
alt: PropTypes.string,
|
alt: PropTypes.string,
|
||||||
src: PropTypes.string.isRequired,
|
src: PropTypes.string.isRequired,
|
||||||
previewSrc: PropTypes.string.isRequired,
|
previewSrc: PropTypes.string.isRequired,
|
||||||
width: PropTypes.number.isRequired,
|
width: PropTypes.number,
|
||||||
height: PropTypes.number.isRequired,
|
height: PropTypes.number,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
alt: '',
|
alt: '',
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -46,8 +48,8 @@ export default class ImageLoader extends React.PureComponent {
|
||||||
this.setState({ loading: true, error: false });
|
this.setState({ loading: true, error: false });
|
||||||
Promise.all([
|
Promise.all([
|
||||||
this.loadPreviewCanvas(props),
|
this.loadPreviewCanvas(props),
|
||||||
this.loadOriginalImage(props),
|
this.hasSize() && this.loadOriginalImage(props),
|
||||||
])
|
].filter(Boolean))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setState({ loading: false, error: false });
|
this.setState({ loading: false, error: false });
|
||||||
this.clearPreviewCanvas();
|
this.clearPreviewCanvas();
|
||||||
|
@ -106,6 +108,11 @@ export default class ImageLoader extends React.PureComponent {
|
||||||
this.removers = [];
|
this.removers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasSize () {
|
||||||
|
const { width, height } = this.props;
|
||||||
|
return typeof width === 'number' && typeof height === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
setCanvasRef = c => {
|
setCanvasRef = c => {
|
||||||
this.canvas = c;
|
this.canvas = c;
|
||||||
}
|
}
|
||||||
|
@ -116,6 +123,7 @@ export default class ImageLoader extends React.PureComponent {
|
||||||
|
|
||||||
const className = classNames('image-loader', {
|
const className = classNames('image-loader', {
|
||||||
'image-loader--loading': loading,
|
'image-loader--loading': loading,
|
||||||
|
'image-loader--amorphous': !this.hasSize(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -125,6 +133,7 @@ export default class ImageLoader extends React.PureComponent {
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
ref={this.setCanvasRef}
|
ref={this.setCanvasRef}
|
||||||
|
style={{ opacity: loading ? 1 : 0 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!loading && (
|
{!loading && (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactSwipeable from 'react-swipeable';
|
import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ExtendedVideoPlayer from '../../../components/extended_video_player';
|
import ExtendedVideoPlayer from '../../../components/extended_video_player';
|
||||||
|
@ -26,12 +26,16 @@ export default class MediaModal extends ImmutablePureComponent {
|
||||||
index: null,
|
index: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleSwipe = (index) => {
|
||||||
|
this.setState({ index: (index) % this.props.media.size });
|
||||||
|
}
|
||||||
|
|
||||||
handleNextClick = () => {
|
handleNextClick = () => {
|
||||||
this.setState({ index: (this.getIndex() + 1) % this.props.media.size });
|
this.setState({ index: (this.getIndex() + 1) % this.props.media.size });
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePrevClick = () => {
|
handlePrevClick = () => {
|
||||||
this.setState({ index: (this.getIndex() - 1) % this.props.media.size });
|
this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyUp = (e) => {
|
handleKeyUp = (e) => {
|
||||||
|
@ -74,7 +78,12 @@ export default class MediaModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.get('type') === 'image') {
|
if (attachment.get('type') === 'image') {
|
||||||
content = <ImageLoader previewSrc={attachment.get('preview_url')} src={url} width={attachment.getIn(['meta', 'original', 'width'])} height={attachment.getIn(['meta', 'original', 'height'])} />;
|
content = media.map((image) => {
|
||||||
|
const width = image.getIn(['meta', 'original', 'width']) || null;
|
||||||
|
const height = image.getIn(['meta', 'original', 'height']) || null;
|
||||||
|
|
||||||
|
return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} key={image.get('preview_url')} />;
|
||||||
|
}).toArray();
|
||||||
} else if (attachment.get('type') === 'gifv') {
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
content = <ExtendedVideoPlayer src={url} muted controls={false} />;
|
content = <ExtendedVideoPlayer src={url} muted controls={false} />;
|
||||||
}
|
}
|
||||||
|
@ -85,9 +94,9 @@ export default class MediaModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='media-modal__content'>
|
<div className='media-modal__content'>
|
||||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
|
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
|
||||||
<ReactSwipeable onSwipedRight={this.handlePrevClick} onSwipedLeft={this.handleNextClick}>
|
<ReactSwipeableViews onChangeIndex={this.handleSwipe} index={index} animateHeight>
|
||||||
{content}
|
{content}
|
||||||
</ReactSwipeable>
|
</ReactSwipeableViews>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{rightNav}
|
{rightNav}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import LoadingIndicator from '../../../components/loading_indicator';
|
||||||
|
|
||||||
|
// Keep the markup in sync with <BundleModalError />
|
||||||
|
// (make sure they have the same dimensions)
|
||||||
|
const ModalLoading = () => (
|
||||||
|
<div className='modal-root__modal error-modal'>
|
||||||
|
<div className='error-modal__body'>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
<div className='error-modal__footer'>
|
||||||
|
<div>
|
||||||
|
<button className='error-modal__nav onboarding-modal__skip' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ModalLoading;
|
|
@ -1,14 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MediaModal from './media_modal';
|
|
||||||
import OnboardingModal from './onboarding_modal';
|
|
||||||
import VideoModal from './video_modal';
|
|
||||||
import BoostModal from './boost_modal';
|
|
||||||
import ConfirmationModal from './confirmation_modal';
|
|
||||||
import ReportModal from './report_modal';
|
|
||||||
import SettingsContainer from '../../../../glitch/containers/settings';
|
|
||||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
|
import BundleContainer from '../containers/bundle_container';
|
||||||
|
import BundleModalError from './bundle_modal_error';
|
||||||
|
import ModalLoading from './modal_loading';
|
||||||
|
import {
|
||||||
|
MediaModal,
|
||||||
|
OnboardingModal,
|
||||||
|
VideoModal,
|
||||||
|
BoostModal,
|
||||||
|
ConfirmationModal,
|
||||||
|
ReportModal,
|
||||||
|
SettingsModal,
|
||||||
|
} from '../../../features/ui/util/async-components';
|
||||||
|
|
||||||
const MODAL_COMPONENTS = {
|
const MODAL_COMPONENTS = {
|
||||||
'MEDIA': MediaModal,
|
'MEDIA': MediaModal,
|
||||||
|
@ -17,7 +22,7 @@ const MODAL_COMPONENTS = {
|
||||||
'BOOST': BoostModal,
|
'BOOST': BoostModal,
|
||||||
'CONFIRM': ConfirmationModal,
|
'CONFIRM': ConfirmationModal,
|
||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'SETTINGS': SettingsContainer,
|
'SETTINGS': SettingsModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
@ -51,6 +56,22 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
return { opacity: spring(0), scale: spring(0.98) };
|
return { opacity: spring(0), scale: spring(0.98) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderModal = (SpecificComponent) => {
|
||||||
|
const { props, onClose } = this.props;
|
||||||
|
|
||||||
|
return <SpecificComponent {...props} onClose={onClose} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoading = () => {
|
||||||
|
return <ModalLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError = (props) => {
|
||||||
|
const { onClose } = this.props;
|
||||||
|
|
||||||
|
return <BundleModalError {...props} onClose={onClose} />;
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { type, props, onClose } = this.props;
|
const { type, props, onClose } = this.props;
|
||||||
const visible = !!type;
|
const visible = !!type;
|
||||||
|
@ -72,18 +93,14 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
>
|
>
|
||||||
{interpolatedStyles =>
|
{interpolatedStyles =>
|
||||||
<div className='modal-root'>
|
<div className='modal-root'>
|
||||||
{interpolatedStyles.map(({ key, data: { type, props }, style }) => {
|
{interpolatedStyles.map(({ key, data: { type }, style }) => (
|
||||||
const SpecificComponent = MODAL_COMPONENTS[type];
|
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
||||||
|
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
|
||||||
return (
|
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||||
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>{this.renderModal}</BundleContainer>
|
||||||
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
|
|
||||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
|
||||||
<SpecificComponent {...props} onClose={onClose} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</TransitionMotion>
|
</TransitionMotion>
|
||||||
|
|
|
@ -3,16 +3,14 @@ import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ReactSwipeable from 'react-swipeable';
|
import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Permalink from '../../../components/permalink';
|
import Permalink from '../../../components/permalink';
|
||||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
import ComposeForm from '../../compose/components/compose_form';
|
import ComposeForm from '../../compose/components/compose_form';
|
||||||
import Search from '../../compose/components/search';
|
import Search from '../../compose/components/search';
|
||||||
import NavigationBar from '../../compose/components/navigation_bar';
|
import NavigationBar from '../../compose/components/navigation_bar';
|
||||||
import ColumnHeader from './column_header';
|
import ColumnHeader from './column_header';
|
||||||
import Immutable from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const noop = () => { };
|
const noop = () => { };
|
||||||
|
|
||||||
|
@ -50,7 +48,7 @@ const PageTwo = ({ me }) => (
|
||||||
</div>
|
</div>
|
||||||
<ComposeForm
|
<ComposeForm
|
||||||
text='Awoo! #introductions'
|
text='Awoo! #introductions'
|
||||||
suggestions={Immutable.List()}
|
suggestions={ImmutableList()}
|
||||||
mentionedDomains={[]}
|
mentionedDomains={[]}
|
||||||
spoiler={false}
|
spoiler={false}
|
||||||
onChange={noop}
|
onChange={noop}
|
||||||
|
@ -227,6 +225,10 @@ export default class OnboardingModal extends React.PureComponent {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSwipe = (index) => {
|
||||||
|
this.setState({ currentIndex: index });
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyUp = ({ key }) => {
|
handleKeyUp = ({ key }) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
|
@ -263,30 +265,18 @@ export default class OnboardingModal extends React.PureComponent {
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = pages.map((data, i) => ({
|
|
||||||
key: `page-${i}`,
|
|
||||||
data,
|
|
||||||
style: {
|
|
||||||
opacity: spring(i === currentIndex ? 1 : 0),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root__modal onboarding-modal'>
|
<div className='modal-root__modal onboarding-modal'>
|
||||||
<TransitionMotion styles={styles}>
|
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'>
|
||||||
{interpolatedStyles => (
|
{pages.map((page, i) => {
|
||||||
<ReactSwipeable onSwipedRight={this.handlePrev} onSwipedLeft={this.handleNext} className='onboarding-modal__pager'>
|
const className = classNames('onboarding-modal__page__wrapper', {
|
||||||
{interpolatedStyles.map(({ key, data, style }, i) => {
|
'onboarding-modal__page__wrapper--active': i === currentIndex,
|
||||||
const className = classNames('onboarding-modal__page__wrapper', {
|
});
|
||||||
'onboarding-modal__page__wrapper--active': i === currentIndex,
|
return (
|
||||||
});
|
<div key={i} className={className}>{page}</div>
|
||||||
return (
|
);
|
||||||
<div key={key} style={style} className={className}>{data}</div>
|
})}
|
||||||
);
|
</ReactSwipeableViews>
|
||||||
})}
|
|
||||||
</ReactSwipeable>
|
|
||||||
)}
|
|
||||||
</TransitionMotion>
|
|
||||||
|
|
||||||
<div className='onboarding-modal__paginator'>
|
<div className='onboarding-modal__paginator'>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { makeGetAccount } from '../../../selectors';
|
import { makeGetAccount } from '../../../selectors';
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import StatusCheckBox from '../../report/containers/status_check_box_container';
|
import StatusCheckBox from '../../report/containers/status_check_box_container';
|
||||||
import Immutable from 'immutable';
|
import { OrderedSet } from 'immutable';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Button from '../../../components/button';
|
import Button from '../../../components/button';
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ const makeMapStateToProps = () => {
|
||||||
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
||||||
account: getAccount(state, accountId),
|
account: getAccount(state, accountId),
|
||||||
comment: state.getIn(['reports', 'new', 'comment']),
|
comment: state.getIn(['reports', 'new', 'comment']),
|
||||||
statusIds: Immutable.OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
|
statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import NavLink from 'react-router-dom/NavLink';
|
import NavLink from 'react-router-dom/NavLink';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const links = [
|
export const links = [
|
||||||
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
|
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||||
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
<NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
||||||
|
@ -13,25 +13,13 @@ const links = [
|
||||||
<NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></NavLink>,
|
<NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></NavLink>,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getPreviousLink (path) {
|
export function getIndex (path) {
|
||||||
const index = links.findIndex(link => link.props.to === path);
|
return links.findIndex(link => link.props.to === path);
|
||||||
|
}
|
||||||
|
|
||||||
if (index > 0) {
|
export function getLink (index) {
|
||||||
return links[index - 1].props.to;
|
return links[index].props.to;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getNextLink (path) {
|
|
||||||
const index = links.findIndex(link => link.props.to === path);
|
|
||||||
|
|
||||||
if (index !== -1 && index < links.length - 1) {
|
|
||||||
return links[index + 1].props.to;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class TabsBar extends React.Component {
|
export default class TabsBar extends React.Component {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import Bundle from '../components/bundle';
|
||||||
|
|
||||||
|
import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles';
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onFetch () {
|
||||||
|
dispatch(fetchBundleRequest());
|
||||||
|
},
|
||||||
|
onFetchSuccess () {
|
||||||
|
dispatch(fetchBundleSuccess());
|
||||||
|
},
|
||||||
|
onFetchFail (error) {
|
||||||
|
dispatch(fetchBundleFail(error));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(Bundle);
|
|
@ -1,13 +1,13 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import StatusList from '../../../components/status_list';
|
import StatusList from '../../../components/status_list';
|
||||||
import { scrollTopTimeline } from '../../../actions/timelines';
|
import { scrollTopTimeline } from '../../../actions/timelines';
|
||||||
import Immutable from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
const makeGetStatusIds = () => createSelector([
|
const makeGetStatusIds = () => createSelector([
|
||||||
(state, { type }) => state.getIn(['settings', type], Immutable.Map()),
|
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||||
(state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()),
|
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
||||||
(state) => state.get('statuses'),
|
(state) => state.get('statuses'),
|
||||||
(state) => state.getIn(['meta', 'me']),
|
(state) => state.getIn(['meta', 'me']),
|
||||||
], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => {
|
], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Switch from 'react-router-dom/Switch';
|
import classNames from 'classnames';
|
||||||
import Route from 'react-router-dom/Route';
|
|
||||||
import Redirect from 'react-router-dom/Redirect';
|
import Redirect from 'react-router-dom/Redirect';
|
||||||
import NotificationsContainer from './containers/notifications_container';
|
import NotificationsContainer from './containers/notifications_container';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -13,66 +12,37 @@ import { debounce } from 'lodash';
|
||||||
import { uploadCompose } from '../../actions/compose';
|
import { uploadCompose } from '../../actions/compose';
|
||||||
import { refreshHomeTimeline } from '../../actions/timelines';
|
import { refreshHomeTimeline } from '../../actions/timelines';
|
||||||
import { refreshNotifications } from '../../actions/notifications';
|
import { refreshNotifications } from '../../actions/notifications';
|
||||||
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
import UploadArea from './components/upload_area';
|
import UploadArea from './components/upload_area';
|
||||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||||
import Status from '../../features/status';
|
import {
|
||||||
import GettingStarted from '../../features/getting_started';
|
Compose,
|
||||||
import PublicTimeline from '../../features/public_timeline';
|
Status,
|
||||||
import CommunityTimeline from '../../features/community_timeline';
|
GettingStarted,
|
||||||
import AccountTimeline from '../../features/account_timeline';
|
PublicTimeline,
|
||||||
import AccountGallery from '../../features/account_gallery';
|
CommunityTimeline,
|
||||||
import HomeTimeline from '../../features/home_timeline';
|
AccountTimeline,
|
||||||
import Compose from '../../features/compose';
|
AccountGallery,
|
||||||
import Followers from '../../features/followers';
|
HomeTimeline,
|
||||||
import Following from '../../features/following';
|
Followers,
|
||||||
import Reblogs from '../../features/reblogs';
|
Following,
|
||||||
import Favourites from '../../features/favourites';
|
Reblogs,
|
||||||
import HashtagTimeline from '../../features/hashtag_timeline';
|
Favourites,
|
||||||
import Notifications from '../../features/notifications';
|
HashtagTimeline,
|
||||||
import FollowRequests from '../../features/follow_requests';
|
Notifications,
|
||||||
import GenericNotFound from '../../features/generic_not_found';
|
FollowRequests,
|
||||||
import FavouritedStatuses from '../../features/favourited_statuses';
|
GenericNotFound,
|
||||||
import Blocks from '../../features/blocks';
|
FavouritedStatuses,
|
||||||
import Mutes from '../../features/mutes';
|
Blocks,
|
||||||
|
Mutes,
|
||||||
|
} from './util/async-components';
|
||||||
|
|
||||||
// Small wrapper to pass multiColumn to the route components
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
const WrappedSwitch = ({ multiColumn, children }) => (
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
<Switch>
|
import '../../../glitch/components/status';
|
||||||
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
|
|
||||||
WrappedSwitch.propTypes = {
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Small Wraper to extract the params from the route and pass
|
|
||||||
// them to the rendered component, together with the content to
|
|
||||||
// be rendered inside (the children)
|
|
||||||
class WrappedRoute extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
component: PropTypes.func.isRequired,
|
|
||||||
content: PropTypes.node,
|
|
||||||
multiColumn: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
renderComponent = ({ match: { params } }) => {
|
|
||||||
const { component: Component, content, multiColumn } = this.props;
|
|
||||||
|
|
||||||
return <Component params={params} multiColumn={multiColumn}>{content}</Component>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { component: Component, content, ...rest } = this.props;
|
|
||||||
|
|
||||||
return <Route {...rest} render={this.renderComponent} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
systemFontUi: state.getIn(['meta', 'system_font_ui']),
|
||||||
layout: state.getIn(['local_settings', 'layout']),
|
layout: state.getIn(['local_settings', 'layout']),
|
||||||
isWide: state.getIn(['local_settings', 'stretch']),
|
isWide: state.getIn(['local_settings', 'stretch']),
|
||||||
});
|
});
|
||||||
|
@ -85,6 +55,7 @@ export default class UI extends React.PureComponent {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
layout: PropTypes.string,
|
layout: PropTypes.string,
|
||||||
isWide: PropTypes.bool,
|
isWide: PropTypes.bool,
|
||||||
|
systemFontUi: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -194,8 +165,13 @@ export default class UI extends React.PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const className = classNames('ui', columnsClass(layout), {
|
||||||
|
'wide': isWide,
|
||||||
|
'system-font': this.props.systemFontUi,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'ui ' + columnsClass(layout) + (isWide ? ' wide' : '')} ref={this.setRef}>
|
<div className={className} ref={this.setRef}>
|
||||||
<TabsBar />
|
<TabsBar />
|
||||||
<ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
|
<ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
// Get the bounding client rect from an IntersectionObserver entry.
|
||||||
|
// This is to work around a bug in Chrome: https://crbug.com/737228
|
||||||
|
|
||||||
|
let hasBoundingRectBug;
|
||||||
|
|
||||||
|
function getRectFromEntry(entry) {
|
||||||
|
if (typeof hasBoundingRectBug !== 'boolean') {
|
||||||
|
const boundingRect = entry.target.getBoundingClientRect();
|
||||||
|
const observerRect = entry.boundingClientRect;
|
||||||
|
hasBoundingRectBug = boundingRect.height !== observerRect.height ||
|
||||||
|
boundingRect.top !== observerRect.top ||
|
||||||
|
boundingRect.width !== observerRect.width ||
|
||||||
|
boundingRect.bottom !== observerRect.bottom ||
|
||||||
|
boundingRect.left !== observerRect.left ||
|
||||||
|
boundingRect.right !== observerRect.right;
|
||||||
|
}
|
||||||
|
return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getRectFromEntry;
|
|
@ -37,9 +37,18 @@ class IntersectionObserverWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unobserve (id, node) {
|
||||||
|
if (this.observer) {
|
||||||
|
delete this.callbacks[id];
|
||||||
|
this.observer.unobserve(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disconnect () {
|
disconnect () {
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
|
this.callbacks = {};
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
|
this.observer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Switch from 'react-router-dom/Switch';
|
||||||
|
import Route from 'react-router-dom/Route';
|
||||||
|
|
||||||
|
import ColumnLoading from '../components/column_loading';
|
||||||
|
import BundleColumnError from '../components/bundle_column_error';
|
||||||
|
import BundleContainer from '../containers/bundle_container';
|
||||||
|
|
||||||
|
// Small wrapper to pass multiColumn to the route components
|
||||||
|
export const WrappedSwitch = ({ multiColumn, children }) => (
|
||||||
|
<Switch>
|
||||||
|
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
|
||||||
|
WrappedSwitch.propTypes = {
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Small Wraper to extract the params from the route and pass
|
||||||
|
// them to the rendered component, together with the content to
|
||||||
|
// be rendered inside (the children)
|
||||||
|
export class WrappedRoute extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
component: PropTypes.func.isRequired,
|
||||||
|
content: PropTypes.node,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
renderComponent = ({ match }) => {
|
||||||
|
const { component, content, multiColumn } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
|
||||||
|
{Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>}
|
||||||
|
</BundleContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoading = () => {
|
||||||
|
return <ColumnLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError = (props) => {
|
||||||
|
return <BundleColumnError {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { component: Component, content, ...rest } = this.props;
|
||||||
|
|
||||||
|
return <Route {...rest} render={this.renderComponent} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "إلغاء المتابعة",
|
"account.unfollow": "إلغاء المتابعة",
|
||||||
"account.unmute": "إلغاء الكتم عن @{name}",
|
"account.unmute": "إلغاء الكتم عن @{name}",
|
||||||
"boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
|
"boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "الحسابات المحجوبة",
|
"column.blocks": "الحسابات المحجوبة",
|
||||||
"column.community": "الخيط العام المحلي",
|
"column.community": "الخيط العام المحلي",
|
||||||
"column.favourites": "المفضلة",
|
"column.favourites": "المفضلة",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة",
|
"privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة",
|
||||||
"privacy.unlisted.short": "غير مدرج",
|
"privacy.unlisted.short": "غير مدرج",
|
||||||
"reply_indicator.cancel": "إلغاء",
|
"reply_indicator.cancel": "إلغاء",
|
||||||
"report.heading": "تقرير جديد",
|
|
||||||
"report.placeholder": "تعليقات إضافية",
|
"report.placeholder": "تعليقات إضافية",
|
||||||
"report.submit": "إرسال",
|
"report.submit": "إرسال",
|
||||||
"report.target": "إبلاغ",
|
"report.target": "إبلاغ",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Не следвай",
|
"account.unfollow": "Не следвай",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "Do not show in public timelines",
|
"privacy.unlisted.long": "Do not show in public timelines",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Unlisted",
|
||||||
"reply_indicator.cancel": "Отказ",
|
"reply_indicator.cancel": "Отказ",
|
||||||
"report.heading": "New report",
|
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Deixar de seguir",
|
"account.unfollow": "Deixar de seguir",
|
||||||
"account.unmute": "Treure silenci de @{name}",
|
"account.unmute": "Treure silenci de @{name}",
|
||||||
"boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
|
"boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Usuaris bloquejats",
|
"column.blocks": "Usuaris bloquejats",
|
||||||
"column.community": "Línia de temps local",
|
"column.community": "Línia de temps local",
|
||||||
"column.favourites": "Favorits",
|
"column.favourites": "Favorits",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "No publicar en línies de temps públiques",
|
"privacy.unlisted.long": "No publicar en línies de temps públiques",
|
||||||
"privacy.unlisted.short": "No llistat",
|
"privacy.unlisted.short": "No llistat",
|
||||||
"reply_indicator.cancel": "Cancel·lar",
|
"reply_indicator.cancel": "Cancel·lar",
|
||||||
"report.heading": "Nou informe",
|
|
||||||
"report.placeholder": "Comentaris addicionals",
|
"report.placeholder": "Comentaris addicionals",
|
||||||
"report.submit": "Enviar",
|
"report.submit": "Enviar",
|
||||||
"report.target": "Informes",
|
"report.target": "Informes",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Entfolgen",
|
"account.unfollow": "Entfolgen",
|
||||||
"account.unmute": "@{name} nicht mehr stummschalten",
|
"account.unmute": "@{name} nicht mehr stummschalten",
|
||||||
"boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
|
"boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blockierte Benutzer",
|
"column.blocks": "Blockierte Benutzer",
|
||||||
"column.community": "Lokale Zeitleiste",
|
"column.community": "Lokale Zeitleiste",
|
||||||
"column.favourites": "Favoriten",
|
"column.favourites": "Favoriten",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
|
"privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
|
||||||
"privacy.unlisted.short": "Nicht gelistet",
|
"privacy.unlisted.short": "Nicht gelistet",
|
||||||
"reply_indicator.cancel": "Abbrechen",
|
"reply_indicator.cancel": "Abbrechen",
|
||||||
"report.heading": "Neue Meldung",
|
|
||||||
"report.placeholder": "Zusätzliche Kommentare",
|
"report.placeholder": "Zusätzliche Kommentare",
|
||||||
"report.submit": "Absenden",
|
"report.submit": "Absenden",
|
||||||
"report.target": "Melden",
|
"report.target": "Melden",
|
||||||
|
|
|
@ -643,6 +643,14 @@
|
||||||
"defaultMessage": "Getting started",
|
"defaultMessage": "Getting started",
|
||||||
"id": "getting_started.heading"
|
"id": "getting_started.heading"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Home",
|
||||||
|
"id": "tabs_bar.home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Notifications",
|
||||||
|
"id": "tabs_bar.notifications"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Federated timeline",
|
"defaultMessage": "Federated timeline",
|
||||||
"id": "navigation_bar.public_timeline"
|
"id": "navigation_bar.public_timeline"
|
||||||
|
@ -956,27 +964,6 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/public_timeline/index.json"
|
"path": "app/javascript/mastodon/features/public_timeline/index.json"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"descriptors": [
|
|
||||||
{
|
|
||||||
"defaultMessage": "New report",
|
|
||||||
"id": "report.heading"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Additional comments",
|
|
||||||
"id": "report.placeholder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Submit",
|
|
||||||
"id": "report.submit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Reporting",
|
|
||||||
"id": "report.target"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"path": "app/javascript/mastodon/features/report/index.json"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -1036,6 +1023,40 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/ui/components/boost_modal.json"
|
"path": "app/javascript/mastodon/features/ui/components/boost_modal.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Network error",
|
||||||
|
"id": "bundle_column_error.title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Something went wrong while loading this component.",
|
||||||
|
"id": "bundle_column_error.body"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Try again",
|
||||||
|
"id": "bundle_column_error.retry"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/ui/components/bundle_column_error.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Something went wrong while loading this component.",
|
||||||
|
"id": "bundle_modal_error.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Try again",
|
||||||
|
"id": "bundle_modal_error.retry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Close",
|
||||||
|
"id": "bundle_modal_error.close"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/ui/components/bundle_modal_error.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Unfollow",
|
"account.unfollow": "Unfollow",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
|
@ -141,7 +147,6 @@
|
||||||
"privacy.unlisted.long": "Do not post to public timelines",
|
"privacy.unlisted.long": "Do not post to public timelines",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Unlisted",
|
||||||
"reply_indicator.cancel": "Cancel",
|
"reply_indicator.cancel": "Cancel",
|
||||||
"report.heading": "Report {target}",
|
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting {target}",
|
"report.target": "Reporting {target}",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Malsekvi",
|
"account.unfollow": "Malsekvi",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.community": "Loka tempolinio",
|
"column.community": "Loka tempolinio",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "Do not show in public timelines",
|
"privacy.unlisted.long": "Do not show in public timelines",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Unlisted",
|
||||||
"reply_indicator.cancel": "Rezigni",
|
"reply_indicator.cancel": "Rezigni",
|
||||||
"report.heading": "New report",
|
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Dejar de seguir",
|
"account.unfollow": "Dejar de seguir",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Usuarios bloqueados",
|
"column.blocks": "Usuarios bloqueados",
|
||||||
"column.community": "Historia local",
|
"column.community": "Historia local",
|
||||||
"column.favourites": "Favoritos",
|
"column.favourites": "Favoritos",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "No mostrar en la historia federada",
|
"privacy.unlisted.long": "No mostrar en la historia federada",
|
||||||
"privacy.unlisted.short": "Sin federar",
|
"privacy.unlisted.short": "Sin federar",
|
||||||
"reply_indicator.cancel": "Cancelar",
|
"reply_indicator.cancel": "Cancelar",
|
||||||
"report.heading": "New report",
|
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "پایان پیگیری",
|
"account.unfollow": "پایان پیگیری",
|
||||||
"account.unmute": "باصدا کردن @{name}",
|
"account.unmute": "باصدا کردن @{name}",
|
||||||
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
|
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "کاربران مسدودشده",
|
"column.blocks": "کاربران مسدودشده",
|
||||||
"column.community": "نوشتههای محلی",
|
"column.community": "نوشتههای محلی",
|
||||||
"column.favourites": "پسندیدهها",
|
"column.favourites": "پسندیدهها",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "عمومی، ولی فهرست نکن",
|
"privacy.unlisted.long": "عمومی، ولی فهرست نکن",
|
||||||
"privacy.unlisted.short": "فهرستنشده",
|
"privacy.unlisted.short": "فهرستنشده",
|
||||||
"reply_indicator.cancel": "لغو",
|
"reply_indicator.cancel": "لغو",
|
||||||
"report.heading": "گزارش تازه",
|
|
||||||
"report.placeholder": "توضیح اضافه",
|
"report.placeholder": "توضیح اضافه",
|
||||||
"report.submit": "بفرست",
|
"report.submit": "بفرست",
|
||||||
"report.target": "گزارشدادن",
|
"report.target": "گزارشدادن",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Lopeta seuraaminen",
|
"account.unfollow": "Lopeta seuraaminen",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.community": "Paikallinen aikajana",
|
"column.community": "Paikallinen aikajana",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "Do not show in public timelines",
|
"privacy.unlisted.long": "Do not show in public timelines",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Unlisted",
|
||||||
"reply_indicator.cancel": "Peruuta",
|
"reply_indicator.cancel": "Peruuta",
|
||||||
"report.heading": "New report",
|
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"account.followers": "Abonné⋅e⋅s",
|
"account.followers": "Abonné⋅e⋅s",
|
||||||
"account.follows": "Abonnements",
|
"account.follows": "Abonnements",
|
||||||
"account.follows_you": "Vous suit",
|
"account.follows_you": "Vous suit",
|
||||||
"account.media": "Media",
|
"account.media": "Média",
|
||||||
"account.mention": "Mentionner",
|
"account.mention": "Mentionner",
|
||||||
"account.mute": "Masquer",
|
"account.mute": "Masquer",
|
||||||
"account.posts": "Statuts",
|
"account.posts": "Statuts",
|
||||||
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Ne plus suivre",
|
"account.unfollow": "Ne plus suivre",
|
||||||
"account.unmute": "Ne plus masquer",
|
"account.unmute": "Ne plus masquer",
|
||||||
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
|
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Comptes bloqués",
|
"column.blocks": "Comptes bloqués",
|
||||||
"column.community": "Fil public local",
|
"column.community": "Fil public local",
|
||||||
"column.favourites": "Favoris",
|
"column.favourites": "Favoris",
|
||||||
|
@ -31,10 +37,10 @@
|
||||||
"column_header.unpin": "Retirer",
|
"column_header.unpin": "Retirer",
|
||||||
"column_subheading.navigation": "Navigation",
|
"column_subheading.navigation": "Navigation",
|
||||||
"column_subheading.settings": "Paramètres",
|
"column_subheading.settings": "Paramètres",
|
||||||
"compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
|
"compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
|
||||||
"compose_form.lock_disclaimer.lock": "verrouillé",
|
"compose_form.lock_disclaimer.lock": "verrouillé",
|
||||||
"compose_form.placeholder": "Qu’avez-vous en tête ?",
|
"compose_form.placeholder": "Qu’avez-vous en tête ?",
|
||||||
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.",
|
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.",
|
||||||
"compose_form.publish": "Pouet ",
|
"compose_form.publish": "Pouet ",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.sensitive": "Marquer le média comme délicat",
|
"compose_form.sensitive": "Marquer le média comme délicat",
|
||||||
|
@ -42,13 +48,13 @@
|
||||||
"compose_form.spoiler_placeholder": "Avertissement",
|
"compose_form.spoiler_placeholder": "Avertissement",
|
||||||
"confirmation_modal.cancel": "Annuler",
|
"confirmation_modal.cancel": "Annuler",
|
||||||
"confirmations.block.confirm": "Bloquer",
|
"confirmations.block.confirm": "Bloquer",
|
||||||
"confirmations.block.message": "Confirmez vous le blocage de {name} ?",
|
"confirmations.block.message": "Confirmez vous le blocage de {name} ?",
|
||||||
"confirmations.delete.confirm": "Supprimer",
|
"confirmations.delete.confirm": "Supprimer",
|
||||||
"confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?",
|
"confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?",
|
||||||
"confirmations.domain_block.confirm": "Masquer le domaine entier",
|
"confirmations.domain_block.confirm": "Masquer le domaine entier",
|
||||||
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou silenciations ciblés sont suffisants et préférables.",
|
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou silenciations ciblés sont suffisants et préférables.",
|
||||||
"confirmations.mute.confirm": "Silencer",
|
"confirmations.mute.confirm": "Silencer",
|
||||||
"confirmations.mute.message": "Confirmez vous la silenciation {name} ?",
|
"confirmations.mute.message": "Confirmez vous la silenciation {name} ?",
|
||||||
"emoji_button.activity": "Activités",
|
"emoji_button.activity": "Activités",
|
||||||
"emoji_button.flags": "Drapeaux",
|
"emoji_button.flags": "Drapeaux",
|
||||||
"emoji_button.food": "Boire et manger",
|
"emoji_button.food": "Boire et manger",
|
||||||
|
@ -59,20 +65,20 @@
|
||||||
"emoji_button.search": "Recherche…",
|
"emoji_button.search": "Recherche…",
|
||||||
"emoji_button.symbols": "Symboles",
|
"emoji_button.symbols": "Symboles",
|
||||||
"emoji_button.travel": "Lieux et voyages",
|
"emoji_button.travel": "Lieux et voyages",
|
||||||
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
|
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
|
||||||
"empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag",
|
"empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag",
|
||||||
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateurs⋅trices.",
|
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
|
||||||
"empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
|
"empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
|
||||||
"empty_column.home.public_timeline": "le fil public",
|
"empty_column.home.public_timeline": "le fil public",
|
||||||
"empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
|
"empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
|
||||||
"empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs⋅trices d’autres instances pour remplir le fil public.",
|
"empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
|
||||||
"follow_request.authorize": "Autoriser",
|
"follow_request.authorize": "Autoriser",
|
||||||
"follow_request.reject": "Rejeter",
|
"follow_request.reject": "Rejeter",
|
||||||
"getting_started.appsshort": "Applications",
|
"getting_started.appsshort": "Applications",
|
||||||
"getting_started.faq": "FAQ",
|
"getting_started.faq": "FAQ",
|
||||||
"getting_started.heading": "Pour commencer",
|
"getting_started.heading": "Pour commencer",
|
||||||
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
|
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
|
||||||
"getting_started.userguide": "Guide d'utilisation",
|
"getting_started.userguide": "Guide d’utilisation",
|
||||||
"home.column_settings.advanced": "Avancé",
|
"home.column_settings.advanced": "Avancé",
|
||||||
"home.column_settings.basic": "Basique",
|
"home.column_settings.basic": "Basique",
|
||||||
"home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
|
"home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
|
||||||
|
@ -93,37 +99,37 @@
|
||||||
"navigation_bar.mutes": "Comptes silencés",
|
"navigation_bar.mutes": "Comptes silencés",
|
||||||
"navigation_bar.preferences": "Préférences",
|
"navigation_bar.preferences": "Préférences",
|
||||||
"navigation_bar.public_timeline": "Fil public global",
|
"navigation_bar.public_timeline": "Fil public global",
|
||||||
"notification.favourite": "{name} a ajouté à ses favoris :",
|
"notification.favourite": "{name} a ajouté à ses favoris :",
|
||||||
"notification.follow": "{name} vous suit.",
|
"notification.follow": "{name} vous suit.",
|
||||||
"notification.mention": "{name} vous a mentionné⋅e :",
|
"notification.mention": "{name} vous a mentionné⋅e :",
|
||||||
"notification.reblog": "{name} a partagé votre statut :",
|
"notification.reblog": "{name} a partagé votre statut :",
|
||||||
"notifications.clear": "Nettoyer",
|
"notifications.clear": "Nettoyer",
|
||||||
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
|
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
|
||||||
"notifications.column_settings.alert": "Notifications locales",
|
"notifications.column_settings.alert": "Notifications locales",
|
||||||
"notifications.column_settings.favourite": "Favoris :",
|
"notifications.column_settings.favourite": "Favoris :",
|
||||||
"notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
|
"notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
|
||||||
"notifications.column_settings.mention": "Mentions :",
|
"notifications.column_settings.mention": "Mentions :",
|
||||||
"notifications.column_settings.reblog": "Partages :",
|
"notifications.column_settings.reblog": "Partages :",
|
||||||
"notifications.column_settings.show": "Afficher dans la colonne",
|
"notifications.column_settings.show": "Afficher dans la colonne",
|
||||||
"notifications.column_settings.sound": "Émettre un son",
|
"notifications.column_settings.sound": "Émettre un son",
|
||||||
"onboarding.done": "Effectué",
|
"onboarding.done": "Effectué",
|
||||||
"onboarding.next": "Suivant",
|
"onboarding.next": "Suivant",
|
||||||
"onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.",
|
"onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateur⋅ice⋅s de {domain}.",
|
||||||
"onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez",
|
"onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s que vous suivez",
|
||||||
"onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous",
|
"onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous",
|
||||||
"onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.",
|
"onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.",
|
||||||
"onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅trice complet est {handle}",
|
"onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅ice complet est {handle}",
|
||||||
"onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
|
"onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
|
||||||
"onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}",
|
"onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}",
|
||||||
"onboarding.page_six.almost_done": "Nous y sommes presque…",
|
"onboarding.page_six.almost_done": "Nous y sommes presque…",
|
||||||
"onboarding.page_six.appetoot": "Bon Appetoot!",
|
"onboarding.page_six.appetoot": "Bon Appetoot!",
|
||||||
"onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!",
|
"onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!",
|
||||||
"onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
|
"onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
|
||||||
"onboarding.page_six.guidelines": "règles de la communauté",
|
"onboarding.page_six.guidelines": "règles de la communauté",
|
||||||
"onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
|
"onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
|
||||||
"onboarding.page_six.various_app": "applications mobiles",
|
"onboarding.page_six.various_app": "applications mobiles",
|
||||||
"onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.",
|
"onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.",
|
||||||
"onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅trice complet.",
|
"onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅ice complet.",
|
||||||
"onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.",
|
"onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.",
|
||||||
"onboarding.skip": "Passer",
|
"onboarding.skip": "Passer",
|
||||||
"privacy.change": "Ajuster la confidentialité du message",
|
"privacy.change": "Ajuster la confidentialité du message",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "Ne pas afficher dans les fils publics",
|
"privacy.unlisted.long": "Ne pas afficher dans les fils publics",
|
||||||
"privacy.unlisted.short": "Non-listé",
|
"privacy.unlisted.short": "Non-listé",
|
||||||
"reply_indicator.cancel": "Annuler",
|
"reply_indicator.cancel": "Annuler",
|
||||||
"report.heading": "Nouveau signalement",
|
|
||||||
"report.placeholder": "Commentaires additionnels",
|
"report.placeholder": "Commentaires additionnels",
|
||||||
"report.submit": "Envoyer",
|
"report.submit": "Envoyer",
|
||||||
"report.target": "Signalement",
|
"report.target": "Signalement",
|
||||||
|
@ -151,7 +156,7 @@
|
||||||
"status.mute_conversation": "Masquer la conversation",
|
"status.mute_conversation": "Masquer la conversation",
|
||||||
"status.open": "Déplier ce statut",
|
"status.open": "Déplier ce statut",
|
||||||
"status.reblog": "Partager",
|
"status.reblog": "Partager",
|
||||||
"status.reblogged_by": "{name} a partagé :",
|
"status.reblogged_by": "{name} a partagé :",
|
||||||
"status.reply": "Répondre",
|
"status.reply": "Répondre",
|
||||||
"status.replyAll": "Répondre au fil",
|
"status.replyAll": "Répondre au fil",
|
||||||
"status.report": "Signaler @{name}",
|
"status.report": "Signaler @{name}",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "הפסקת מעקב",
|
"account.unfollow": "הפסקת מעקב",
|
||||||
"account.unmute": "הפסקת השתקת @{name}",
|
"account.unmute": "הפסקת השתקת @{name}",
|
||||||
"boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
|
"boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "חסימות",
|
"column.blocks": "חסימות",
|
||||||
"column.community": "ציר זמן מקומי",
|
"column.community": "ציר זמן מקומי",
|
||||||
"column.favourites": "חיבובים",
|
"column.favourites": "חיבובים",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים",
|
"privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים",
|
||||||
"privacy.unlisted.short": "לא לפיד הכללי",
|
"privacy.unlisted.short": "לא לפיד הכללי",
|
||||||
"reply_indicator.cancel": "ביטול",
|
"reply_indicator.cancel": "ביטול",
|
||||||
"report.heading": "דווח חדש",
|
|
||||||
"report.placeholder": "הערות נוספות",
|
"report.placeholder": "הערות נוספות",
|
||||||
"report.submit": "שליחה",
|
"report.submit": "שליחה",
|
||||||
"report.target": "דיווח",
|
"report.target": "דיווח",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Prestani slijediti",
|
"account.unfollow": "Prestani slijediti",
|
||||||
"account.unmute": "Poništi utišavanje @{name}",
|
"account.unmute": "Poništi utišavanje @{name}",
|
||||||
"boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
|
"boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blokirani korisnici",
|
"column.blocks": "Blokirani korisnici",
|
||||||
"column.community": "Lokalni timeline",
|
"column.community": "Lokalni timeline",
|
||||||
"column.favourites": "Favoriti",
|
"column.favourites": "Favoriti",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "Ne prikazuj u javnim timelineovima",
|
"privacy.unlisted.long": "Ne prikazuj u javnim timelineovima",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Unlisted",
|
||||||
"reply_indicator.cancel": "Otkaži",
|
"reply_indicator.cancel": "Otkaži",
|
||||||
"report.heading": "Nova prijava",
|
|
||||||
"report.placeholder": "Dodatni komentari",
|
"report.placeholder": "Dodatni komentari",
|
||||||
"report.submit": "Pošalji",
|
"report.submit": "Pošalji",
|
||||||
"report.target": "Prijavljivanje",
|
"report.target": "Prijavljivanje",
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
"account.unfollow": "Követés abbahagyása",
|
"account.unfollow": "Követés abbahagyása",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
|
"bundle_modal_error.close": "Close",
|
||||||
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
|
@ -136,7 +142,6 @@
|
||||||
"privacy.unlisted.long": "Do not show in public timelines",
|
"privacy.unlisted.long": "Do not show in public timelines",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.short": "Unlisted",
|
||||||
"reply_indicator.cancel": "Mégsem",
|
"reply_indicator.cancel": "Mégsem",
|
||||||
"report.heading": "New report",
|
|
||||||
"report.placeholder": "Additional comments",
|
"report.placeholder": "Additional comments",
|
||||||
"report.submit": "Submit",
|
"report.submit": "Submit",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue